aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav
diff options
context:
space:
mode:
Diffstat (limited to 'apps/dav')
-rw-r--r--apps/dav/.gitignore2
-rw-r--r--apps/dav/.l10nignore2
-rw-r--r--apps/dav/appinfo/info.xml36
-rw-r--r--apps/dav/appinfo/routes.php30
-rw-r--r--apps/dav/appinfo/v1/caldav.php111
-rw-r--r--apps/dav/appinfo/v1/carddav.php104
-rw-r--r--apps/dav/appinfo/v1/publicwebdav.php143
-rw-r--r--apps/dav/appinfo/v1/webdav.php106
-rw-r--r--apps/dav/appinfo/v2/direct.php46
-rw-r--r--apps/dav/appinfo/v2/publicremote.php152
-rw-r--r--apps/dav/appinfo/v2/remote.php33
-rw-r--r--apps/dav/bin/chunkperf.php79
-rw-r--r--apps/dav/composer/autoload.php17
-rw-r--r--apps/dav/composer/composer/ClassLoader.php137
-rw-r--r--apps/dav/composer/composer/InstalledVersions.php31
-rw-r--r--apps/dav/composer/composer/autoload_classmap.php118
-rw-r--r--apps/dav/composer/composer/autoload_static.php118
-rw-r--r--apps/dav/composer/composer/installed.php6
-rw-r--r--apps/dav/css/schedule-response.css15
-rw-r--r--apps/dav/img/calendar.svg1
-rw-r--r--apps/dav/img/calendar.svg.license2
-rw-r--r--apps/dav/img/schedule.svg1
-rw-r--r--apps/dav/img/schedule.svg.license2
-rw-r--r--apps/dav/l10n/ar.js326
-rw-r--r--apps/dav/l10n/ar.json324
-rw-r--r--apps/dav/l10n/ast.js224
-rw-r--r--apps/dav/l10n/ast.json222
-rw-r--r--apps/dav/l10n/bg.js120
-rw-r--r--apps/dav/l10n/bg.json120
-rw-r--r--apps/dav/l10n/ca.js320
-rw-r--r--apps/dav/l10n/ca.json320
-rw-r--r--apps/dav/l10n/cs.js243
-rw-r--r--apps/dav/l10n/cs.json243
-rw-r--r--apps/dav/l10n/da.js277
-rw-r--r--apps/dav/l10n/da.json277
-rw-r--r--apps/dav/l10n/de.js262
-rw-r--r--apps/dav/l10n/de.json262
-rw-r--r--apps/dav/l10n/de_DE.js244
-rw-r--r--apps/dav/l10n/de_DE.json244
-rw-r--r--apps/dav/l10n/el.js119
-rw-r--r--apps/dav/l10n/el.json117
-rw-r--r--apps/dav/l10n/en_GB.js307
-rw-r--r--apps/dav/l10n/en_GB.json307
-rw-r--r--apps/dav/l10n/eo.js105
-rw-r--r--apps/dav/l10n/eo.json103
-rw-r--r--apps/dav/l10n/es.js310
-rw-r--r--apps/dav/l10n/es.json310
-rw-r--r--apps/dav/l10n/es_419.js66
-rw-r--r--apps/dav/l10n/es_419.json64
-rw-r--r--apps/dav/l10n/es_AR.js56
-rw-r--r--apps/dav/l10n/es_AR.json54
-rw-r--r--apps/dav/l10n/es_CL.js66
-rw-r--r--apps/dav/l10n/es_CL.json64
-rw-r--r--apps/dav/l10n/es_CO.js66
-rw-r--r--apps/dav/l10n/es_CO.json64
-rw-r--r--apps/dav/l10n/es_CR.js66
-rw-r--r--apps/dav/l10n/es_CR.json64
-rw-r--r--apps/dav/l10n/es_DO.js66
-rw-r--r--apps/dav/l10n/es_DO.json64
-rw-r--r--apps/dav/l10n/es_EC.js182
-rw-r--r--apps/dav/l10n/es_EC.json182
-rw-r--r--apps/dav/l10n/es_GT.js66
-rw-r--r--apps/dav/l10n/es_GT.json64
-rw-r--r--apps/dav/l10n/es_HN.js66
-rw-r--r--apps/dav/l10n/es_HN.json64
-rw-r--r--apps/dav/l10n/es_MX.js224
-rw-r--r--apps/dav/l10n/es_MX.json224
-rw-r--r--apps/dav/l10n/es_NI.js66
-rw-r--r--apps/dav/l10n/es_NI.json64
-rw-r--r--apps/dav/l10n/es_PA.js66
-rw-r--r--apps/dav/l10n/es_PA.json64
-rw-r--r--apps/dav/l10n/es_PE.js66
-rw-r--r--apps/dav/l10n/es_PE.json64
-rw-r--r--apps/dav/l10n/es_PR.js66
-rw-r--r--apps/dav/l10n/es_PR.json64
-rw-r--r--apps/dav/l10n/es_PY.js66
-rw-r--r--apps/dav/l10n/es_PY.json64
-rw-r--r--apps/dav/l10n/es_SV.js66
-rw-r--r--apps/dav/l10n/es_SV.json64
-rw-r--r--apps/dav/l10n/es_UY.js66
-rw-r--r--apps/dav/l10n/es_UY.json64
-rw-r--r--apps/dav/l10n/et_EE.js315
-rw-r--r--apps/dav/l10n/et_EE.json315
-rw-r--r--apps/dav/l10n/eu.js336
-rw-r--r--apps/dav/l10n/eu.json336
-rw-r--r--apps/dav/l10n/fa.js326
-rw-r--r--apps/dav/l10n/fa.json324
-rw-r--r--apps/dav/l10n/fi.js146
-rw-r--r--apps/dav/l10n/fi.json144
-rw-r--r--apps/dav/l10n/fr.js328
-rw-r--r--apps/dav/l10n/fr.json328
-rw-r--r--apps/dav/l10n/ga.js338
-rw-r--r--apps/dav/l10n/ga.json336
-rw-r--r--apps/dav/l10n/gl.js301
-rw-r--r--apps/dav/l10n/gl.json301
-rw-r--r--apps/dav/l10n/he.js117
-rw-r--r--apps/dav/l10n/he.json115
-rw-r--r--apps/dav/l10n/hr.js157
-rw-r--r--apps/dav/l10n/hr.json155
-rw-r--r--apps/dav/l10n/hu.js173
-rw-r--r--apps/dav/l10n/hu.json173
-rw-r--r--apps/dav/l10n/id.js11
-rw-r--r--apps/dav/l10n/id.json9
-rw-r--r--apps/dav/l10n/is.js269
-rw-r--r--apps/dav/l10n/is.json269
-rw-r--r--apps/dav/l10n/it.js165
-rw-r--r--apps/dav/l10n/it.json165
-rw-r--r--apps/dav/l10n/ja.js256
-rw-r--r--apps/dav/l10n/ja.json256
-rw-r--r--apps/dav/l10n/ka.js220
-rw-r--r--apps/dav/l10n/ka.json218
-rw-r--r--apps/dav/l10n/ka_GE.js66
-rw-r--r--apps/dav/l10n/ka_GE.json64
-rw-r--r--apps/dav/l10n/ko.js197
-rw-r--r--apps/dav/l10n/ko.json197
-rw-r--r--apps/dav/l10n/lt_LT.js127
-rw-r--r--apps/dav/l10n/lt_LT.json125
-rw-r--r--apps/dav/l10n/lv.js11
-rw-r--r--apps/dav/l10n/lv.json9
-rw-r--r--apps/dav/l10n/mk.js153
-rw-r--r--apps/dav/l10n/mk.json153
-rw-r--r--apps/dav/l10n/nb.js234
-rw-r--r--apps/dav/l10n/nb.json234
-rw-r--r--apps/dav/l10n/nl.js198
-rw-r--r--apps/dav/l10n/nl.json198
-rw-r--r--apps/dav/l10n/nn_NO.js43
-rw-r--r--apps/dav/l10n/nn_NO.json41
-rw-r--r--apps/dav/l10n/pl.js207
-rw-r--r--apps/dav/l10n/pl.json207
-rw-r--r--apps/dav/l10n/pt_BR.js312
-rw-r--r--apps/dav/l10n/pt_BR.json312
-rw-r--r--apps/dav/l10n/pt_PT.js336
-rw-r--r--apps/dav/l10n/pt_PT.json334
-rw-r--r--apps/dav/l10n/ro.js60
-rw-r--r--apps/dav/l10n/ro.json58
-rw-r--r--apps/dav/l10n/ru.js253
-rw-r--r--apps/dav/l10n/ru.json253
-rw-r--r--apps/dav/l10n/sc.js151
-rw-r--r--apps/dav/l10n/sc.json149
-rw-r--r--apps/dav/l10n/sk.js233
-rw-r--r--apps/dav/l10n/sk.json233
-rw-r--r--apps/dav/l10n/sl.js180
-rw-r--r--apps/dav/l10n/sl.json180
-rw-r--r--apps/dav/l10n/sq.js63
-rw-r--r--apps/dav/l10n/sq.json61
-rw-r--r--apps/dav/l10n/sr.js288
-rw-r--r--apps/dav/l10n/sr.json288
-rw-r--r--apps/dav/l10n/sv.js268
-rw-r--r--apps/dav/l10n/sv.json268
-rw-r--r--apps/dav/l10n/tr.js256
-rw-r--r--apps/dav/l10n/tr.json256
-rw-r--r--apps/dav/l10n/ug.js268
-rw-r--r--apps/dav/l10n/ug.json266
-rw-r--r--apps/dav/l10n/uk.js308
-rw-r--r--apps/dav/l10n/uk.json308
-rw-r--r--apps/dav/l10n/zh_CN.js267
-rw-r--r--apps/dav/l10n/zh_CN.json267
-rw-r--r--apps/dav/l10n/zh_HK.js244
-rw-r--r--apps/dav/l10n/zh_HK.json244
-rw-r--r--apps/dav/l10n/zh_TW.js386
-rw-r--r--apps/dav/l10n/zh_TW.json386
-rw-r--r--apps/dav/lib/AppInfo/Application.php337
-rw-r--r--apps/dav/lib/AppInfo/PluginManager.php49
-rw-r--r--apps/dav/lib/Avatars/AvatarHome.php41
-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.php80
-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.php50
-rw-r--r--apps/dav/lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php49
-rw-r--r--apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php75
-rw-r--r--apps/dav/lib/BackgroundJob/PruneOutdatedSyncTokensJob.php45
-rw-r--r--apps/dav/lib/BackgroundJob/RefreshWebcalJob.php80
-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.php60
-rw-r--r--apps/dav/lib/BackgroundJob/UserStatusAutomation.php243
-rw-r--r--apps/dav/lib/BulkUpload/BulkUploadPlugin.php61
-rw-r--r--apps/dav/lib/BulkUpload/MultipartRequestParser.php111
-rw-r--r--apps/dav/lib/CalDAV/Activity/Backend.php189
-rw-r--r--apps/dav/lib/CalDAV/Activity/Filter/Calendar.php40
-rw-r--r--apps/dav/lib/CalDAV/Activity/Filter/Todo.php41
-rw-r--r--apps/dav/lib/CalDAV/Activity/Provider/Base.php98
-rw-r--r--apps/dav/lib/CalDAV/Activity/Provider/Calendar.php56
-rw-r--r--apps/dav/lib/CalDAV/Activity/Provider/Event.php137
-rw-r--r--apps/dav/lib/CalDAV/Activity/Provider/Todo.php83
-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.php29
-rw-r--r--apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php194
-rw-r--r--apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php58
-rw-r--r--apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php134
-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.php62
-rw-r--r--apps/dav/lib/CalDAV/BirthdayService.php245
-rw-r--r--apps/dav/lib/CalDAV/CachedSubscription.php49
-rw-r--r--apps/dav/lib/CalDAV/CachedSubscriptionImpl.php102
-rw-r--r--apps/dav/lib/CalDAV/CachedSubscriptionObject.php23
-rw-r--r--apps/dav/lib/CalDAV/CachedSubscriptionProvider.php40
-rw-r--r--apps/dav/lib/CalDAV/CalDavBackend.php2841
-rw-r--r--apps/dav/lib/CalDAV/Calendar.php149
-rw-r--r--apps/dav/lib/CalDAV/CalendarHome.php76
-rw-r--r--apps/dav/lib/CalDAV/CalendarImpl.php264
-rw-r--r--apps/dav/lib/CalDAV/CalendarManager.php47
-rw-r--r--apps/dav/lib/CalDAV/CalendarObject.php75
-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.php100
-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.php26
-rw-r--r--apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php40
-rw-r--r--apps/dav/lib/CalDAV/IRestorable.php21
-rw-r--r--apps/dav/lib/CalDAV/Integration/ExternalCalendar.php39
-rw-r--r--apps/dav/lib/CalDAV/Integration/ICalendarProvider.php22
-rw-r--r--apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php71
-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.php25
-rw-r--r--apps/dav/lib/CalDAV/PublicCalendar.php26
-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.php169
-rw-r--r--apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php42
-rw-r--r--apps/dav/lib/CalDAV/Reminder/Backend.php99
-rw-r--r--apps/dav/lib/CalDAV/Reminder/INotificationProvider.php31
-rw-r--r--apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php73
-rw-r--r--apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php21
-rw-r--r--apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php197
-rw-r--r--apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php21
-rw-r--r--apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php80
-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.php96
-rw-r--r--apps/dav/lib/CalDAV/Reminder/ReminderService.php341
-rw-r--r--apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php110
-rw-r--r--apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php38
-rw-r--r--apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php38
-rw-r--r--apps/dav/lib/CalDAV/RetentionService.php45
-rw-r--r--apps/dav/lib/CalDAV/Schedule/IMipPlugin.php791
-rw-r--r--apps/dav/lib/CalDAV/Schedule/IMipService.php1294
-rw-r--r--apps/dav/lib/CalDAV/Schedule/Plugin.php301
-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.php87
-rw-r--r--apps/dav/lib/CalDAV/Sharing/Backend.php30
-rw-r--r--apps/dav/lib/CalDAV/Sharing/Service.php21
-rw-r--r--apps/dav/lib/CalDAV/Status/StatusService.php186
-rw-r--r--apps/dav/lib/CalDAV/TimeZoneFactory.php213
-rw-r--r--apps/dav/lib/CalDAV/TimezoneService.php93
-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.php63
-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.php52
-rw-r--r--apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php329
-rw-r--r--apps/dav/lib/Capabilities.php46
-rw-r--r--apps/dav/lib/CardDAV/Activity/Backend.php100
-rw-r--r--apps/dav/lib/CardDAV/Activity/Filter.php39
-rw-r--r--apps/dav/lib/CardDAV/Activity/Provider/Addressbook.php58
-rw-r--r--apps/dav/lib/CardDAV/Activity/Provider/Base.php89
-rw-r--r--apps/dav/lib/CardDAV/Activity/Provider/Card.php64
-rw-r--r--apps/dav/lib/CardDAV/Activity/Setting.php25
-rw-r--r--apps/dav/lib/CardDAV/AddressBook.php133
-rw-r--r--apps/dav/lib/CardDAV/AddressBookImpl.php131
-rw-r--r--apps/dav/lib/CardDAV/AddressBookRoot.php44
-rw-r--r--apps/dav/lib/CardDAV/Card.php42
-rw-r--r--apps/dav/lib/CardDAV/CardDavBackend.php1190
-rw-r--r--apps/dav/lib/CardDAV/ContactsManager.php61
-rw-r--r--apps/dav/lib/CardDAV/Converter.php183
-rw-r--r--apps/dav/lib/CardDAV/HasPhotoPlugin.php27
-rw-r--r--apps/dav/lib/CardDAV/ImageExportPlugin.php40
-rw-r--r--apps/dav/lib/CardDAV/Integration/ExternalAddressBook.php43
-rw-r--r--apps/dav/lib/CardDAV/Integration/IAddressBookProvider.php21
-rw-r--r--apps/dav/lib/CardDAV/MultiGetExportPlugin.php29
-rw-r--r--apps/dav/lib/CardDAV/PhotoCache.php124
-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.php29
-rw-r--r--apps/dav/lib/CardDAV/Sharing/Service.php21
-rw-r--r--apps/dav/lib/CardDAV/SyncService.php407
-rw-r--r--apps/dav/lib/CardDAV/SystemAddressbook.php338
-rw-r--r--apps/dav/lib/CardDAV/UserAddressBooks.php103
-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.php63
-rw-r--r--apps/dav/lib/Command/CreateCalendar.php89
-rw-r--r--apps/dav/lib/Command/CreateSubscription.php78
-rw-r--r--apps/dav/lib/Command/DeleteCalendar.php64
-rw-r--r--apps/dav/lib/Command/DeleteSubscription.php79
-rw-r--r--apps/dav/lib/Command/ExportCalendar.php95
-rw-r--r--apps/dav/lib/Command/FixCalendarSyncCommand.php73
-rw-r--r--apps/dav/lib/Command/GetAbsenceCommand.php62
-rw-r--r--apps/dav/lib/Command/ListAddressbooks.php76
-rw-r--r--apps/dav/lib/Command/ListCalendarShares.php131
-rw-r--r--apps/dav/lib/Command/ListCalendars.php49
-rw-r--r--apps/dav/lib/Command/ListSubscriptions.php77
-rw-r--r--apps/dav/lib/Command/MoveCalendar.php118
-rw-r--r--apps/dav/lib/Command/RemoveInvalidShares.php49
-rw-r--r--apps/dav/lib/Command/RetentionCleanupCommand.php32
-rw-r--r--apps/dav/lib/Command/SendEventReminders.php52
-rw-r--r--apps/dav/lib/Command/SetAbsenceCommand.php95
-rw-r--r--apps/dav/lib/Command/SyncBirthdayCalendar.php66
-rw-r--r--apps/dav/lib/Command/SyncSystemAddressBook.php49
-rw-r--r--apps/dav/lib/Comments/CommentNode.php75
-rw-r--r--apps/dav/lib/Comments/CommentsPlugin.php61
-rw-r--r--apps/dav/lib/Comments/EntityCollection.php49
-rw-r--r--apps/dav/lib/Comments/EntityTypeCollection.php57
-rw-r--r--apps/dav/lib/Comments/RootCollection.php81
-rw-r--r--apps/dav/lib/Connector/LegacyDAVACL.php26
-rw-r--r--apps/dav/lib/Connector/LegacyPublicAuth.php102
-rw-r--r--apps/dav/lib/Connector/PublicAuth.php147
-rw-r--r--apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php28
-rw-r--r--apps/dav/lib/Connector/Sabre/AppEnabledPlugin.php88
-rw-r--r--apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php112
-rw-r--r--apps/dav/lib/Connector/Sabre/Auth.php157
-rw-r--r--apps/dav/lib/Connector/Sabre/BearerAuth.php73
-rw-r--r--apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php72
-rw-r--r--apps/dav/lib/Connector/Sabre/CachingTree.php36
-rw-r--r--apps/dav/lib/Connector/Sabre/ChecksumList.php34
-rw-r--r--apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php44
-rw-r--r--apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php85
-rw-r--r--apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php29
-rw-r--r--apps/dav/lib/Connector/Sabre/DavAclPlugin.php78
-rw-r--r--apps/dav/lib/Connector/Sabre/Directory.php240
-rw-r--r--apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php40
-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.php36
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/Forbidden.php41
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php42
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/PasswordLoginForbidden.php24
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/TooManyRequests.php38
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/UnsupportedMediaType.php24
-rw-r--r--apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php71
-rw-r--r--apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php50
-rw-r--r--apps/dav/lib/Connector/Sabre/File.php485
-rw-r--r--apps/dav/lib/Connector/Sabre/FilesPlugin.php540
-rw-r--r--apps/dav/lib/Connector/Sabre/FilesReportPlugin.php259
-rw-r--r--apps/dav/lib/Connector/Sabre/LockPlugin.php31
-rw-r--r--apps/dav/lib/Connector/Sabre/MaintenancePlugin.php44
-rw-r--r--apps/dav/lib/Connector/Sabre/MtimeSanitizer.php21
-rw-r--r--apps/dav/lib/Connector/Sabre/Node.php229
-rw-r--r--apps/dav/lib/Connector/Sabre/ObjectTree.php85
-rw-r--r--apps/dav/lib/Connector/Sabre/Principal.php180
-rw-r--r--apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php78
-rw-r--r--apps/dav/lib/Connector/Sabre/PropfindCompressionPlugin.php23
-rw-r--r--apps/dav/lib/Connector/Sabre/PublicAuth.php227
-rw-r--r--apps/dav/lib/Connector/Sabre/QuotaPlugin.php189
-rw-r--r--apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php31
-rw-r--r--apps/dav/lib/Connector/Sabre/Server.php228
-rw-r--r--apps/dav/lib/Connector/Sabre/ServerFactory.php272
-rw-r--r--apps/dav/lib/Connector/Sabre/ShareTypeList.php40
-rw-r--r--apps/dav/lib/Connector/Sabre/ShareeList.php34
-rw-r--r--apps/dav/lib/Connector/Sabre/SharesPlugin.php147
-rw-r--r--apps/dav/lib/Connector/Sabre/TagList.php41
-rw-r--r--apps/dav/lib/Connector/Sabre/TagsPlugin.php118
-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.php98
-rw-r--r--apps/dav/lib/Controller/ExampleContentController.php98
-rw-r--r--apps/dav/lib/Controller/InvitationResponseController.php93
-rw-r--r--apps/dav/lib/Controller/OutOfOfficeController.php189
-rw-r--r--apps/dav/lib/Controller/UpcomingEventsController.php57
-rw-r--r--apps/dav/lib/DAV/CustomPropertiesBackend.php487
-rw-r--r--apps/dav/lib/DAV/GroupPrincipalBackend.php70
-rw-r--r--apps/dav/lib/DAV/PublicAuth.php29
-rw-r--r--apps/dav/lib/DAV/Sharing/Backend.php323
-rw-r--r--apps/dav/lib/DAV/Sharing/IShareable.php44
-rw-r--r--apps/dav/lib/DAV/Sharing/Plugin.php70
-rw-r--r--apps/dav/lib/DAV/Sharing/SharingMapper.php137
-rw-r--r--apps/dav/lib/DAV/Sharing/SharingService.php53
-rw-r--r--apps/dav/lib/DAV/Sharing/Xml/Invite.php59
-rw-r--r--apps/dav/lib/DAV/Sharing/Xml/ShareRequest.php40
-rw-r--r--apps/dav/lib/DAV/SystemPrincipalBackend.php27
-rw-r--r--apps/dav/lib/DAV/ViewOnlyPlugin.php114
-rw-r--r--apps/dav/lib/Db/Absence.php99
-rw-r--r--apps/dav/lib/Db/AbsenceMapper.php89
-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.php37
-rw-r--r--apps/dav/lib/Db/PropertyMapper.php55
-rw-r--r--apps/dav/lib/Direct/DirectFile.php52
-rw-r--r--apps/dav/lib/Direct/DirectHome.php62
-rw-r--r--apps/dav/lib/Direct/Server.php21
-rw-r--r--apps/dav/lib/Direct/ServerFactory.php48
-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/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.php42
-rw-r--r--apps/dav/lib/Events/CalendarRestoredEvent.php41
-rw-r--r--apps/dav/lib/Events/CalendarShareUpdatedEvent.php72
-rw-r--r--apps/dav/lib/Events/CalendarUnpublishedEvent.php36
-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.php90
-rw-r--r--apps/dav/lib/Events/CardUpdatedEvent.php47
-rw-r--r--apps/dav/lib/Events/SabrePluginAddEvent.php37
-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.php14
-rw-r--r--apps/dav/lib/Exception/UnsupportedLimitOnInitialSyncException.php22
-rw-r--r--apps/dav/lib/Files/BrowserErrorPagePlugin.php42
-rw-r--r--apps/dav/lib/Files/FileSearchBackend.php296
-rw-r--r--apps/dav/lib/Files/FilesHome.php40
-rw-r--r--apps/dav/lib/Files/LazySearchBackend.php23
-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.php68
-rw-r--r--apps/dav/lib/Listener/AddMissingIndicesListener.php40
-rw-r--r--apps/dav/lib/Listener/AddressbookListener.php36
-rw-r--r--apps/dav/lib/Listener/BirthdayListener.php37
-rw-r--r--apps/dav/lib/Listener/CalendarContactInteractionListener.php63
-rw-r--r--apps/dav/lib/Listener/CalendarDeletionDefaultUpdaterListener.php40
-rw-r--r--apps/dav/lib/Listener/CalendarObjectReminderUpdaterListener.php58
-rw-r--r--apps/dav/lib/Listener/CalendarPublicationListener.php46
-rw-r--r--apps/dav/lib/Listener/CalendarShareUpdateListener.php45
-rw-r--r--apps/dav/lib/Listener/CardListener.php38
-rw-r--r--apps/dav/lib/Listener/ClearPhotoCacheListener.php31
-rw-r--r--apps/dav/lib/Listener/DavAdminSettingsListener.php65
-rw-r--r--apps/dav/lib/Listener/OutOfOfficeListener.php179
-rw-r--r--apps/dav/lib/Listener/SubscriptionListener.php64
-rw-r--r--apps/dav/lib/Listener/TrustedServerRemovedListener.php33
-rw-r--r--apps/dav/lib/Listener/UserEventsListener.php186
-rw-r--r--apps/dav/lib/Listener/UserPreferenceListener.php42
-rw-r--r--apps/dav/lib/Migration/BuildCalendarSearchIndex.php55
-rw-r--r--apps/dav/lib/Migration/BuildCalendarSearchIndexBackgroundJob.php82
-rw-r--r--apps/dav/lib/Migration/BuildSocialSearchIndex.php43
-rw-r--r--apps/dav/lib/Migration/BuildSocialSearchIndexBackgroundJob.php85
-rw-r--r--apps/dav/lib/Migration/CalDAVRemoveEmptyValue.php58
-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.php48
-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.php31
-rw-r--r--apps/dav/lib/Migration/Version1004Date20170926103422.php25
-rw-r--r--apps/dav/lib/Migration/Version1005Date20180413093149.php27
-rw-r--r--apps/dav/lib/Migration/Version1005Date20180530124431.php28
-rw-r--r--apps/dav/lib/Migration/Version1006Date20180619154313.php28
-rw-r--r--apps/dav/lib/Migration/Version1006Date20180628111625.php28
-rw-r--r--apps/dav/lib/Migration/Version1008Date20181030113700.php26
-rw-r--r--apps/dav/lib/Migration/Version1008Date20181105104826.php34
-rw-r--r--apps/dav/lib/Migration/Version1008Date20181105104833.php22
-rw-r--r--apps/dav/lib/Migration/Version1008Date20181105110300.php34
-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.php26
-rw-r--r--apps/dav/lib/Migration/Version1011Date20190806104428.php26
-rw-r--r--apps/dav/lib/Migration/Version1012Date20190808122342.php31
-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.php61
-rw-r--r--apps/dav/lib/Migration/Version1025Date20240308063933.php91
-rw-r--r--apps/dav/lib/Migration/Version1027Date20230504122946.php53
-rw-r--r--apps/dav/lib/Migration/Version1029Date20221114151721.php38
-rw-r--r--apps/dav/lib/Migration/Version1029Date20231004091403.php66
-rw-r--r--apps/dav/lib/Migration/Version1030Date20240205103243.php37
-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.php32
-rw-r--r--apps/dav/lib/Provisioning/Apple/AppleProvisioningNode.php32
-rw-r--r--apps/dav/lib/Provisioning/Apple/AppleProvisioningPlugin.php88
-rw-r--r--apps/dav/lib/ResponseDefinitions.php47
-rw-r--r--apps/dav/lib/RootCollection.php157
-rw-r--r--apps/dav/lib/Search/ACalendarSearchProvider.php51
-rw-r--r--apps/dav/lib/Search/ContactsSearchProvider.php166
-rw-r--r--apps/dav/lib/Search/EventsSearchProvider.php187
-rw-r--r--apps/dav/lib/Search/TasksSearchProvider.php61
-rw-r--r--apps/dav/lib/Server.php318
-rw-r--r--apps/dav/lib/ServerFactory.php24
-rw-r--r--apps/dav/lib/Service/AbsenceService.php155
-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.php65
-rw-r--r--apps/dav/lib/Settings/CalDAVSettings.php59
-rw-r--r--apps/dav/lib/Settings/ExampleContentSettings.php71
-rw-r--r--apps/dav/lib/SetupChecks/NeedsSystemAddressBookSync.php39
-rw-r--r--apps/dav/lib/SetupChecks/WebdavEndpoint.php75
-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.php64
-rw-r--r--apps/dav/lib/SystemTag/SystemTagMappingNode.php94
-rw-r--r--apps/dav/lib/SystemTag/SystemTagNode.php137
-rw-r--r--apps/dav/lib/SystemTag/SystemTagObjectType.php82
-rw-r--r--apps/dav/lib/SystemTag/SystemTagPlugin.php322
-rw-r--r--apps/dav/lib/SystemTag/SystemTagsByIdCollection.php78
-rw-r--r--apps/dav/lib/SystemTag/SystemTagsInUseCollection.php85
-rw-r--r--apps/dav/lib/SystemTag/SystemTagsObjectList.php85
-rw-r--r--apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php110
-rw-r--r--apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php107
-rw-r--r--apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php79
-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.php385
-rw-r--r--apps/dav/lib/Upload/CleanupService.php41
-rw-r--r--apps/dav/lib/Upload/FutureFile.php41
-rw-r--r--apps/dav/lib/Upload/PartFile.php91
-rw-r--r--apps/dav/lib/Upload/RootCollection.php49
-rw-r--r--apps/dav/lib/Upload/UploadAutoMkcolPlugin.php68
-rw-r--r--apps/dav/lib/Upload/UploadFile.php50
-rw-r--r--apps/dav/lib/Upload/UploadFolder.php78
-rw-r--r--apps/dav/lib/Upload/UploadHome.php108
-rw-r--r--apps/dav/lib/UserMigration/CalendarMigrator.php105
-rw-r--r--apps/dav/lib/UserMigration/CalendarMigratorException.php21
-rw-r--r--apps/dav/lib/UserMigration/ContactsMigrator.php126
-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
-rw-r--r--apps/dav/openapi.json999
-rw-r--r--apps/dav/openapi.json.license2
-rw-r--r--apps/dav/src/components/AbsenceForm.vue274
-rw-r--r--apps/dav/src/components/AvailabilityForm.vue223
-rw-r--r--apps/dav/src/components/ExampleContactSettings.vue172
-rw-r--r--apps/dav/src/components/ExampleContentDownloadButton.vue57
-rw-r--r--apps/dav/src/components/ExampleEventSettings.vue217
-rw-r--r--apps/dav/src/dav/client.js54
-rw-r--r--apps/dav/src/service/CalendarService.js28
-rw-r--r--apps/dav/src/service/ExampleEventService.js43
-rw-r--r--apps/dav/src/service/PreferenceService.js34
-rw-r--r--apps/dav/src/service/logger.js22
-rw-r--r--apps/dav/src/settings-example-content.js18
-rw-r--r--apps/dav/src/settings-personal-availability.js6
-rw-r--r--apps/dav/src/settings.js12
-rw-r--r--apps/dav/src/utils/date.js17
-rw-r--r--apps/dav/src/views/Availability.vue196
-rw-r--r--apps/dav/src/views/CalDavSettings.spec.js61
-rw-r--r--apps/dav/src/views/CalDavSettings.vue104
-rw-r--r--apps/dav/src/views/ExampleContentSettingsSection.vue38
-rw-r--r--apps/dav/src/views/__snapshots__/CalDavSettings.spec.js.snap480
-rw-r--r--apps/dav/src/views/__snapshots__/CalDavSettings.spec.js.snap.license2
-rw-r--r--apps/dav/templates/schedule-response-error.php21
-rw-r--r--apps/dav/templates/schedule-response-options.php12
-rw-r--r--apps/dav/templates/schedule-response-success.php11
-rw-r--r--apps/dav/templates/settings-admin-caldav.php26
-rw-r--r--apps/dav/templates/settings-example-content.php11
-rw-r--r--apps/dav/templates/settings-personal-availability.php26
-rwxr-xr-xapps/dav/tests/benchmarks/benchmark.sh5
-rwxr-xr-xapps/dav/tests/benchmarks/bulk_upload.sh5
-rwxr-xr-xapps/dav/tests/benchmarks/single_upload.sh5
-rw-r--r--apps/dav/tests/integration/DAV/Sharing/CalDavSharingBackendTest.php251
-rw-r--r--apps/dav/tests/integration/DAV/Sharing/SharingMapperTest.php95
-rw-r--r--apps/dav/tests/integration/Db/PropertyMapperTest.php40
-rw-r--r--apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php31
-rw-r--r--apps/dav/tests/integration/UserMigration/ContactsMigratorTest.php59
-rw-r--r--apps/dav/tests/integration/UserMigration/assets/address_books/contact-complex.vcf15883
-rw-r--r--apps/dav/tests/misc/sharing.xml7
-rw-r--r--apps/dav/tests/testsuits/caldav/install.sh (renamed from apps/dav/tests/travis/caldav/install.sh)5
-rw-r--r--apps/dav/tests/testsuits/caldav/script-new-endpoint.sh (renamed from apps/dav/tests/travis/caldav/script-new-endpoint.sh)5
-rw-r--r--apps/dav/tests/testsuits/caldav/script-old-endpoint.sh (renamed from apps/dav/tests/travis/caldav/script-old-endpoint.sh)5
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/current-user-principal/1.xml (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/current-user-principal/1.xml)4
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/1.txt (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/put/1.txt)0
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/1.xml (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/put/1.xml)4
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/2.txt (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/put/2.txt)0
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/3.txt (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/put/3.txt)0
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/4.txt (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/put/4.txt)0
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/5.txt (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/put/5.txt)0
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/6.txt (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/put/6.txt)0
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/1.xml (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/1.xml)4
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/10.xml (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/10.xml)4
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/11.xml (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/11.xml)4
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/2.xml (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/vreports/sync/2.xml)4
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/21.xml (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/21.xml)4
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/3.xml (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/3.xml)4
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/4.xml (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/4.xml)4
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/5.xml (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/5.xml)4
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/6.xml (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/6.xml)4
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/7.xml (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/7.xml)4
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/8.xml (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/8.xml)4
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/1.xml (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/1.xml)4
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/4.xml (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/4.xml)4
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/5.ics (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/5.ics)0
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/5.xml (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/5.xml)4
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/6.ics (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/6.ics)0
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/7.ics (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/7.ics)0
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/8.ics (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/8.ics)0
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/9.ics (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/9.ics)0
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/1.xml (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/sharing/read-write/1.xml)4
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/4.xml (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/sharing/read-write/4.xml)4
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/6.vcf (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/sharing/read-write/6.vcf)0
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/7.vcf (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/sharing/read-write/7.vcf)0
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/8.vcf (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/sharing/read-write/8.vcf)0
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/9.vcf (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/sharing/read-write/9.vcf)0
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vcurrent-user-principal/1.xml (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/vcurrent-user-principal/1.xml)4
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vreports/put/1.vcf (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/vreports/put/1.vcf)0
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vreports/put/2.vcf (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/vreports/put/2.vcf)0
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vreports/put/3.vcf (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/vreports/put/3.vcf)0
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vreports/sync/1.xml (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/vreports/sync/1.xml)4
-rw-r--r--apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vreports/sync/2.xml (renamed from apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/2.xml)4
-rw-r--r--apps/dav/tests/testsuits/caldavtest/serverinfo-new-endpoint.xml (renamed from apps/dav/tests/travis/caldavtest/serverinfo-new-endpoint.xml)22
-rw-r--r--apps/dav/tests/testsuits/caldavtest/serverinfo-old-caldav-endpoint.xml (renamed from apps/dav/tests/travis/caldavtest/serverinfo-old-caldav-endpoint.xml)22
-rw-r--r--apps/dav/tests/testsuits/caldavtest/serverinfo-old-carddav-endpoint.xml (renamed from apps/dav/tests/travis/caldavtest/serverinfo-old-carddav-endpoint.xml)22
-rw-r--r--apps/dav/tests/testsuits/caldavtest/serverinfo.dtd (renamed from apps/dav/tests/travis/caldavtest/serverinfo.dtd)17
-rw-r--r--apps/dav/tests/testsuits/caldavtest/tests/CalDAV/current-user-principal.xml (renamed from apps/dav/tests/travis/caldavtest/tests/CalDAV/current-user-principal.xml)17
-rw-r--r--apps/dav/tests/testsuits/caldavtest/tests/CalDAV/sharing-calendars.xml (renamed from apps/dav/tests/travis/caldavtest/tests/CalDAV/sharing-calendars.xml)17
-rw-r--r--apps/dav/tests/testsuits/caldavtest/tests/CalDAV/sync-report.xml (renamed from apps/dav/tests/travis/caldavtest/tests/CalDAV/sync-report.xml)17
-rw-r--r--apps/dav/tests/testsuits/caldavtest/tests/CardDAV/current-user-principal.xml (renamed from apps/dav/tests/travis/caldavtest/tests/CardDAV/current-user-principal.xml)17
-rw-r--r--apps/dav/tests/testsuits/caldavtest/tests/CardDAV/sharing-addressbooks.xml (renamed from apps/dav/tests/travis/caldavtest/tests/CardDAV/sharing-addressbooks.xml)5
-rw-r--r--apps/dav/tests/testsuits/caldavtest/tests/CardDAV/sync-report.xml (renamed from apps/dav/tests/travis/caldavtest/tests/CardDAV/sync-report.xml)17
-rw-r--r--apps/dav/tests/testsuits/carddav/install.sh (renamed from apps/dav/tests/travis/carddav/install.sh)4
-rw-r--r--apps/dav/tests/testsuits/carddav/script-new-endpoint.sh (renamed from apps/dav/tests/travis/carddav/script-new-endpoint.sh)4
-rw-r--r--apps/dav/tests/testsuits/carddav/script-old-endpoint.sh (renamed from apps/dav/tests/travis/carddav/script-old-endpoint.sh)4
-rw-r--r--apps/dav/tests/testsuits/carddav/script.sh (renamed from apps/dav/tests/travis/carddav/script.sh)4
-rw-r--r--apps/dav/tests/testsuits/litmus-v1/script.sh (renamed from apps/dav/tests/travis/litmus-v1/script.sh)7
-rw-r--r--apps/dav/tests/testsuits/litmus-v2/script.sh (renamed from apps/dav/tests/travis/litmus-v2/script.sh)7
-rw-r--r--apps/dav/tests/unit/AppInfo/ApplicationTest.php26
-rw-r--r--apps/dav/tests/unit/AppInfo/PluginManagerTest.php56
-rw-r--r--apps/dav/tests/unit/Avatars/AvatarHomeTest.php65
-rw-r--r--apps/dav/tests/unit/Avatars/AvatarNodeTest.php35
-rw-r--r--apps/dav/tests/unit/BackgroundJob/CleanupInvitationTokenJobTest.php54
-rw-r--r--apps/dav/tests/unit/BackgroundJob/CleanupOrphanedChildrenJobTest.php170
-rw-r--r--apps/dav/tests/unit/BackgroundJob/EventReminderJobTest.php59
-rw-r--r--apps/dav/tests/unit/BackgroundJob/GenerateBirthdayCalendarBackgroundJobTest.php48
-rw-r--r--apps/dav/tests/unit/BackgroundJob/OutOfOfficeEventDispatcherJobTest.php148
-rw-r--r--apps/dav/tests/unit/BackgroundJob/PruneOutdatedSyncTokensJobTest.php82
-rw-r--r--apps/dav/tests/unit/BackgroundJob/RefreshWebcalJobTest.php62
-rw-r--r--apps/dav/tests/unit/BackgroundJob/RegisterRegenerateBirthdayCalendarsTest.php82
-rw-r--r--apps/dav/tests/unit/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJobTest.php432
-rw-r--r--apps/dav/tests/unit/BackgroundJob/UserStatusAutomationTest.php220
-rw-r--r--apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php130
-rw-r--r--apps/dav/tests/unit/CalDAV/Activity/BackendTest.php124
-rw-r--r--apps/dav/tests/unit/CalDAV/Activity/Filter/CalendarTest.php44
-rw-r--r--apps/dav/tests/unit/CalDAV/Activity/Filter/GenericTest.php90
-rw-r--r--apps/dav/tests/unit/CalDAV/Activity/Filter/TodoTest.php43
-rw-r--r--apps/dav/tests/unit/CalDAV/Activity/Provider/BaseTest.php134
-rw-r--r--apps/dav/tests/unit/CalDAV/Activity/Provider/EventTest.php165
-rw-r--r--apps/dav/tests/unit/CalDAV/Activity/Setting/GenericTest.php99
-rw-r--r--apps/dav/tests/unit/CalDAV/AppCalendar/AppCalendarTest.php120
-rw-r--r--apps/dav/tests/unit/CalDAV/AppCalendar/CalendarObjectTest.php170
-rw-r--r--apps/dav/tests/unit/CalDAV/BirthdayCalendar/EnablePluginTest.php120
-rw-r--r--apps/dav/tests/unit/CalDAV/CachedSubscriptionImplTest.php80
-rw-r--r--apps/dav/tests/unit/CalDAV/CachedSubscriptionObjectTest.php35
-rw-r--r--apps/dav/tests/unit/CalDAV/CachedSubscriptionProviderTest.php72
-rw-r--r--apps/dav/tests/unit/CalDAV/CachedSubscriptionTest.php99
-rw-r--r--apps/dav/tests/unit/CalDAV/CalDavBackendTest.php794
-rw-r--r--apps/dav/tests/unit/CalDAV/CalendarHomeTest.php233
-rw-r--r--apps/dav/tests/unit/CalDAV/CalendarImplTest.php284
-rw-r--r--apps/dav/tests/unit/CalDAV/CalendarManagerTest.php68
-rw-r--r--apps/dav/tests/unit/CalDAV/CalendarTest.php143
-rw-r--r--apps/dav/tests/unit/CalDAV/DefaultCalendarValidatorTest.php171
-rw-r--r--apps/dav/tests/unit/CalDAV/EventComparisonServiceTest.php188
-rw-r--r--apps/dav/tests/unit/CalDAV/EventReaderTest.php1087
-rw-r--r--apps/dav/tests/unit/CalDAV/Export/ExportServiceTest.php81
-rw-r--r--apps/dav/tests/unit/CalDAV/Integration/ExternalCalendarTest.php44
-rw-r--r--apps/dav/tests/unit/CalDAV/Listener/CalendarPublicationListenerTest.php55
-rw-r--r--apps/dav/tests/unit/CalDAV/Listener/CalendarShareUpdateListenerTest.php48
-rw-r--r--apps/dav/tests/unit/CalDAV/Listener/SubscriptionListenerTest.php67
-rw-r--r--apps/dav/tests/unit/CalDAV/OutboxTest.php38
-rw-r--r--apps/dav/tests/unit/CalDAV/PluginTest.php42
-rw-r--r--apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php99
-rw-r--r--apps/dav/tests/unit/CalDAV/PublicCalendarTest.php58
-rw-r--r--apps/dav/tests/unit/CalDAV/Publishing/PublisherTest.php36
-rw-r--r--apps/dav/tests/unit/CalDAV/Publishing/PublishingTest.php63
-rw-r--r--apps/dav/tests/unit/CalDAV/Reminder/BackendTest.php91
-rw-r--r--apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AbstractNotificationProviderTest.php94
-rw-r--r--apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AbstractNotificationProviderTestCase.php52
-rw-r--r--apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AudioProviderTest.php22
-rw-r--r--apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/EmailProviderTest.php443
-rw-r--r--apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/PushProviderTest.php102
-rw-r--r--apps/dav/tests/unit/CalDAV/Reminder/NotificationProviderManagerTest.php41
-rw-r--r--apps/dav/tests/unit/CalDAV/Reminder/NotifierTest.php86
-rw-r--r--apps/dav/tests/unit/CalDAV/Reminder/ReminderServiceTest.php546
-rw-r--r--apps/dav/tests/unit/CalDAV/ResourceBooking/AbstractPrincipalBackendTestCase.php (renamed from apps/dav/tests/unit/CalDAV/ResourceBooking/AbstractPrincipalBackendTest.php)158
-rw-r--r--apps/dav/tests/unit/CalDAV/ResourceBooking/ResourcePrincipalBackendTest.php27
-rw-r--r--apps/dav/tests/unit/CalDAV/ResourceBooking/RoomPrincipalBackendTest.php27
-rw-r--r--apps/dav/tests/unit/CalDAV/Schedule/IMipPluginCharsetTest.php193
-rw-r--r--apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php1229
-rw-r--r--apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php2200
-rw-r--r--apps/dav/tests/unit/CalDAV/Schedule/PluginTest.php592
-rw-r--r--apps/dav/tests/unit/CalDAV/Search/Request/CalendarSearchReportTest.php57
-rw-r--r--apps/dav/tests/unit/CalDAV/Search/SearchPluginTest.php51
-rw-r--r--apps/dav/tests/unit/CalDAV/Security/RateLimitingPluginTest.php188
-rw-r--r--apps/dav/tests/unit/CalDAV/Status/StatusServiceTest.php445
-rw-r--r--apps/dav/tests/unit/CalDAV/TimeZoneFactoryTest.php51
-rw-r--r--apps/dav/tests/unit/CalDAV/TimezoneServiceTest.php142
-rw-r--r--apps/dav/tests/unit/CalDAV/TipBrokerTest.php180
-rw-r--r--apps/dav/tests/unit/CalDAV/Validation/CalDavValidatePluginTest.php73
-rw-r--r--apps/dav/tests/unit/CalDAV/WebcalCaching/ConnectionTest.php176
-rw-r--r--apps/dav/tests/unit/CalDAV/WebcalCaching/PluginTest.php143
-rw-r--r--apps/dav/tests/unit/CalDAV/WebcalCaching/RefreshWebcalServiceTest.php536
-rw-r--r--apps/dav/tests/unit/CapabilitiesTest.php85
-rw-r--r--apps/dav/tests/unit/CardDAV/Activity/BackendTest.php483
-rw-r--r--apps/dav/tests/unit/CardDAV/AddressBookImplTest.php190
-rw-r--r--apps/dav/tests/unit/CardDAV/AddressBookTest.php149
-rw-r--r--apps/dav/tests/unit/CardDAV/BirthdayServiceTest.php253
-rw-r--r--apps/dav/tests/unit/CardDAV/CardDavBackendTest.php486
-rw-r--r--apps/dav/tests/unit/CardDAV/ContactsManagerTest.php44
-rw-r--r--apps/dav/tests/unit/CardDAV/ConverterTest.php134
-rw-r--r--apps/dav/tests/unit/CardDAV/ImageExportPluginTest.php113
-rw-r--r--apps/dav/tests/unit/CardDAV/Security/CardDavRateLimitingPluginTest.php146
-rw-r--r--apps/dav/tests/unit/CardDAV/Sharing/PluginTest.php57
-rw-r--r--apps/dav/tests/unit/CardDAV/SyncServiceTest.php518
-rw-r--r--apps/dav/tests/unit/CardDAV/SystemAddressBookTest.php428
-rw-r--r--apps/dav/tests/unit/CardDAV/Validation/CardDavValidatePluginTest.php73
-rw-r--r--apps/dav/tests/unit/Command/DeleteCalendarTest.php71
-rw-r--r--apps/dav/tests/unit/Command/ListAddressbooksTest.php107
-rw-r--r--apps/dav/tests/unit/Command/ListCalendarSharesTest.php172
-rw-r--r--apps/dav/tests/unit/Command/ListCalendarsTest.php58
-rw-r--r--apps/dav/tests/unit/Command/MoveCalendarTest.php332
-rw-r--r--apps/dav/tests/unit/Command/RemoveInvalidSharesTest.php45
-rw-r--r--apps/dav/tests/unit/Comments/CommentsNodeTest.php174
-rw-r--r--apps/dav/tests/unit/Comments/CommentsPluginTest.php99
-rw-r--r--apps/dav/tests/unit/Comments/EntityCollectionTest.php96
-rw-r--r--apps/dav/tests/unit/Comments/EntityTypeCollectionTest.php84
-rw-r--r--apps/dav/tests/unit/Comments/RootCollectionTest.php126
-rw-r--r--apps/dav/tests/unit/Connector/LegacyPublicAuthTest.php (renamed from apps/dav/tests/unit/Connector/PublicAuthTest.php)136
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/AuthTest.php403
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php69
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/BlockLegacyClientPluginTest.php187
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/CommentsPropertiesPluginTest.php118
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/CopyEtagHeaderPluginTest.php59
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php113
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php224
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/DummyGetResponsePluginTest.php47
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/Exception/ForbiddenTest.php37
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/Exception/InvalidPathTest.php38
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/ExceptionLoggerPluginTest.php91
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/FakeLockerPluginTest.php130
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/FileTest.php654
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php554
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/FilesReportPluginTest.php747
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/MaintenancePluginTest.php44
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/NodeTest.php236
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php162
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/PrincipalTest.php383
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php123
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/PropfindCompressionPluginTest.php32
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/PublicAuthTest.php384
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/QuotaPluginTest.php156
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/RequestTest/Auth.php51
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/RequestTest/DeleteTest.php31
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/RequestTest/DownloadTest.php38
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionMasterKeyUploadTest.php32
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionUploadTest.php35
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/RequestTest/ExceptionPlugin.php31
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/RequestTest/PartFileInRootUploadTest.php36
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/RequestTest/RequestTestCase.php96
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/RequestTest/Sapi.php37
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/RequestTest/UploadTest.php153
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php117
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/TagsPluginTest.php213
-rw-r--r--apps/dav/tests/unit/Controller/BirthdayCalendarControllerTest.php84
-rw-r--r--apps/dav/tests/unit/Controller/DirectControllerTest.php74
-rw-r--r--apps/dav/tests/unit/Controller/InvitationResponseControllerTest.php123
-rw-r--r--apps/dav/tests/unit/Controller/UpcomingEventsControllerTest.php73
-rw-r--r--apps/dav/tests/unit/DAV/AnonymousOptionsTest.php48
-rw-r--r--apps/dav/tests/unit/DAV/BrowserErrorPagePluginTest.php46
-rw-r--r--apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php343
-rw-r--r--apps/dav/tests/unit/DAV/GroupPrincipalTest.php110
-rw-r--r--apps/dav/tests/unit/DAV/HookManagerTest.php250
-rw-r--r--apps/dav/tests/unit/DAV/Listener/UserEventsListenerTest.php183
-rw-r--r--apps/dav/tests/unit/DAV/Sharing/BackendTest.php399
-rw-r--r--apps/dav/tests/unit/DAV/Sharing/PluginTest.php51
-rw-r--r--apps/dav/tests/unit/DAV/SystemPrincipalBackendTest.php84
-rw-r--r--apps/dav/tests/unit/DAV/ViewOnlyPluginTest.php167
-rw-r--r--apps/dav/tests/unit/Direct/DirectFileTest.php72
-rw-r--r--apps/dav/tests/unit/Direct/DirectHomeTest.php82
-rw-r--r--apps/dav/tests/unit/Files/FileSearchBackendTest.php243
-rw-r--r--apps/dav/tests/unit/Files/MultipartRequestParserTest.php205
-rw-r--r--apps/dav/tests/unit/Files/Sharing/FilesDropPluginTest.php256
-rw-r--r--apps/dav/tests/unit/Listener/ActivityUpdaterListenerTest.php80
-rw-r--r--apps/dav/tests/unit/Listener/CalendarContactInteractionListenerTest.php49
-rw-r--r--apps/dav/tests/unit/Listener/OutOfOfficeListenerTest.php606
-rw-r--r--apps/dav/tests/unit/Migration/CalDAVRemoveEmptyValueTest.php86
-rw-r--r--apps/dav/tests/unit/Migration/CreateSystemAddressBookStepTest.php47
-rw-r--r--apps/dav/tests/unit/Migration/RefreshWebcalJobRegistrarTest.php143
-rw-r--r--apps/dav/tests/unit/Migration/RegenerateBirthdayCalendarsTest.php46
-rw-r--r--apps/dav/tests/unit/Migration/RemoveDeletedUsersCalendarSubscriptionsTest.php81
-rw-r--r--apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningNodeTest.php51
-rw-r--r--apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningPluginTest.php145
-rw-r--r--apps/dav/tests/unit/Search/ContactsSearchProviderTest.php94
-rw-r--r--apps/dav/tests/unit/Search/EventsSearchProviderTest.php431
-rw-r--r--apps/dav/tests/unit/Search/TasksSearchProviderTest.php201
-rw-r--r--apps/dav/tests/unit/ServerTest.php37
-rw-r--r--apps/dav/tests/unit/Service/AbsenceServiceTest.php445
-rw-r--r--apps/dav/tests/unit/Service/ExampleContactServiceTest.php194
-rw-r--r--apps/dav/tests/unit/Service/ExampleEventServiceTest.php196
-rw-r--r--apps/dav/tests/unit/Service/UpcomingEventsServiceTest.php89
-rw-r--r--apps/dav/tests/unit/Settings/CalDAVSettingsTest.php103
-rw-r--r--apps/dav/tests/unit/SystemTag/SystemTagMappingNodeTest.php116
-rw-r--r--apps/dav/tests/unit/SystemTag/SystemTagNodeTest.php167
-rw-r--r--apps/dav/tests/unit/SystemTag/SystemTagPluginTest.php263
-rw-r--r--apps/dav/tests/unit/SystemTag/SystemTagsByIdCollectionTest.php134
-rw-r--r--apps/dav/tests/unit/SystemTag/SystemTagsObjectMappingCollectionTest.php172
-rw-r--r--apps/dav/tests/unit/SystemTag/SystemTagsObjectTypeCollectionTest.php138
-rw-r--r--apps/dav/tests/unit/Upload/AssemblyStreamTest.php133
-rw-r--r--apps/dav/tests/unit/Upload/ChunkingPluginTest.php146
-rw-r--r--apps/dav/tests/unit/Upload/FutureFileTest.php63
-rw-r--r--apps/dav/tests/unit/Upload/UploadAutoMkcolPluginTest.php133
-rw-r--r--apps/dav/tests/unit/bootstrap.php38
-rw-r--r--apps/dav/tests/unit/phpunit.xml5
-rw-r--r--apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-1.ics17
-rw-r--r--apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-2.ics17
-rw-r--r--apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-3.ics17
-rw-r--r--apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-4.ics17
-rw-r--r--apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-5.ics14
-rw-r--r--apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-6.ics15
-rw-r--r--apps/dav/tests/unit/test_fixtures/caldav-search-missing-start-1.ics14
-rw-r--r--apps/dav/tests/unit/test_fixtures/caldav-search-missing-start-2.ics14
-rw-r--r--apps/dav/tests/unit/test_fixtures/example-event-default-expected.ics20
-rw-r--r--apps/dav/tests/unit/test_fixtures/example-event-default-expected.ics.license2
-rw-r--r--apps/dav/tests/unit/test_fixtures/example-event-expected.ics18
-rw-r--r--apps/dav/tests/unit/test_fixtures/example-event-expected.ics.license2
-rw-r--r--apps/dav/tests/unit/test_fixtures/example-event-with-attendees.ics21
-rw-r--r--apps/dav/tests/unit/test_fixtures/example-event-with-attendees.ics.license2
-rw-r--r--apps/dav/tests/unit/test_fixtures/example-event.ics18
-rw-r--r--apps/dav/tests/unit/test_fixtures/example-event.ics.license2
870 files changed, 69547 insertions, 52051 deletions
diff --git a/apps/dav/.gitignore b/apps/dav/.gitignore
index 885b6b3e6de..a1d833a0f0a 100644
--- a/apps/dav/.gitignore
+++ b/apps/dav/.gitignore
@@ -1 +1,3 @@
+# SPDX-FileCopyrightText: 2015 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
tests/travis/CalDAVTester
diff --git a/apps/dav/.l10nignore b/apps/dav/.l10nignore
index ea53c49c185..41c1e48d55e 100644
--- a/apps/dav/.l10nignore
+++ b/apps/dav/.l10nignore
@@ -1,2 +1,4 @@
+# SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
#webpack bundled files
js/
diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml
index 10f78d8332a..9021ba98a0f 100644
--- a/apps/dav/appinfo/info.xml
+++ b/apps/dav/appinfo/info.xml
@@ -1,22 +1,26 @@
<?xml version="1.0"?>
+<!--
+ - SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-FileCopyrightText: 2015-2016 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+ -->
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<id>dav</id>
<name>WebDAV</name>
<summary>WebDAV endpoint</summary>
<description>WebDAV endpoint</description>
- <version>1.23.0</version>
+ <version>1.34.0</version>
<licence>agpl</licence>
<author>owncloud.org</author>
<namespace>DAV</namespace>
- <default_enable/>
<types>
<filesystem/>
</types>
<category>integration</category>
<bugs>https://github.com/nextcloud/server/issues</bugs>
<dependencies>
- <nextcloud min-version="25" max-version="25"/>
+ <nextcloud min-version="32" max-version="32"/>
</dependencies>
<background-jobs>
@@ -25,6 +29,7 @@
<job>OCA\DAV\BackgroundJob\CleanupInvitationTokenJob</job>
<job>OCA\DAV\BackgroundJob\EventReminderJob</job>
<job>OCA\DAV\BackgroundJob\CalendarRetentionJob</job>
+ <job>OCA\DAV\BackgroundJob\PruneOutdatedSyncTokensJob</job>
</background-jobs>
<repair-steps>
@@ -36,30 +41,47 @@
<step>OCA\DAV\Migration\BuildSocialSearchIndex</step>
<step>OCA\DAV\Migration\RefreshWebcalJobRegistrar</step>
<step>OCA\DAV\Migration\RegisterBuildReminderIndexBackgroundJob</step>
+ <step>OCA\DAV\Migration\RegisterUpdateCalendarResourcesRoomBackgroundJob</step>
<step>OCA\DAV\Migration\RemoveOrphanEventsAndContacts</step>
<step>OCA\DAV\Migration\RemoveClassifiedEventActivity</step>
<step>OCA\DAV\Migration\RemoveDeletedUsersCalendarSubscriptions</step>
+ <step>OCA\DAV\Migration\RemoveObjectProperties</step>
</post-migration>
<live-migration>
<step>OCA\DAV\Migration\ChunkCleanup</step>
</live-migration>
+ <install>
+ <step>OCA\DAV\Migration\CreateSystemAddressBookStep</step>
+ </install>
</repair-steps>
<commands>
+ <command>OCA\DAV\Command\ClearCalendarUnshares</command>
+ <command>OCA\DAV\Command\ClearContactsPhotoCache</command>
<command>OCA\DAV\Command\CreateAddressBook</command>
<command>OCA\DAV\Command\CreateCalendar</command>
+ <command>OCA\DAV\Command\CreateSubscription</command>
<command>OCA\DAV\Command\DeleteCalendar</command>
- <command>OCA\DAV\Command\MoveCalendar</command>
+ <command>OCA\DAV\Command\DeleteSubscription</command>
+ <command>OCA\DAV\Command\ExportCalendar</command>
+ <command>OCA\DAV\Command\FixCalendarSyncCommand</command>
+ <command>OCA\DAV\Command\GetAbsenceCommand</command>
+ <command>OCA\DAV\Command\ListAddressbooks</command>
+ <command>OCA\DAV\Command\ListCalendarShares</command>
<command>OCA\DAV\Command\ListCalendars</command>
+ <command>OCA\DAV\Command\ListSubscriptions</command>
+ <command>OCA\DAV\Command\MoveCalendar</command>
+ <command>OCA\DAV\Command\RemoveInvalidShares</command>
<command>OCA\DAV\Command\RetentionCleanupCommand</command>
<command>OCA\DAV\Command\SendEventReminders</command>
+ <command>OCA\DAV\Command\SetAbsenceCommand</command>
<command>OCA\DAV\Command\SyncBirthdayCalendar</command>
<command>OCA\DAV\Command\SyncSystemAddressBook</command>
- <command>OCA\DAV\Command\RemoveInvalidShares</command>
</commands>
<settings>
<admin>OCA\DAV\Settings\CalDAVSettings</admin>
+ <admin>OCA\DAV\Settings\ExampleContentSettings</admin>
<personal>OCA\DAV\Settings\AvailabilitySettings</personal>
</settings>
@@ -85,8 +107,4 @@
<provider>OCA\DAV\CardDAV\Activity\Provider\Card</provider>
</providers>
</activity>
-
- <public>
- <webdav>appinfo/v1/publicwebdav.php</webdav>
- </public>
</info>
diff --git a/apps/dav/appinfo/routes.php b/apps/dav/appinfo/routes.php
index 6d237ee2a1f..dba7bcbbdf0 100644
--- a/apps/dav/appinfo/routes.php
+++ b/apps/dav/appinfo/routes.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright 2017, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @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
*/
return [
'routes' => [
@@ -28,9 +11,14 @@ return [
['name' => 'invitation_response#accept', 'url' => '/invitation/accept/{token}', 'verb' => 'GET'],
['name' => 'invitation_response#decline', 'url' => '/invitation/decline/{token}', 'verb' => 'GET'],
['name' => 'invitation_response#options', 'url' => '/invitation/moreOptions/{token}', 'verb' => 'GET'],
- ['name' => 'invitation_response#processMoreOptionsResult', 'url' => '/invitation/moreOptions/{token}', 'verb' => 'POST']
+ ['name' => 'invitation_response#processMoreOptionsResult', 'url' => '/invitation/moreOptions/{token}', 'verb' => 'POST'],
],
'ocs' => [
['name' => 'direct#getUrl', 'url' => '/api/v1/direct', 'verb' => 'POST'],
+ ['name' => 'upcoming_events#getEvents', 'url' => '/api/v1/events/upcoming', 'verb' => 'GET'],
+ ['name' => 'out_of_office#getCurrentOutOfOfficeData', 'url' => '/api/v1/outOfOffice/{userId}/now', 'verb' => 'GET'],
+ ['name' => 'out_of_office#getOutOfOffice', 'url' => '/api/v1/outOfOffice/{userId}', 'verb' => 'GET'],
+ ['name' => 'out_of_office#setOutOfOffice', 'url' => '/api/v1/outOfOffice/{userId}', 'verb' => 'POST'],
+ ['name' => 'out_of_office#clearOutOfOffice', 'url' => '/api/v1/outOfOffice/{userId}', 'verb' => 'DELETE'],
],
];
diff --git a/apps/dav/appinfo/v1/caldav.php b/apps/dav/appinfo/v1/caldav.php
index 75400264f75..2cee1866a36 100644
--- a/apps/dav/appinfo/v1/caldav.php
+++ b/apps/dav/appinfo/v1/caldav.php
@@ -1,90 +1,87 @@
<?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 Morris Jobke <hey@morrisjobke.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
*/
// Backends
use OC\KnownUser\KnownUserService;
use OCA\DAV\CalDAV\CalDavBackend;
-use OCA\DAV\Connector\LegacyDAVACL;
use OCA\DAV\CalDAV\CalendarRoot;
+use OCA\DAV\CalDAV\DefaultCalendarValidator;
+use OCA\DAV\CalDAV\Proxy\ProxyMapper;
+use OCA\DAV\CalDAV\Schedule\IMipPlugin;
+use OCA\DAV\CalDAV\Security\RateLimitingPlugin;
+use OCA\DAV\CalDAV\Validation\CalDavValidatePlugin;
+use OCA\DAV\Connector\LegacyDAVACL;
use OCA\DAV\Connector\Sabre\Auth;
use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin;
use OCA\DAV\Connector\Sabre\MaintenancePlugin;
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\IRequest;
+use OCP\ISession;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use OCP\Security\Bruteforce\IThrottler;
+use OCP\Security\ISecureRandom;
+use OCP\Server;
use Psr\Log\LoggerInterface;
$authBackend = new Auth(
- \OC::$server->getSession(),
- \OC::$server->getUserSession(),
- \OC::$server->getRequest(),
- \OC::$server->getTwoFactorAuthManager(),
- \OC::$server->getBruteForceThrottler(),
+ Server::get(ISession::class),
+ Server::get(IUserSession::class),
+ Server::get(IRequest::class),
+ Server::get(\OC\Authentication\TwoFactorAuth\Manager::class),
+ Server::get(IThrottler::class),
'principals/'
);
$principalBackend = new Principal(
- \OC::$server->getUserManager(),
- \OC::$server->getGroupManager(),
- \OC::$server->getShareManager(),
- \OC::$server->getUserSession(),
- \OC::$server->getAppManager(),
- \OC::$server->query(\OCA\DAV\CalDAV\Proxy\ProxyMapper::class),
- \OC::$server->get(KnownUserService::class),
- \OC::$server->getConfig(),
+ Server::get(IUserManager::class),
+ Server::get(IGroupManager::class),
+ 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(),
'principals/'
);
-$db = \OC::$server->getDatabaseConnection();
-$userManager = \OC::$server->getUserManager();
-$random = \OC::$server->getSecureRandom();
-$logger = \OC::$server->getLogger();
-$dispatcher = \OC::$server->get(\OCP\EventDispatcher\IEventDispatcher::class);
-$legacyDispatcher = \OC::$server->getEventDispatcher();
-$config = \OC::$server->get(\OCP\IConfig::class);
+$db = Server::get(IDBConnection::class);
+$userManager = Server::get(IUserManager::class);
+$random = Server::get(ISecureRandom::class);
+$logger = Server::get(LoggerInterface::class);
+$dispatcher = Server::get(IEventDispatcher::class);
+$config = Server::get(IConfig::class);
$calDavBackend = new CalDavBackend(
$db,
$principalBackend,
$userManager,
- \OC::$server->getGroupManager(),
$random,
$logger,
$dispatcher,
- $legacyDispatcher,
$config,
+ Server::get(\OCA\DAV\CalDAV\Sharing\Backend::class),
true
);
-$debugging = \OC::$server->getConfig()->getSystemValue('debug', false);
-$sendInvitations = \OC::$server->getConfig()->getAppValue('dav', 'sendInvitations', 'yes') === 'yes';
+$debugging = Server::get(IConfig::class)->getSystemValue('debug', false);
+$sendInvitations = Server::get(IConfig::class)->getAppValue('dav', 'sendInvitations', 'yes') === 'yes';
// Root nodes
$principalCollection = new \Sabre\CalDAV\Principal\Collection($principalBackend);
$principalCollection->disableListing = !$debugging; // Disable listing
-$addressBookRoot = new CalendarRoot($principalBackend, $calDavBackend, 'principals', \OC::$server->get(LoggerInterface::class));
+$addressBookRoot = new CalendarRoot($principalBackend, $calDavBackend, 'principals', $logger);
$addressBookRoot->disableListing = !$debugging; // Disable listing
$nodes = [
@@ -95,12 +92,12 @@ $nodes = [
// Fire up server
$server = new \Sabre\DAV\Server($nodes);
$server::$exposeVersion = false;
-$server->httpRequest->setUrl(\OC::$server->getRequest()->getRequestUri());
+$server->httpRequest->setUrl(Server::get(IRequest::class)->getRequestUri());
$server->setBaseUri($baseuri);
// Add plugins
-$server->addPlugin(new MaintenancePlugin(\OC::$server->getConfig(), \OC::$server->getL10N('dav')));
-$server->addPlugin(new \Sabre\DAV\Auth\Plugin($authBackend, 'ownCloud'));
+$server->addPlugin(new MaintenancePlugin(Server::get(IConfig::class), \OC::$server->getL10N('dav')));
+$server->addPlugin(new \Sabre\DAV\Auth\Plugin($authBackend));
$server->addPlugin(new \Sabre\CalDAV\Plugin());
$server->addPlugin(new LegacyDAVACL());
@@ -110,12 +107,14 @@ if ($debugging) {
$server->addPlugin(new \Sabre\DAV\Sync\Plugin());
$server->addPlugin(new \Sabre\CalDAV\ICSExportPlugin());
-$server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(\OC::$server->getConfig()));
+$server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(Server::get(IConfig::class), Server::get(LoggerInterface::class), Server::get(DefaultCalendarValidator::class)));
if ($sendInvitations) {
- $server->addPlugin(\OC::$server->query(\OCA\DAV\CalDAV\Schedule\IMipPlugin::class));
+ $server->addPlugin(Server::get(IMipPlugin::class));
}
-$server->addPlugin(new ExceptionLoggerPlugin('caldav', \OC::$server->getLogger()));
+$server->addPlugin(new ExceptionLoggerPlugin('caldav', $logger));
+$server->addPlugin(Server::get(RateLimitingPlugin::class));
+$server->addPlugin(Server::get(CalDavValidatePlugin::class));
// And off we go!
$server->exec();
diff --git a/apps/dav/appinfo/v1/carddav.php b/apps/dav/appinfo/v1/carddav.php
index 53449b91c4b..415a5c9634a 100644
--- a/apps/dav/appinfo/v1/carddav.php
+++ b/apps/dav/appinfo/v1/carddav.php
@@ -1,74 +1,79 @@
<?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 John Molakvoæ <skjnldsv@protonmail.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @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
*/
// Backends
use OC\KnownUser\KnownUserService;
use OCA\DAV\AppInfo\PluginManager;
+use OCA\DAV\CalDAV\Proxy\ProxyMapper;
use OCA\DAV\CardDAV\AddressBookRoot;
use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\CardDAV\ImageExportPlugin;
+use OCA\DAV\CardDAV\PhotoCache;
+use OCA\DAV\CardDAV\Security\CardDavRateLimitingPlugin;
+use OCA\DAV\CardDAV\Validation\CardDavValidatePlugin;
use OCA\DAV\Connector\LegacyDAVACL;
use OCA\DAV\Connector\Sabre\Auth;
use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin;
use OCA\DAV\Connector\Sabre\MaintenancePlugin;
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\IRequest;
+use OCP\ISession;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use OCP\Security\Bruteforce\IThrottler;
+use OCP\Server;
+use Psr\Log\LoggerInterface;
use Sabre\CardDAV\Plugin;
$authBackend = new Auth(
- \OC::$server->getSession(),
- \OC::$server->getUserSession(),
- \OC::$server->getRequest(),
- \OC::$server->getTwoFactorAuthManager(),
- \OC::$server->getBruteForceThrottler(),
+ Server::get(ISession::class),
+ Server::get(IUserSession::class),
+ Server::get(IRequest::class),
+ Server::get(\OC\Authentication\TwoFactorAuth\Manager::class),
+ Server::get(IThrottler::class),
'principals/'
);
$principalBackend = new Principal(
- \OC::$server->getUserManager(),
- \OC::$server->getGroupManager(),
- \OC::$server->getShareManager(),
- \OC::$server->getUserSession(),
- \OC::$server->getAppManager(),
- \OC::$server->query(\OCA\DAV\CalDAV\Proxy\ProxyMapper::class),
- \OC::$server->get(KnownUserService::class),
- \OC::$server->getConfig(),
+ Server::get(IUserManager::class),
+ Server::get(IGroupManager::class),
+ 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(),
'principals/'
);
-$db = \OC::$server->getDatabaseConnection();
-$cardDavBackend = new CardDavBackend($db, $principalBackend, \OC::$server->getUserManager(), \OC::$server->getGroupManager(), \OC::$server->get(\OCP\EventDispatcher\IEventDispatcher::class), \OC::$server->getEventDispatcher());
+$db = Server::get(IDBConnection::class);
+$cardDavBackend = new CardDavBackend(
+ $db,
+ $principalBackend,
+ Server::get(IUserManager::class),
+ Server::get(IEventDispatcher::class),
+ Server::get(\OCA\DAV\CardDAV\Sharing\Backend::class),
+ Server::get(IConfig::class),
+);
-$debugging = \OC::$server->getConfig()->getSystemValue('debug', false);
+$debugging = Server::get(IConfig::class)->getSystemValue('debug', false);
// Root nodes
$principalCollection = new \Sabre\CalDAV\Principal\Collection($principalBackend);
$principalCollection->disableListing = !$debugging; // Disable listing
-$pluginManager = new PluginManager(\OC::$server, \OC::$server->query(IAppManager::class));
-$addressBookRoot = new AddressBookRoot($principalBackend, $cardDavBackend, $pluginManager);
+$pluginManager = new PluginManager(\OC::$server, Server::get(IAppManager::class));
+$addressBookRoot = new AddressBookRoot($principalBackend, $cardDavBackend, $pluginManager, Server::get(IUserSession::class)->getUser(), Server::get(IGroupManager::class));
$addressBookRoot->disableListing = !$debugging; // Disable listing
$nodes = [
@@ -79,11 +84,11 @@ $nodes = [
// Fire up server
$server = new \Sabre\DAV\Server($nodes);
$server::$exposeVersion = false;
-$server->httpRequest->setUrl(\OC::$server->getRequest()->getRequestUri());
+$server->httpRequest->setUrl(Server::get(IRequest::class)->getRequestUri());
$server->setBaseUri($baseuri);
// Add plugins
-$server->addPlugin(new MaintenancePlugin(\OC::$server->getConfig(), \OC::$server->getL10N('dav')));
-$server->addPlugin(new \Sabre\DAV\Auth\Plugin($authBackend, 'ownCloud'));
+$server->addPlugin(new MaintenancePlugin(Server::get(IConfig::class), \OC::$server->getL10N('dav')));
+$server->addPlugin(new \Sabre\DAV\Auth\Plugin($authBackend));
$server->addPlugin(new Plugin());
$server->addPlugin(new LegacyDAVACL());
@@ -93,11 +98,10 @@ if ($debugging) {
$server->addPlugin(new \Sabre\DAV\Sync\Plugin());
$server->addPlugin(new \Sabre\CardDAV\VCFExportPlugin());
-$server->addPlugin(new \OCA\DAV\CardDAV\ImageExportPlugin(new \OCA\DAV\CardDAV\PhotoCache(
- \OC::$server->getAppDataDir('dav-photocache'),
- \OC::$server->getLogger()
-)));
-$server->addPlugin(new ExceptionLoggerPlugin('carddav', \OC::$server->getLogger()));
+$server->addPlugin(new ImageExportPlugin(Server::get(PhotoCache::class)));
+$server->addPlugin(new ExceptionLoggerPlugin('carddav', Server::get(LoggerInterface::class)));
+$server->addPlugin(Server::get(CardDavRateLimitingPlugin::class));
+$server->addPlugin(Server::get(CardDavValidatePlugin::class));
// And off we go!
$server->exec();
diff --git a/apps/dav/appinfo/v1/publicwebdav.php b/apps/dav/appinfo/v1/publicwebdav.php
index c99c5bcd833..af49ca5462c 100644
--- a/apps/dav/appinfo/v1/publicwebdav.php
+++ b/apps/dav/appinfo/v1/publicwebdav.php
@@ -1,73 +1,77 @@
<?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 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
*/
+use OC\Files\Filesystem;
+use OC\Files\Storage\Wrapper\PermissionsMask;
+use OC\Files\View;
+use OCA\DAV\Connector\LegacyPublicAuth;
+use OCA\DAV\Connector\Sabre\ServerFactory;
+use OCA\DAV\Files\Sharing\FilesDropPlugin;
+use OCA\DAV\Files\Sharing\PublicLinkCheckPlugin;
+use OCA\DAV\Storage\PublicOwnerWrapper;
+use OCA\FederatedFileSharing\FederatedShareProvider;
+use OCP\BeforeSabrePubliclyLoadedEvent;
+use OCP\Constants;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\IRootFolder;
+use OCP\Files\Mount\IMountManager;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IPreview;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\ITagManager;
+use OCP\IUserSession;
+use OCP\Security\Bruteforce\IThrottler;
+use OCP\Server;
+use Psr\Log\LoggerInterface;
+
// load needed apps
$RUNTIME_APPTYPES = ['filesystem', 'authentication', 'logging'];
OC_App::loadApps($RUNTIME_APPTYPES);
OC_Util::obEnd();
-\OC::$server->getSession()->close();
+Server::get(ISession::class)->close();
// Backends
-$authBackend = new OCA\DAV\Connector\PublicAuth(
- \OC::$server->getRequest(),
- \OC::$server->getShareManager(),
- \OC::$server->getSession(),
- \OC::$server->getBruteForceThrottler()
+$authBackend = new LegacyPublicAuth(
+ Server::get(IRequest::class),
+ Server::get(\OCP\Share\IManager::class),
+ Server::get(ISession::class),
+ Server::get(IThrottler::class)
);
$authPlugin = new \Sabre\DAV\Auth\Plugin($authBackend);
-$serverFactory = new OCA\DAV\Connector\Sabre\ServerFactory(
- \OC::$server->getConfig(),
- \OC::$server->getLogger(),
- \OC::$server->getDatabaseConnection(),
- \OC::$server->getUserSession(),
- \OC::$server->getMountManager(),
- \OC::$server->getTagManager(),
- \OC::$server->getRequest(),
- \OC::$server->getPreviewManager(),
- \OC::$server->getEventDispatcher(),
+/** @var IEventDispatcher $eventDispatcher */
+$eventDispatcher = Server::get(IEventDispatcher::class);
+
+$serverFactory = new ServerFactory(
+ Server::get(IConfig::class),
+ Server::get(LoggerInterface::class),
+ Server::get(IDBConnection::class),
+ Server::get(IUserSession::class),
+ Server::get(IMountManager::class),
+ Server::get(ITagManager::class),
+ Server::get(IRequest::class),
+ Server::get(IPreview::class),
+ $eventDispatcher,
\OC::$server->getL10N('dav')
);
-$requestUri = \OC::$server->getRequest()->getRequestUri();
+$requestUri = Server::get(IRequest::class)->getRequestUri();
-$linkCheckPlugin = new \OCA\DAV\Files\Sharing\PublicLinkCheckPlugin();
-$filesDropPlugin = new \OCA\DAV\Files\Sharing\FilesDropPlugin();
+$linkCheckPlugin = new PublicLinkCheckPlugin();
+$filesDropPlugin = new FilesDropPlugin();
-$server = $serverFactory->createServer($baseuri, $requestUri, $authPlugin, function (\Sabre\DAV\Server $server) use ($authBackend, $linkCheckPlugin, $filesDropPlugin) {
- $isAjax = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest');
- /** @var \OCA\FederatedFileSharing\FederatedShareProvider $shareProvider */
- $federatedShareProvider = \OC::$server->query(\OCA\FederatedFileSharing\FederatedShareProvider::class);
+$server = $serverFactory->createServer(false, $baseuri, $requestUri, $authPlugin, function (\Sabre\DAV\Server $server) use ($authBackend, $linkCheckPlugin, $filesDropPlugin) {
+ $isAjax = in_array('XMLHttpRequest', explode(',', $_SERVER['HTTP_X_REQUESTED_WITH'] ?? ''));
+ /** @var FederatedShareProvider $shareProvider */
+ $federatedShareProvider = Server::get(FederatedShareProvider::class);
if ($federatedShareProvider->isOutgoingServer2serverShareEnabled() === false && !$isAjax) {
// this is what is thrown when trying to access a non-existing share
throw new \Sabre\DAV\Exception\NotAuthenticated();
@@ -75,39 +79,42 @@ $server = $serverFactory->createServer($baseuri, $requestUri, $authPlugin, funct
$share = $authBackend->getShare();
$owner = $share->getShareOwner();
- $isReadable = $share->getPermissions() & \OCP\Constants::PERMISSION_READ;
+ $isReadable = $share->getPermissions() & Constants::PERMISSION_READ;
$fileId = $share->getNodeId();
// FIXME: should not add storage wrappers outside of preSetup, need to find a better way
- $previousLog = \OC\Files\Filesystem::logWarningWhenAddingStorageWrapper(false);
- \OC\Files\Filesystem::addStorageWrapper('sharePermissions', function ($mountPoint, $storage) use ($share) {
- return new \OC\Files\Storage\Wrapper\PermissionsMask(['storage' => $storage, 'mask' => $share->getPermissions() | \OCP\Constants::PERMISSION_SHARE]);
+ $previousLog = Filesystem::logWarningWhenAddingStorageWrapper(false);
+ Filesystem::addStorageWrapper('sharePermissions', function ($mountPoint, $storage) use ($share) {
+ return new PermissionsMask(['storage' => $storage, 'mask' => $share->getPermissions() | Constants::PERMISSION_SHARE]);
});
- \OC\Files\Filesystem::addStorageWrapper('shareOwner', function ($mountPoint, $storage) use ($share) {
- return new \OCA\DAV\Storage\PublicOwnerWrapper(['storage' => $storage, 'owner' => $share->getShareOwner()]);
+ Filesystem::addStorageWrapper('shareOwner', function ($mountPoint, $storage) use ($share) {
+ return new PublicOwnerWrapper(['storage' => $storage, 'owner' => $share->getShareOwner()]);
});
- \OC\Files\Filesystem::logWarningWhenAddingStorageWrapper($previousLog);
+ Filesystem::logWarningWhenAddingStorageWrapper($previousLog);
- OC_Util::tearDownFS();
- OC_Util::setupFS($owner);
- $ownerView = new \OC\Files\View('/'. $owner . '/files');
- $path = $ownerView->getPath($fileId);
- $fileInfo = $ownerView->getFileInfo($path);
- $linkCheckPlugin->setFileInfo($fileInfo);
+ $rootFolder = Server::get(IRootFolder::class);
+ $userFolder = $rootFolder->getUserFolder($owner);
+ $node = $userFolder->getFirstNodeById($fileId);
+ if (!$node) {
+ throw new \Sabre\DAV\Exception\NotFound();
+ }
+ $linkCheckPlugin->setFileInfo($node);
- // If not readble (files_drop) enable the filesdrop plugin
+ // If not readable (files_drop) enable the filesdrop plugin
if (!$isReadable) {
$filesDropPlugin->enable();
}
+ $filesDropPlugin->setShare($share);
- $view = new \OC\Files\View($ownerView->getAbsolutePath($path));
- $filesDropPlugin->setView($view);
-
+ $view = new View($node->getPath());
return $view;
});
$server->addPlugin($linkCheckPlugin);
$server->addPlugin($filesDropPlugin);
+// allow setup of additional plugins
+$event = new BeforeSabrePubliclyLoadedEvent($server);
+$eventDispatcher->dispatchTyped($event);
// And off we go!
$server->exec();
diff --git a/apps/dav/appinfo/v1/webdav.php b/apps/dav/appinfo/v1/webdav.php
index 7b0a0ae11df..92ff55c850e 100644
--- a/apps/dav/appinfo/v1/webdav.php
+++ b/apps/dav/appinfo/v1/webdav.php
@@ -1,35 +1,31 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Ko- <k.stoffelen@cs.ru.nl>
- * @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
*/
+use OC\Files\Filesystem;
+use OCA\DAV\Connector\Sabre\Auth;
+use OCA\DAV\Connector\Sabre\BearerAuth;
+use OCA\DAV\Connector\Sabre\ServerFactory;
+use OCA\DAV\Events\SabrePluginAddEvent;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\Mount\IMountManager;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IPreview;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\ITagManager;
+use OCP\IUserSession;
+use OCP\SabrePluginEvent;
+use OCP\Security\Bruteforce\IThrottler;
+use OCP\Server;
+use Psr\Log\LoggerInterface;
+
// no php execution timeout for webdav
-if (strpos(@ini_get('disable_functions'), 'set_time_limit') === false) {
+if (!str_contains(@ini_get('disable_functions'), 'set_time_limit')) {
@set_time_limit(0);
}
ignore_user_abort(true);
@@ -37,47 +33,51 @@ ignore_user_abort(true);
// Turn off output buffering to prevent memory problems
\OC_Util::obEnd();
-$serverFactory = new \OCA\DAV\Connector\Sabre\ServerFactory(
- \OC::$server->getConfig(),
- \OC::$server->getLogger(),
- \OC::$server->getDatabaseConnection(),
- \OC::$server->getUserSession(),
- \OC::$server->getMountManager(),
- \OC::$server->getTagManager(),
- \OC::$server->getRequest(),
- \OC::$server->getPreviewManager(),
- \OC::$server->getEventDispatcher(),
+$dispatcher = Server::get(IEventDispatcher::class);
+
+$serverFactory = new ServerFactory(
+ Server::get(IConfig::class),
+ Server::get(LoggerInterface::class),
+ Server::get(IDBConnection::class),
+ Server::get(IUserSession::class),
+ Server::get(IMountManager::class),
+ Server::get(ITagManager::class),
+ Server::get(IRequest::class),
+ Server::get(IPreview::class),
+ $dispatcher,
\OC::$server->getL10N('dav')
);
// Backends
-$authBackend = new \OCA\DAV\Connector\Sabre\Auth(
- \OC::$server->getSession(),
- \OC::$server->getUserSession(),
- \OC::$server->getRequest(),
- \OC::$server->getTwoFactorAuthManager(),
- \OC::$server->getBruteForceThrottler(),
+$authBackend = new Auth(
+ Server::get(ISession::class),
+ Server::get(IUserSession::class),
+ Server::get(IRequest::class),
+ Server::get(\OC\Authentication\TwoFactorAuth\Manager::class),
+ Server::get(IThrottler::class),
'principals/'
);
$authPlugin = new \Sabre\DAV\Auth\Plugin($authBackend);
-$bearerAuthPlugin = new \OCA\DAV\Connector\Sabre\BearerAuth(
- \OC::$server->getUserSession(),
- \OC::$server->getSession(),
- \OC::$server->getRequest()
+$bearerAuthPlugin = new BearerAuth(
+ Server::get(IUserSession::class),
+ Server::get(ISession::class),
+ Server::get(IRequest::class),
+ Server::get(IConfig::class),
);
$authPlugin->addBackend($bearerAuthPlugin);
-$requestUri = \OC::$server->getRequest()->getRequestUri();
+$requestUri = Server::get(IRequest::class)->getRequestUri();
-$server = $serverFactory->createServer($baseuri, $requestUri, $authPlugin, function () {
+$server = $serverFactory->createServer(false, $baseuri, $requestUri, $authPlugin, function () {
// use the view for the logged in user
- return \OC\Files\Filesystem::getView();
+ return Filesystem::getView();
});
-$dispatcher = \OC::$server->getEventDispatcher();
// allow setup of additional plugins
-$event = new \OCP\SabrePluginEvent($server);
+$event = new SabrePluginEvent($server);
$dispatcher->dispatch('OCA\DAV\Connector\Sabre::addPlugin', $event);
+$event = new SabrePluginAddEvent($server);
+$dispatcher->dispatchTyped($event);
// And off we go!
$server->exec();
diff --git a/apps/dav/appinfo/v2/direct.php b/apps/dav/appinfo/v2/direct.php
index cab6109e5e6..78156317029 100644
--- a/apps/dav/appinfo/v2/direct.php
+++ b/apps/dav/appinfo/v2/direct.php
@@ -3,31 +3,19 @@
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
*/
-use \OCA\DAV\Direct\ServerFactory;
+use OCA\DAV\Db\DirectMapper;
+use OCA\DAV\Direct\ServerFactory;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Files\IRootFolder;
+use OCP\IRequest;
+use OCP\Security\Bruteforce\IThrottler;
+use OCP\Server;
// no php execution timeout for webdav
-if (strpos(@ini_get('disable_functions'), 'set_time_limit') === false) {
+if (!str_contains(@ini_get('disable_functions'), 'set_time_limit')) {
@set_time_limit(0);
}
ignore_user_abort(true);
@@ -35,18 +23,18 @@ ignore_user_abort(true);
// Turn off output buffering to prevent memory problems
\OC_Util::obEnd();
-$requestUri = \OC::$server->getRequest()->getRequestUri();
+$requestUri = Server::get(IRequest::class)->getRequestUri();
/** @var ServerFactory $serverFactory */
-$serverFactory = \OC::$server->query(ServerFactory::class);
+$serverFactory = Server::get(ServerFactory::class);
$server = $serverFactory->createServer(
$baseuri,
$requestUri,
- \OC::$server->getRootFolder(),
- \OC::$server->query(\OCA\DAV\Db\DirectMapper::class),
- \OC::$server->query(\OCP\AppFramework\Utility\ITimeFactory::class),
- \OC::$server->getBruteForceThrottler(),
- \OC::$server->getRequest()
+ Server::get(IRootFolder::class),
+ Server::get(DirectMapper::class),
+ Server::get(ITimeFactory::class),
+ Server::get(IThrottler::class),
+ Server::get(IRequest::class)
);
$server->exec();
diff --git a/apps/dav/appinfo/v2/publicremote.php b/apps/dav/appinfo/v2/publicremote.php
new file mode 100644
index 00000000000..e089fa7bb62
--- /dev/null
+++ b/apps/dav/appinfo/v2/publicremote.php
@@ -0,0 +1,152 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2020-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+use OC\Files\Filesystem;
+use OC\Files\Storage\Wrapper\PermissionsMask;
+use OC\Files\View;
+use OCA\DAV\Connector\Sabre\PublicAuth;
+use OCA\DAV\Connector\Sabre\ServerFactory;
+use OCA\DAV\Files\Sharing\FilesDropPlugin;
+use OCA\DAV\Files\Sharing\PublicLinkCheckPlugin;
+use OCA\DAV\Storage\PublicOwnerWrapper;
+use OCA\DAV\Storage\PublicShareWrapper;
+use OCA\DAV\Upload\ChunkingPlugin;
+use OCA\DAV\Upload\ChunkingV2Plugin;
+use OCA\FederatedFileSharing\FederatedShareProvider;
+use OCP\BeforeSabrePubliclyLoadedEvent;
+use OCP\Constants;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\IRootFolder;
+use OCP\Files\Mount\IMountManager;
+use OCP\ICacheFactory;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IPreview;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\ITagManager;
+use OCP\IURLGenerator;
+use OCP\IUserSession;
+use OCP\L10N\IFactory;
+use OCP\Security\Bruteforce\IThrottler;
+use OCP\Server;
+use OCP\Share\IManager;
+use Psr\Log\LoggerInterface;
+use Sabre\DAV\Exception\NotAuthenticated;
+use Sabre\DAV\Exception\NotFound;
+
+// load needed apps
+$RUNTIME_APPTYPES = ['filesystem', 'authentication', 'logging'];
+OC_App::loadApps($RUNTIME_APPTYPES);
+OC_Util::obEnd();
+
+$session = Server::get(ISession::class);
+$request = Server::get(IRequest::class);
+$eventDispatcher = Server::get(IEventDispatcher::class);
+
+$session->close();
+$requestUri = $request->getRequestUri();
+
+// Backends
+$authBackend = new PublicAuth(
+ $request,
+ Server::get(IManager::class),
+ $session,
+ Server::get(IThrottler::class),
+ Server::get(LoggerInterface::class),
+ Server::get(IURLGenerator::class),
+);
+$authPlugin = new \Sabre\DAV\Auth\Plugin($authBackend);
+
+$l10nFactory = Server::get(IFactory::class);
+$serverFactory = new ServerFactory(
+ Server::get(IConfig::class),
+ Server::get(LoggerInterface::class),
+ Server::get(IDBConnection::class),
+ Server::get(IUserSession::class),
+ Server::get(IMountManager::class),
+ Server::get(ITagManager::class),
+ $request,
+ Server::get(IPreview::class),
+ $eventDispatcher,
+ $l10nFactory->get('dav'),
+);
+
+
+$linkCheckPlugin = new PublicLinkCheckPlugin();
+$filesDropPlugin = new FilesDropPlugin();
+
+/** @var string $baseuri defined in public.php */
+$server = $serverFactory->createServer(true, $baseuri, $requestUri, $authPlugin, function (\Sabre\DAV\Server $server) use ($authBackend, $linkCheckPlugin, $filesDropPlugin) {
+ // GET must be allowed for e.g. showing images and allowing Zip downloads
+ if ($server->httpRequest->getMethod() !== 'GET') {
+ // If this is *not* a GET request we only allow access to public DAV from AJAX or when Server2Server is allowed
+ $isAjax = in_array('XMLHttpRequest', explode(',', $_SERVER['HTTP_X_REQUESTED_WITH'] ?? ''));
+ $federatedShareProvider = Server::get(FederatedShareProvider::class);
+ if ($federatedShareProvider->isOutgoingServer2serverShareEnabled() === false && $isAjax === false) {
+ // this is what is thrown when trying to access a non-existing share
+ throw new NotAuthenticated();
+ }
+ }
+
+ $share = $authBackend->getShare();
+ $owner = $share->getShareOwner();
+ $isReadable = $share->getPermissions() & Constants::PERMISSION_READ;
+ $fileId = $share->getNodeId();
+
+ // FIXME: should not add storage wrappers outside of preSetup, need to find a better way
+ /** @psalm-suppress InternalMethod */
+ $previousLog = Filesystem::logWarningWhenAddingStorageWrapper(false);
+
+ /** @psalm-suppress MissingClosureParamType */
+ Filesystem::addStorageWrapper('sharePermissions', function ($mountPoint, $storage) use ($share) {
+ return new PermissionsMask(['storage' => $storage, 'mask' => $share->getPermissions() | Constants::PERMISSION_SHARE]);
+ });
+
+ /** @psalm-suppress MissingClosureParamType */
+ Filesystem::addStorageWrapper('shareOwner', function ($mountPoint, $storage) use ($share) {
+ return new PublicOwnerWrapper(['storage' => $storage, 'owner' => $share->getShareOwner()]);
+ });
+
+ // Ensure that also private shares have the `getShare` method
+ /** @psalm-suppress MissingClosureParamType */
+ Filesystem::addStorageWrapper('getShare', function ($mountPoint, $storage) use ($share) {
+ return new PublicShareWrapper(['storage' => $storage, 'share' => $share]);
+ }, 0);
+
+ /** @psalm-suppress InternalMethod */
+ Filesystem::logWarningWhenAddingStorageWrapper($previousLog);
+
+ $rootFolder = Server::get(IRootFolder::class);
+ $userFolder = $rootFolder->getUserFolder($owner);
+ $node = $userFolder->getFirstNodeById($fileId);
+ if (!$node) {
+ throw new NotFound();
+ }
+ $linkCheckPlugin->setFileInfo($node);
+
+ // If not readable (files_drop) enable the filesdrop plugin
+ if (!$isReadable) {
+ $filesDropPlugin->enable();
+ }
+ $filesDropPlugin->setShare($share);
+
+ $view = new View($node->getPath());
+ return $view;
+});
+
+$server->addPlugin($linkCheckPlugin);
+$server->addPlugin($filesDropPlugin);
+$server->addPlugin(new ChunkingV2Plugin(Server::get(ICacheFactory::class)));
+$server->addPlugin(new ChunkingPlugin());
+
+// allow setup of additional plugins
+$event = new BeforeSabrePubliclyLoadedEvent($server);
+$eventDispatcher->dispatchTyped($event);
+
+// And off we go!
+$server->start();
diff --git a/apps/dav/appinfo/v2/remote.php b/apps/dav/appinfo/v2/remote.php
index de2ba989f8d..51d26b48a5a 100644
--- a/apps/dav/appinfo/v2/remote.php
+++ b/apps/dav/appinfo/v2/remote.php
@@ -1,28 +1,15 @@
<?php
+
+use OCA\DAV\Server;
+use OCP\IRequest;
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Ko- <k.stoffelen@cs.ru.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
*/
// no php execution timeout for webdav
-if (strpos(@ini_get('disable_functions'), 'set_time_limit') === false) {
+if (!str_contains(@ini_get('disable_functions'), 'set_time_limit')) {
@set_time_limit(0);
}
ignore_user_abort(true);
@@ -30,6 +17,6 @@ ignore_user_abort(true);
// Turn off output buffering to prevent memory problems
\OC_Util::obEnd();
-$request = \OC::$server->getRequest();
-$server = new \OCA\DAV\Server($request, $baseuri);
+$request = \OCP\Server::get(IRequest::class);
+$server = new Server($request, $baseuri);
$server->exec();
diff --git a/apps/dav/bin/chunkperf.php b/apps/dav/bin/chunkperf.php
deleted file mode 100644
index a8652654bca..00000000000
--- a/apps/dav/bin/chunkperf.php
+++ /dev/null
@@ -1,79 +0,0 @@
-<?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/>
- *
- */
-require '../../../../3rdparty/autoload.php';
-
-if ($argc !== 6) {
- echo "Invalid number of arguments" . PHP_EOL;
- exit;
-}
-
-/**
- * @param \Sabre\DAV\Client $client
- * @param $uploadUrl
- * @return mixed
- */
-function request($client, $method, $uploadUrl, $data = null, $headers = []) {
- echo "$method $uploadUrl ... ";
- $t0 = microtime(true);
- $result = $client->request($method, $uploadUrl, $data, $headers);
- $t1 = microtime(true);
- echo $result['statusCode'] . " - " . ($t1 - $t0) . ' seconds' . PHP_EOL;
- if (!in_array($result['statusCode'], [200, 201])) {
- echo $result['body'] . PHP_EOL;
- }
- return $result;
-}
-
-$baseUri = $argv[1];
-$userName = $argv[2];
-$password = $argv[3];
-$file = $argv[4];
-$chunkSize = $argv[5] * 1024 * 1024;
-
-$client = new \Sabre\DAV\Client([
- 'baseUri' => $baseUri,
- 'userName' => $userName,
- 'password' => $password
-]);
-
-$transfer = uniqid('transfer', true);
-$uploadUrl = "$baseUri/uploads/$userName/$transfer";
-
-request($client, 'MKCOL', $uploadUrl);
-
-$size = filesize($file);
-$stream = fopen($file, 'r');
-
-$index = 0;
-while (!feof($stream)) {
- request($client, 'PUT', "$uploadUrl/$index", fread($stream, $chunkSize));
- $index++;
-}
-
-$destination = pathinfo($file, PATHINFO_BASENAME);
-//echo "Moving $uploadUrl/.file to it's final destination $baseUri/files/$userName/$destination" . PHP_EOL;
-request($client, 'MOVE', "$uploadUrl/.file", null, [
- 'Destination' => "$baseUri/files/$userName/$destination",
- 'OC-Total-Length' => filesize($file),
- 'X-OC-MTime' => filemtime($file)
-]);
diff --git a/apps/dav/composer/autoload.php b/apps/dav/composer/autoload.php
index a3040af8caa..0103857e976 100644
--- a/apps/dav/composer/autoload.php
+++ b/apps/dav/composer/autoload.php
@@ -3,8 +3,21 @@
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
- echo 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
- exit(1);
+ if (!headers_sent()) {
+ header('HTTP/1.1 500 Internal Server Error');
+ }
+ $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
+ if (!ini_get('display_errors')) {
+ if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+ fwrite(STDERR, $err);
+ } elseif (!headers_sent()) {
+ echo $err;
+ }
+ }
+ trigger_error(
+ $err,
+ E_USER_ERROR
+ );
}
require_once __DIR__ . '/composer/autoload_real.php';
diff --git a/apps/dav/composer/composer/ClassLoader.php b/apps/dav/composer/composer/ClassLoader.php
index afef3fa2ad8..7824d8f7eaf 100644
--- a/apps/dav/composer/composer/ClassLoader.php
+++ b/apps/dav/composer/composer/ClassLoader.php
@@ -42,35 +42,37 @@ namespace Composer\Autoload;
*/
class ClassLoader
{
- /** @var ?string */
+ /** @var \Closure(string):void */
+ private static $includeFile;
+
+ /** @var string|null */
private $vendorDir;
// PSR-4
/**
- * @var array[]
- * @psalm-var array<string, array<string, int>>
+ * @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
- * @var array[]
- * @psalm-var array<string, array<int, string>>
+ * @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
- * @var array[]
- * @psalm-var array<string, string>
+ * @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
- * @var array[]
- * @psalm-var array<string, array<string, string[]>>
+ * List of PSR-0 prefixes
+ *
+ * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
+ *
+ * @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
- * @var array[]
- * @psalm-var array<string, string>
+ * @var list<string>
*/
private $fallbackDirsPsr0 = array();
@@ -78,8 +80,7 @@ class ClassLoader
private $useIncludePath = false;
/**
- * @var string[]
- * @psalm-var array<string, string>
+ * @var array<string, string>
*/
private $classMap = array();
@@ -87,29 +88,29 @@ class ClassLoader
private $classMapAuthoritative = false;
/**
- * @var bool[]
- * @psalm-var array<string, bool>
+ * @var array<string, bool>
*/
private $missingClasses = array();
- /** @var ?string */
+ /** @var string|null */
private $apcuPrefix;
/**
- * @var self[]
+ * @var array<string, self>
*/
private static $registeredLoaders = array();
/**
- * @param ?string $vendorDir
+ * @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
+ self::initializeIncludeClosure();
}
/**
- * @return string[]
+ * @return array<string, list<string>>
*/
public function getPrefixes()
{
@@ -121,8 +122,7 @@ class ClassLoader
}
/**
- * @return array[]
- * @psalm-return array<string, array<int, string>>
+ * @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
@@ -130,8 +130,7 @@ class ClassLoader
}
/**
- * @return array[]
- * @psalm-return array<string, string>
+ * @return list<string>
*/
public function getFallbackDirs()
{
@@ -139,8 +138,7 @@ class ClassLoader
}
/**
- * @return array[]
- * @psalm-return array<string, string>
+ * @return list<string>
*/
public function getFallbackDirsPsr4()
{
@@ -148,8 +146,7 @@ class ClassLoader
}
/**
- * @return string[] Array of classname => path
- * @psalm-return array<string, string>
+ * @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
@@ -157,8 +154,7 @@ class ClassLoader
}
/**
- * @param string[] $classMap Class to filename map
- * @psalm-param array<string, string> $classMap
+ * @param array<string, string> $classMap Class to filename map
*
* @return void
*/
@@ -175,24 +171,25 @@ class ClassLoader
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
- * @param string $prefix The prefix
- * @param string[]|string $paths The PSR-0 root directories
- * @param bool $prepend Whether to prepend the directories
+ * @param string $prefix The prefix
+ * @param list<string>|string $paths The PSR-0 root directories
+ * @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
+ $paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
- (array) $paths,
+ $paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
- (array) $paths
+ $paths
);
}
@@ -201,19 +198,19 @@ class ClassLoader
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
- $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+ $this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
- (array) $paths,
+ $paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
- (array) $paths
+ $paths
);
}
}
@@ -222,9 +219,9 @@ class ClassLoader
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
- * @param string $prefix The prefix/namespace, with trailing '\\'
- * @param string[]|string $paths The PSR-4 base directories
- * @param bool $prepend Whether to prepend the directories
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param list<string>|string $paths The PSR-4 base directories
+ * @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
@@ -232,17 +229,18 @@ class ClassLoader
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
+ $paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
- (array) $paths,
+ $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
- (array) $paths
+ $paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
@@ -252,18 +250,18 @@ class ClassLoader
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
- $this->prefixDirsPsr4[$prefix] = (array) $paths;
+ $this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
- (array) $paths,
+ $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
- (array) $paths
+ $paths
);
}
}
@@ -272,8 +270,8 @@ class ClassLoader
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
- * @param string $prefix The prefix
- * @param string[]|string $paths The PSR-0 base directories
+ * @param string $prefix The prefix
+ * @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
@@ -290,8 +288,8 @@ class ClassLoader
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
- * @param string $prefix The prefix/namespace, with trailing '\\'
- * @param string[]|string $paths The PSR-4 base directories
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
@@ -425,7 +423,8 @@ class ClassLoader
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
- includeFile($file);
+ $includeFile = self::$includeFile;
+ $includeFile($file);
return true;
}
@@ -476,9 +475,9 @@ class ClassLoader
}
/**
- * Returns the currently registered loaders indexed by their corresponding vendor directories.
+ * Returns the currently registered loaders keyed by their corresponding vendor directories.
*
- * @return self[]
+ * @return array<string, self>
*/
public static function getRegisteredLoaders()
{
@@ -555,18 +554,26 @@ class ClassLoader
return false;
}
-}
-/**
- * Scope isolated include.
- *
- * Prevents access to $this/self from included files.
- *
- * @param string $file
- * @return void
- * @private
- */
-function includeFile($file)
-{
- include $file;
+ /**
+ * @return void
+ */
+ private static function initializeIncludeClosure()
+ {
+ if (self::$includeFile !== null) {
+ return;
+ }
+
+ /**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ *
+ * @param string $file
+ * @return void
+ */
+ self::$includeFile = \Closure::bind(static function($file) {
+ include $file;
+ }, null, null);
+ }
}
diff --git a/apps/dav/composer/composer/InstalledVersions.php b/apps/dav/composer/composer/InstalledVersions.php
index 41bc143c114..51e734a774b 100644
--- a/apps/dav/composer/composer/InstalledVersions.php
+++ b/apps/dav/composer/composer/InstalledVersions.php
@@ -28,7 +28,7 @@ class InstalledVersions
{
/**
* @var mixed[]|null
- * @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}|array{}|null
+ * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
@@ -39,7 +39,7 @@ class InstalledVersions
/**
* @var array[]
- * @psalm-var array<string, array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
+ * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
@@ -98,7 +98,7 @@ class InstalledVersions
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
- return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
+ return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
@@ -119,7 +119,7 @@ class InstalledVersions
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
- $constraint = $parser->parseConstraints($constraint);
+ $constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
@@ -243,7 +243,7 @@ class InstalledVersions
/**
* @return array
- * @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}
+ * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
@@ -257,7 +257,7 @@ class InstalledVersions
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
- * @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}
+ * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@@ -280,7 +280,7 @@ class InstalledVersions
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
- * @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
+ * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
@@ -303,7 +303,7 @@ class InstalledVersions
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
- * @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>} $data
+ * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
@@ -313,7 +313,7 @@ class InstalledVersions
/**
* @return array[]
- * @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
+ * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
@@ -328,7 +328,9 @@ class InstalledVersions
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
- $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
+ /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+ $required = require $vendorDir.'/composer/installed.php';
+ $installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
@@ -340,12 +342,17 @@ class InstalledVersions
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
- self::$installed = require __DIR__ . '/installed.php';
+ /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+ $required = require __DIR__ . '/installed.php';
+ self::$installed = $required;
} else {
self::$installed = array();
}
}
- $installed[] = self::$installed;
+
+ if (self::$installed !== array()) {
+ $installed[] = self::$installed;
+ }
return $installed;
}
diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php
index 5f7815a9bfc..b9708ea5589 100644
--- a/apps/dav/composer/composer/autoload_classmap.php
+++ b/apps/dav/composer/composer/autoload_classmap.php
@@ -16,12 +16,17 @@ return array(
'OCA\\DAV\\BackgroundJob\\CalendarRetentionJob' => $baseDir . '/../lib/BackgroundJob/CalendarRetentionJob.php',
'OCA\\DAV\\BackgroundJob\\CleanupDirectLinksJob' => $baseDir . '/../lib/BackgroundJob/CleanupDirectLinksJob.php',
'OCA\\DAV\\BackgroundJob\\CleanupInvitationTokenJob' => $baseDir . '/../lib/BackgroundJob/CleanupInvitationTokenJob.php',
+ 'OCA\\DAV\\BackgroundJob\\CleanupOrphanedChildrenJob' => $baseDir . '/../lib/BackgroundJob/CleanupOrphanedChildrenJob.php',
+ 'OCA\\DAV\\BackgroundJob\\DeleteOutdatedSchedulingObjects' => $baseDir . '/../lib/BackgroundJob/DeleteOutdatedSchedulingObjects.php',
'OCA\\DAV\\BackgroundJob\\EventReminderJob' => $baseDir . '/../lib/BackgroundJob/EventReminderJob.php',
'OCA\\DAV\\BackgroundJob\\GenerateBirthdayCalendarBackgroundJob' => $baseDir . '/../lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php',
+ 'OCA\\DAV\\BackgroundJob\\OutOfOfficeEventDispatcherJob' => $baseDir . '/../lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php',
+ 'OCA\\DAV\\BackgroundJob\\PruneOutdatedSyncTokensJob' => $baseDir . '/../lib/BackgroundJob/PruneOutdatedSyncTokensJob.php',
'OCA\\DAV\\BackgroundJob\\RefreshWebcalJob' => $baseDir . '/../lib/BackgroundJob/RefreshWebcalJob.php',
'OCA\\DAV\\BackgroundJob\\RegisterRegenerateBirthdayCalendars' => $baseDir . '/../lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php',
'OCA\\DAV\\BackgroundJob\\UpdateCalendarResourcesRoomsBackgroundJob' => $baseDir . '/../lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php',
'OCA\\DAV\\BackgroundJob\\UploadCleanup' => $baseDir . '/../lib/BackgroundJob/UploadCleanup.php',
+ 'OCA\\DAV\\BackgroundJob\\UserStatusAutomation' => $baseDir . '/../lib/BackgroundJob/UserStatusAutomation.php',
'OCA\\DAV\\BulkUpload\\BulkUploadPlugin' => $baseDir . '/../lib/BulkUpload/BulkUploadPlugin.php',
'OCA\\DAV\\BulkUpload\\MultipartRequestParser' => $baseDir . '/../lib/BulkUpload/MultipartRequestParser.php',
'OCA\\DAV\\CalDAV\\Activity\\Backend' => $baseDir . '/../lib/CalDAV/Activity/Backend.php',
@@ -35,12 +40,17 @@ return array(
'OCA\\DAV\\CalDAV\\Activity\\Setting\\Calendar' => $baseDir . '/../lib/CalDAV/Activity/Setting/Calendar.php',
'OCA\\DAV\\CalDAV\\Activity\\Setting\\Event' => $baseDir . '/../lib/CalDAV/Activity/Setting/Event.php',
'OCA\\DAV\\CalDAV\\Activity\\Setting\\Todo' => $baseDir . '/../lib/CalDAV/Activity/Setting/Todo.php',
+ 'OCA\\DAV\\CalDAV\\AppCalendar\\AppCalendar' => $baseDir . '/../lib/CalDAV/AppCalendar/AppCalendar.php',
+ 'OCA\\DAV\\CalDAV\\AppCalendar\\AppCalendarPlugin' => $baseDir . '/../lib/CalDAV/AppCalendar/AppCalendarPlugin.php',
+ 'OCA\\DAV\\CalDAV\\AppCalendar\\CalendarObject' => $baseDir . '/../lib/CalDAV/AppCalendar/CalendarObject.php',
'OCA\\DAV\\CalDAV\\Auth\\CustomPrincipalPlugin' => $baseDir . '/../lib/CalDAV/Auth/CustomPrincipalPlugin.php',
'OCA\\DAV\\CalDAV\\Auth\\PublicPrincipalPlugin' => $baseDir . '/../lib/CalDAV/Auth/PublicPrincipalPlugin.php',
'OCA\\DAV\\CalDAV\\BirthdayCalendar\\EnablePlugin' => $baseDir . '/../lib/CalDAV/BirthdayCalendar/EnablePlugin.php',
'OCA\\DAV\\CalDAV\\BirthdayService' => $baseDir . '/../lib/CalDAV/BirthdayService.php',
'OCA\\DAV\\CalDAV\\CachedSubscription' => $baseDir . '/../lib/CalDAV/CachedSubscription.php',
+ 'OCA\\DAV\\CalDAV\\CachedSubscriptionImpl' => $baseDir . '/../lib/CalDAV/CachedSubscriptionImpl.php',
'OCA\\DAV\\CalDAV\\CachedSubscriptionObject' => $baseDir . '/../lib/CalDAV/CachedSubscriptionObject.php',
+ 'OCA\\DAV\\CalDAV\\CachedSubscriptionProvider' => $baseDir . '/../lib/CalDAV/CachedSubscriptionProvider.php',
'OCA\\DAV\\CalDAV\\CalDavBackend' => $baseDir . '/../lib/CalDAV/CalDavBackend.php',
'OCA\\DAV\\CalDAV\\Calendar' => $baseDir . '/../lib/CalDAV/Calendar.php',
'OCA\\DAV\\CalDAV\\CalendarHome' => $baseDir . '/../lib/CalDAV/CalendarHome.php',
@@ -49,6 +59,14 @@ return array(
'OCA\\DAV\\CalDAV\\CalendarObject' => $baseDir . '/../lib/CalDAV/CalendarObject.php',
'OCA\\DAV\\CalDAV\\CalendarProvider' => $baseDir . '/../lib/CalDAV/CalendarProvider.php',
'OCA\\DAV\\CalDAV\\CalendarRoot' => $baseDir . '/../lib/CalDAV/CalendarRoot.php',
+ 'OCA\\DAV\\CalDAV\\DefaultCalendarValidator' => $baseDir . '/../lib/CalDAV/DefaultCalendarValidator.php',
+ 'OCA\\DAV\\CalDAV\\EmbeddedCalDavServer' => $baseDir . '/../lib/CalDAV/EmbeddedCalDavServer.php',
+ 'OCA\\DAV\\CalDAV\\EventComparisonService' => $baseDir . '/../lib/CalDAV/EventComparisonService.php',
+ 'OCA\\DAV\\CalDAV\\EventReader' => $baseDir . '/../lib/CalDAV/EventReader.php',
+ 'OCA\\DAV\\CalDAV\\EventReaderRDate' => $baseDir . '/../lib/CalDAV/EventReaderRDate.php',
+ 'OCA\\DAV\\CalDAV\\EventReaderRRule' => $baseDir . '/../lib/CalDAV/EventReaderRRule.php',
+ 'OCA\\DAV\\CalDAV\\Export\\ExportService' => $baseDir . '/../lib/CalDAV/Export/ExportService.php',
+ 'OCA\\DAV\\CalDAV\\FreeBusy\\FreeBusyGenerator' => $baseDir . '/../lib/CalDAV/FreeBusy/FreeBusyGenerator.php',
'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => $baseDir . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php',
'OCA\\DAV\\CalDAV\\IRestorable' => $baseDir . '/../lib/CalDAV/IRestorable.php',
'OCA\\DAV\\CalDAV\\Integration\\ExternalCalendar' => $baseDir . '/../lib/CalDAV/Integration/ExternalCalendar.php',
@@ -81,6 +99,7 @@ return array(
'OCA\\DAV\\CalDAV\\ResourceBooking\\RoomPrincipalBackend' => $baseDir . '/../lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php',
'OCA\\DAV\\CalDAV\\RetentionService' => $baseDir . '/../lib/CalDAV/RetentionService.php',
'OCA\\DAV\\CalDAV\\Schedule\\IMipPlugin' => $baseDir . '/../lib/CalDAV/Schedule/IMipPlugin.php',
+ 'OCA\\DAV\\CalDAV\\Schedule\\IMipService' => $baseDir . '/../lib/CalDAV/Schedule/IMipService.php',
'OCA\\DAV\\CalDAV\\Schedule\\Plugin' => $baseDir . '/../lib/CalDAV/Schedule/Plugin.php',
'OCA\\DAV\\CalDAV\\Search\\SearchPlugin' => $baseDir . '/../lib/CalDAV/Search/SearchPlugin.php',
'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\CompFilter' => $baseDir . '/../lib/CalDAV/Search/Xml/Filter/CompFilter.php',
@@ -90,11 +109,22 @@ return array(
'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\PropFilter' => $baseDir . '/../lib/CalDAV/Search/Xml/Filter/PropFilter.php',
'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\SearchTermFilter' => $baseDir . '/../lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php',
'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport' => $baseDir . '/../lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php',
+ 'OCA\\DAV\\CalDAV\\Security\\RateLimitingPlugin' => $baseDir . '/../lib/CalDAV/Security/RateLimitingPlugin.php',
+ 'OCA\\DAV\\CalDAV\\Sharing\\Backend' => $baseDir . '/../lib/CalDAV/Sharing/Backend.php',
+ 'OCA\\DAV\\CalDAV\\Sharing\\Service' => $baseDir . '/../lib/CalDAV/Sharing/Service.php',
+ 'OCA\\DAV\\CalDAV\\Status\\StatusService' => $baseDir . '/../lib/CalDAV/Status/StatusService.php',
+ 'OCA\\DAV\\CalDAV\\TimeZoneFactory' => $baseDir . '/../lib/CalDAV/TimeZoneFactory.php',
+ 'OCA\\DAV\\CalDAV\\TimezoneService' => $baseDir . '/../lib/CalDAV/TimezoneService.php',
+ 'OCA\\DAV\\CalDAV\\TipBroker' => $baseDir . '/../lib/CalDAV/TipBroker.php',
'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObject' => $baseDir . '/../lib/CalDAV/Trashbin/DeletedCalendarObject.php',
'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObjectsCollection' => $baseDir . '/../lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php',
'OCA\\DAV\\CalDAV\\Trashbin\\Plugin' => $baseDir . '/../lib/CalDAV/Trashbin/Plugin.php',
'OCA\\DAV\\CalDAV\\Trashbin\\RestoreTarget' => $baseDir . '/../lib/CalDAV/Trashbin/RestoreTarget.php',
'OCA\\DAV\\CalDAV\\Trashbin\\TrashbinHome' => $baseDir . '/../lib/CalDAV/Trashbin/TrashbinHome.php',
+ 'OCA\\DAV\\CalDAV\\UpcomingEvent' => $baseDir . '/../lib/CalDAV/UpcomingEvent.php',
+ 'OCA\\DAV\\CalDAV\\UpcomingEventsService' => $baseDir . '/../lib/CalDAV/UpcomingEventsService.php',
+ 'OCA\\DAV\\CalDAV\\Validation\\CalDavValidatePlugin' => $baseDir . '/../lib/CalDAV/Validation/CalDavValidatePlugin.php',
+ 'OCA\\DAV\\CalDAV\\WebcalCaching\\Connection' => $baseDir . '/../lib/CalDAV/WebcalCaching/Connection.php',
'OCA\\DAV\\CalDAV\\WebcalCaching\\Plugin' => $baseDir . '/../lib/CalDAV/WebcalCaching/Plugin.php',
'OCA\\DAV\\CalDAV\\WebcalCaching\\RefreshWebcalService' => $baseDir . '/../lib/CalDAV/WebcalCaching/RefreshWebcalService.php',
'OCA\\DAV\\Capabilities' => $baseDir . '/../lib/Capabilities.php',
@@ -107,6 +137,7 @@ return array(
'OCA\\DAV\\CardDAV\\AddressBook' => $baseDir . '/../lib/CardDAV/AddressBook.php',
'OCA\\DAV\\CardDAV\\AddressBookImpl' => $baseDir . '/../lib/CardDAV/AddressBookImpl.php',
'OCA\\DAV\\CardDAV\\AddressBookRoot' => $baseDir . '/../lib/CardDAV/AddressBookRoot.php',
+ 'OCA\\DAV\\CardDAV\\Card' => $baseDir . '/../lib/CardDAV/Card.php',
'OCA\\DAV\\CardDAV\\CardDavBackend' => $baseDir . '/../lib/CardDAV/CardDavBackend.php',
'OCA\\DAV\\CardDAV\\ContactsManager' => $baseDir . '/../lib/CardDAV/ContactsManager.php',
'OCA\\DAV\\CardDAV\\Converter' => $baseDir . '/../lib/CardDAV/Converter.php',
@@ -117,18 +148,33 @@ return array(
'OCA\\DAV\\CardDAV\\MultiGetExportPlugin' => $baseDir . '/../lib/CardDAV/MultiGetExportPlugin.php',
'OCA\\DAV\\CardDAV\\PhotoCache' => $baseDir . '/../lib/CardDAV/PhotoCache.php',
'OCA\\DAV\\CardDAV\\Plugin' => $baseDir . '/../lib/CardDAV/Plugin.php',
+ 'OCA\\DAV\\CardDAV\\Security\\CardDavRateLimitingPlugin' => $baseDir . '/../lib/CardDAV/Security/CardDavRateLimitingPlugin.php',
+ 'OCA\\DAV\\CardDAV\\Sharing\\Backend' => $baseDir . '/../lib/CardDAV/Sharing/Backend.php',
+ 'OCA\\DAV\\CardDAV\\Sharing\\Service' => $baseDir . '/../lib/CardDAV/Sharing/Service.php',
'OCA\\DAV\\CardDAV\\SyncService' => $baseDir . '/../lib/CardDAV/SyncService.php',
'OCA\\DAV\\CardDAV\\SystemAddressbook' => $baseDir . '/../lib/CardDAV/SystemAddressbook.php',
'OCA\\DAV\\CardDAV\\UserAddressBooks' => $baseDir . '/../lib/CardDAV/UserAddressBooks.php',
+ 'OCA\\DAV\\CardDAV\\Validation\\CardDavValidatePlugin' => $baseDir . '/../lib/CardDAV/Validation/CardDavValidatePlugin.php',
'OCA\\DAV\\CardDAV\\Xml\\Groups' => $baseDir . '/../lib/CardDAV/Xml/Groups.php',
+ 'OCA\\DAV\\Command\\ClearCalendarUnshares' => $baseDir . '/../lib/Command/ClearCalendarUnshares.php',
+ 'OCA\\DAV\\Command\\ClearContactsPhotoCache' => $baseDir . '/../lib/Command/ClearContactsPhotoCache.php',
'OCA\\DAV\\Command\\CreateAddressBook' => $baseDir . '/../lib/Command/CreateAddressBook.php',
'OCA\\DAV\\Command\\CreateCalendar' => $baseDir . '/../lib/Command/CreateCalendar.php',
+ 'OCA\\DAV\\Command\\CreateSubscription' => $baseDir . '/../lib/Command/CreateSubscription.php',
'OCA\\DAV\\Command\\DeleteCalendar' => $baseDir . '/../lib/Command/DeleteCalendar.php',
+ 'OCA\\DAV\\Command\\DeleteSubscription' => $baseDir . '/../lib/Command/DeleteSubscription.php',
+ 'OCA\\DAV\\Command\\ExportCalendar' => $baseDir . '/../lib/Command/ExportCalendar.php',
+ 'OCA\\DAV\\Command\\FixCalendarSyncCommand' => $baseDir . '/../lib/Command/FixCalendarSyncCommand.php',
+ 'OCA\\DAV\\Command\\GetAbsenceCommand' => $baseDir . '/../lib/Command/GetAbsenceCommand.php',
+ 'OCA\\DAV\\Command\\ListAddressbooks' => $baseDir . '/../lib/Command/ListAddressbooks.php',
+ 'OCA\\DAV\\Command\\ListCalendarShares' => $baseDir . '/../lib/Command/ListCalendarShares.php',
'OCA\\DAV\\Command\\ListCalendars' => $baseDir . '/../lib/Command/ListCalendars.php',
+ 'OCA\\DAV\\Command\\ListSubscriptions' => $baseDir . '/../lib/Command/ListSubscriptions.php',
'OCA\\DAV\\Command\\MoveCalendar' => $baseDir . '/../lib/Command/MoveCalendar.php',
'OCA\\DAV\\Command\\RemoveInvalidShares' => $baseDir . '/../lib/Command/RemoveInvalidShares.php',
'OCA\\DAV\\Command\\RetentionCleanupCommand' => $baseDir . '/../lib/Command/RetentionCleanupCommand.php',
'OCA\\DAV\\Command\\SendEventReminders' => $baseDir . '/../lib/Command/SendEventReminders.php',
+ 'OCA\\DAV\\Command\\SetAbsenceCommand' => $baseDir . '/../lib/Command/SetAbsenceCommand.php',
'OCA\\DAV\\Command\\SyncBirthdayCalendar' => $baseDir . '/../lib/Command/SyncBirthdayCalendar.php',
'OCA\\DAV\\Command\\SyncSystemAddressBook' => $baseDir . '/../lib/Command/SyncSystemAddressBook.php',
'OCA\\DAV\\Comments\\CommentNode' => $baseDir . '/../lib/Comments/CommentNode.php',
@@ -137,9 +183,9 @@ return array(
'OCA\\DAV\\Comments\\EntityTypeCollection' => $baseDir . '/../lib/Comments/EntityTypeCollection.php',
'OCA\\DAV\\Comments\\RootCollection' => $baseDir . '/../lib/Comments/RootCollection.php',
'OCA\\DAV\\Connector\\LegacyDAVACL' => $baseDir . '/../lib/Connector/LegacyDAVACL.php',
- 'OCA\\DAV\\Connector\\PublicAuth' => $baseDir . '/../lib/Connector/PublicAuth.php',
+ 'OCA\\DAV\\Connector\\LegacyPublicAuth' => $baseDir . '/../lib/Connector/LegacyPublicAuth.php',
'OCA\\DAV\\Connector\\Sabre\\AnonymousOptionsPlugin' => $baseDir . '/../lib/Connector/Sabre/AnonymousOptionsPlugin.php',
- 'OCA\\DAV\\Connector\\Sabre\\AppEnabledPlugin' => $baseDir . '/../lib/Connector/Sabre/AppEnabledPlugin.php',
+ 'OCA\\DAV\\Connector\\Sabre\\AppleQuirksPlugin' => $baseDir . '/../lib/Connector/Sabre/AppleQuirksPlugin.php',
'OCA\\DAV\\Connector\\Sabre\\Auth' => $baseDir . '/../lib/Connector/Sabre/Auth.php',
'OCA\\DAV\\Connector\\Sabre\\BearerAuth' => $baseDir . '/../lib/Connector/Sabre/BearerAuth.php',
'OCA\\DAV\\Connector\\Sabre\\BlockLegacyClientPlugin' => $baseDir . '/../lib/Connector/Sabre/BlockLegacyClientPlugin.php',
@@ -158,6 +204,7 @@ return array(
'OCA\\DAV\\Connector\\Sabre\\Exception\\Forbidden' => $baseDir . '/../lib/Connector/Sabre/Exception/Forbidden.php',
'OCA\\DAV\\Connector\\Sabre\\Exception\\InvalidPath' => $baseDir . '/../lib/Connector/Sabre/Exception/InvalidPath.php',
'OCA\\DAV\\Connector\\Sabre\\Exception\\PasswordLoginForbidden' => $baseDir . '/../lib/Connector/Sabre/Exception/PasswordLoginForbidden.php',
+ 'OCA\\DAV\\Connector\\Sabre\\Exception\\TooManyRequests' => $baseDir . '/../lib/Connector/Sabre/Exception/TooManyRequests.php',
'OCA\\DAV\\Connector\\Sabre\\Exception\\UnsupportedMediaType' => $baseDir . '/../lib/Connector/Sabre/Exception/UnsupportedMediaType.php',
'OCA\\DAV\\Connector\\Sabre\\FakeLockerPlugin' => $baseDir . '/../lib/Connector/Sabre/FakeLockerPlugin.php',
'OCA\\DAV\\Connector\\Sabre\\File' => $baseDir . '/../lib/Connector/Sabre/File.php',
@@ -169,7 +216,9 @@ return array(
'OCA\\DAV\\Connector\\Sabre\\Node' => $baseDir . '/../lib/Connector/Sabre/Node.php',
'OCA\\DAV\\Connector\\Sabre\\ObjectTree' => $baseDir . '/../lib/Connector/Sabre/ObjectTree.php',
'OCA\\DAV\\Connector\\Sabre\\Principal' => $baseDir . '/../lib/Connector/Sabre/Principal.php',
+ 'OCA\\DAV\\Connector\\Sabre\\PropFindMonitorPlugin' => $baseDir . '/../lib/Connector/Sabre/PropFindMonitorPlugin.php',
'OCA\\DAV\\Connector\\Sabre\\PropfindCompressionPlugin' => $baseDir . '/../lib/Connector/Sabre/PropfindCompressionPlugin.php',
+ 'OCA\\DAV\\Connector\\Sabre\\PublicAuth' => $baseDir . '/../lib/Connector/Sabre/PublicAuth.php',
'OCA\\DAV\\Connector\\Sabre\\QuotaPlugin' => $baseDir . '/../lib/Connector/Sabre/QuotaPlugin.php',
'OCA\\DAV\\Connector\\Sabre\\RequestIdHeaderPlugin' => $baseDir . '/../lib/Connector/Sabre/RequestIdHeaderPlugin.php',
'OCA\\DAV\\Connector\\Sabre\\Server' => $baseDir . '/../lib/Connector/Sabre/Server.php',
@@ -179,20 +228,31 @@ return array(
'OCA\\DAV\\Connector\\Sabre\\SharesPlugin' => $baseDir . '/../lib/Connector/Sabre/SharesPlugin.php',
'OCA\\DAV\\Connector\\Sabre\\TagList' => $baseDir . '/../lib/Connector/Sabre/TagList.php',
'OCA\\DAV\\Connector\\Sabre\\TagsPlugin' => $baseDir . '/../lib/Connector/Sabre/TagsPlugin.php',
+ 'OCA\\DAV\\Connector\\Sabre\\ZipFolderPlugin' => $baseDir . '/../lib/Connector/Sabre/ZipFolderPlugin.php',
'OCA\\DAV\\Controller\\BirthdayCalendarController' => $baseDir . '/../lib/Controller/BirthdayCalendarController.php',
'OCA\\DAV\\Controller\\DirectController' => $baseDir . '/../lib/Controller/DirectController.php',
+ 'OCA\\DAV\\Controller\\ExampleContentController' => $baseDir . '/../lib/Controller/ExampleContentController.php',
'OCA\\DAV\\Controller\\InvitationResponseController' => $baseDir . '/../lib/Controller/InvitationResponseController.php',
+ 'OCA\\DAV\\Controller\\OutOfOfficeController' => $baseDir . '/../lib/Controller/OutOfOfficeController.php',
+ 'OCA\\DAV\\Controller\\UpcomingEventsController' => $baseDir . '/../lib/Controller/UpcomingEventsController.php',
'OCA\\DAV\\DAV\\CustomPropertiesBackend' => $baseDir . '/../lib/DAV/CustomPropertiesBackend.php',
'OCA\\DAV\\DAV\\GroupPrincipalBackend' => $baseDir . '/../lib/DAV/GroupPrincipalBackend.php',
'OCA\\DAV\\DAV\\PublicAuth' => $baseDir . '/../lib/DAV/PublicAuth.php',
'OCA\\DAV\\DAV\\Sharing\\Backend' => $baseDir . '/../lib/DAV/Sharing/Backend.php',
'OCA\\DAV\\DAV\\Sharing\\IShareable' => $baseDir . '/../lib/DAV/Sharing/IShareable.php',
'OCA\\DAV\\DAV\\Sharing\\Plugin' => $baseDir . '/../lib/DAV/Sharing/Plugin.php',
+ 'OCA\\DAV\\DAV\\Sharing\\SharingMapper' => $baseDir . '/../lib/DAV/Sharing/SharingMapper.php',
+ 'OCA\\DAV\\DAV\\Sharing\\SharingService' => $baseDir . '/../lib/DAV/Sharing/SharingService.php',
'OCA\\DAV\\DAV\\Sharing\\Xml\\Invite' => $baseDir . '/../lib/DAV/Sharing/Xml/Invite.php',
'OCA\\DAV\\DAV\\Sharing\\Xml\\ShareRequest' => $baseDir . '/../lib/DAV/Sharing/Xml/ShareRequest.php',
'OCA\\DAV\\DAV\\SystemPrincipalBackend' => $baseDir . '/../lib/DAV/SystemPrincipalBackend.php',
+ 'OCA\\DAV\\DAV\\ViewOnlyPlugin' => $baseDir . '/../lib/DAV/ViewOnlyPlugin.php',
+ 'OCA\\DAV\\Db\\Absence' => $baseDir . '/../lib/Db/Absence.php',
+ 'OCA\\DAV\\Db\\AbsenceMapper' => $baseDir . '/../lib/Db/AbsenceMapper.php',
'OCA\\DAV\\Db\\Direct' => $baseDir . '/../lib/Db/Direct.php',
'OCA\\DAV\\Db\\DirectMapper' => $baseDir . '/../lib/Db/DirectMapper.php',
+ 'OCA\\DAV\\Db\\Property' => $baseDir . '/../lib/Db/Property.php',
+ 'OCA\\DAV\\Db\\PropertyMapper' => $baseDir . '/../lib/Db/PropertyMapper.php',
'OCA\\DAV\\Direct\\DirectFile' => $baseDir . '/../lib/Direct/DirectFile.php',
'OCA\\DAV\\Direct\\DirectHome' => $baseDir . '/../lib/Direct/DirectHome.php',
'OCA\\DAV\\Direct\\Server' => $baseDir . '/../lib/Direct/Server.php',
@@ -208,11 +268,6 @@ return array(
'OCA\\DAV\\Events\\CalendarCreatedEvent' => $baseDir . '/../lib/Events/CalendarCreatedEvent.php',
'OCA\\DAV\\Events\\CalendarDeletedEvent' => $baseDir . '/../lib/Events/CalendarDeletedEvent.php',
'OCA\\DAV\\Events\\CalendarMovedToTrashEvent' => $baseDir . '/../lib/Events/CalendarMovedToTrashEvent.php',
- 'OCA\\DAV\\Events\\CalendarObjectCreatedEvent' => $baseDir . '/../lib/Events/CalendarObjectCreatedEvent.php',
- 'OCA\\DAV\\Events\\CalendarObjectDeletedEvent' => $baseDir . '/../lib/Events/CalendarObjectDeletedEvent.php',
- 'OCA\\DAV\\Events\\CalendarObjectMovedToTrashEvent' => $baseDir . '/../lib/Events/CalendarObjectMovedToTrashEvent.php',
- 'OCA\\DAV\\Events\\CalendarObjectRestoredEvent' => $baseDir . '/../lib/Events/CalendarObjectRestoredEvent.php',
- 'OCA\\DAV\\Events\\CalendarObjectUpdatedEvent' => $baseDir . '/../lib/Events/CalendarObjectUpdatedEvent.php',
'OCA\\DAV\\Events\\CalendarPublishedEvent' => $baseDir . '/../lib/Events/CalendarPublishedEvent.php',
'OCA\\DAV\\Events\\CalendarRestoredEvent' => $baseDir . '/../lib/Events/CalendarRestoredEvent.php',
'OCA\\DAV\\Events\\CalendarShareUpdatedEvent' => $baseDir . '/../lib/Events/CalendarShareUpdatedEvent.php',
@@ -220,11 +275,15 @@ return array(
'OCA\\DAV\\Events\\CalendarUpdatedEvent' => $baseDir . '/../lib/Events/CalendarUpdatedEvent.php',
'OCA\\DAV\\Events\\CardCreatedEvent' => $baseDir . '/../lib/Events/CardCreatedEvent.php',
'OCA\\DAV\\Events\\CardDeletedEvent' => $baseDir . '/../lib/Events/CardDeletedEvent.php',
+ 'OCA\\DAV\\Events\\CardMovedEvent' => $baseDir . '/../lib/Events/CardMovedEvent.php',
'OCA\\DAV\\Events\\CardUpdatedEvent' => $baseDir . '/../lib/Events/CardUpdatedEvent.php',
+ 'OCA\\DAV\\Events\\SabrePluginAddEvent' => $baseDir . '/../lib/Events/SabrePluginAddEvent.php',
'OCA\\DAV\\Events\\SabrePluginAuthInitEvent' => $baseDir . '/../lib/Events/SabrePluginAuthInitEvent.php',
'OCA\\DAV\\Events\\SubscriptionCreatedEvent' => $baseDir . '/../lib/Events/SubscriptionCreatedEvent.php',
'OCA\\DAV\\Events\\SubscriptionDeletedEvent' => $baseDir . '/../lib/Events/SubscriptionDeletedEvent.php',
'OCA\\DAV\\Events\\SubscriptionUpdatedEvent' => $baseDir . '/../lib/Events/SubscriptionUpdatedEvent.php',
+ 'OCA\\DAV\\Exception\\ExampleEventException' => $baseDir . '/../lib/Exception/ExampleEventException.php',
+ 'OCA\\DAV\\Exception\\ServerMaintenanceMode' => $baseDir . '/../lib/Exception/ServerMaintenanceMode.php',
'OCA\\DAV\\Exception\\UnsupportedLimitOnInitialSyncException' => $baseDir . '/../lib/Exception/UnsupportedLimitOnInitialSyncException.php',
'OCA\\DAV\\Files\\BrowserErrorPagePlugin' => $baseDir . '/../lib/Files/BrowserErrorPagePlugin.php',
'OCA\\DAV\\Files\\FileSearchBackend' => $baseDir . '/../lib/Files/FileSearchBackend.php',
@@ -233,25 +292,40 @@ return array(
'OCA\\DAV\\Files\\RootCollection' => $baseDir . '/../lib/Files/RootCollection.php',
'OCA\\DAV\\Files\\Sharing\\FilesDropPlugin' => $baseDir . '/../lib/Files/Sharing/FilesDropPlugin.php',
'OCA\\DAV\\Files\\Sharing\\PublicLinkCheckPlugin' => $baseDir . '/../lib/Files/Sharing/PublicLinkCheckPlugin.php',
- 'OCA\\DAV\\HookManager' => $baseDir . '/../lib/HookManager.php',
+ 'OCA\\DAV\\Files\\Sharing\\RootCollection' => $baseDir . '/../lib/Files/Sharing/RootCollection.php',
'OCA\\DAV\\Listener\\ActivityUpdaterListener' => $baseDir . '/../lib/Listener/ActivityUpdaterListener.php',
+ 'OCA\\DAV\\Listener\\AddMissingIndicesListener' => $baseDir . '/../lib/Listener/AddMissingIndicesListener.php',
'OCA\\DAV\\Listener\\AddressbookListener' => $baseDir . '/../lib/Listener/AddressbookListener.php',
+ 'OCA\\DAV\\Listener\\BirthdayListener' => $baseDir . '/../lib/Listener/BirthdayListener.php',
'OCA\\DAV\\Listener\\CalendarContactInteractionListener' => $baseDir . '/../lib/Listener/CalendarContactInteractionListener.php',
'OCA\\DAV\\Listener\\CalendarDeletionDefaultUpdaterListener' => $baseDir . '/../lib/Listener/CalendarDeletionDefaultUpdaterListener.php',
'OCA\\DAV\\Listener\\CalendarObjectReminderUpdaterListener' => $baseDir . '/../lib/Listener/CalendarObjectReminderUpdaterListener.php',
+ 'OCA\\DAV\\Listener\\CalendarPublicationListener' => $baseDir . '/../lib/Listener/CalendarPublicationListener.php',
+ 'OCA\\DAV\\Listener\\CalendarShareUpdateListener' => $baseDir . '/../lib/Listener/CalendarShareUpdateListener.php',
'OCA\\DAV\\Listener\\CardListener' => $baseDir . '/../lib/Listener/CardListener.php',
+ 'OCA\\DAV\\Listener\\ClearPhotoCacheListener' => $baseDir . '/../lib/Listener/ClearPhotoCacheListener.php',
+ 'OCA\\DAV\\Listener\\DavAdminSettingsListener' => $baseDir . '/../lib/Listener/DavAdminSettingsListener.php',
+ 'OCA\\DAV\\Listener\\OutOfOfficeListener' => $baseDir . '/../lib/Listener/OutOfOfficeListener.php',
+ 'OCA\\DAV\\Listener\\SubscriptionListener' => $baseDir . '/../lib/Listener/SubscriptionListener.php',
+ 'OCA\\DAV\\Listener\\TrustedServerRemovedListener' => $baseDir . '/../lib/Listener/TrustedServerRemovedListener.php',
+ 'OCA\\DAV\\Listener\\UserEventsListener' => $baseDir . '/../lib/Listener/UserEventsListener.php',
+ 'OCA\\DAV\\Listener\\UserPreferenceListener' => $baseDir . '/../lib/Listener/UserPreferenceListener.php',
'OCA\\DAV\\Migration\\BuildCalendarSearchIndex' => $baseDir . '/../lib/Migration/BuildCalendarSearchIndex.php',
'OCA\\DAV\\Migration\\BuildCalendarSearchIndexBackgroundJob' => $baseDir . '/../lib/Migration/BuildCalendarSearchIndexBackgroundJob.php',
'OCA\\DAV\\Migration\\BuildSocialSearchIndex' => $baseDir . '/../lib/Migration/BuildSocialSearchIndex.php',
'OCA\\DAV\\Migration\\BuildSocialSearchIndexBackgroundJob' => $baseDir . '/../lib/Migration/BuildSocialSearchIndexBackgroundJob.php',
'OCA\\DAV\\Migration\\CalDAVRemoveEmptyValue' => $baseDir . '/../lib/Migration/CalDAVRemoveEmptyValue.php',
'OCA\\DAV\\Migration\\ChunkCleanup' => $baseDir . '/../lib/Migration/ChunkCleanup.php',
+ 'OCA\\DAV\\Migration\\CreateSystemAddressBookStep' => $baseDir . '/../lib/Migration/CreateSystemAddressBookStep.php',
+ 'OCA\\DAV\\Migration\\DeleteSchedulingObjects' => $baseDir . '/../lib/Migration/DeleteSchedulingObjects.php',
'OCA\\DAV\\Migration\\FixBirthdayCalendarComponent' => $baseDir . '/../lib/Migration/FixBirthdayCalendarComponent.php',
'OCA\\DAV\\Migration\\RefreshWebcalJobRegistrar' => $baseDir . '/../lib/Migration/RefreshWebcalJobRegistrar.php',
'OCA\\DAV\\Migration\\RegenerateBirthdayCalendars' => $baseDir . '/../lib/Migration/RegenerateBirthdayCalendars.php',
'OCA\\DAV\\Migration\\RegisterBuildReminderIndexBackgroundJob' => $baseDir . '/../lib/Migration/RegisterBuildReminderIndexBackgroundJob.php',
+ 'OCA\\DAV\\Migration\\RegisterUpdateCalendarResourcesRoomBackgroundJob' => $baseDir . '/../lib/Migration/RegisterUpdateCalendarResourcesRoomBackgroundJob.php',
'OCA\\DAV\\Migration\\RemoveClassifiedEventActivity' => $baseDir . '/../lib/Migration/RemoveClassifiedEventActivity.php',
'OCA\\DAV\\Migration\\RemoveDeletedUsersCalendarSubscriptions' => $baseDir . '/../lib/Migration/RemoveDeletedUsersCalendarSubscriptions.php',
+ 'OCA\\DAV\\Migration\\RemoveObjectProperties' => $baseDir . '/../lib/Migration/RemoveObjectProperties.php',
'OCA\\DAV\\Migration\\RemoveOrphanEventsAndContacts' => $baseDir . '/../lib/Migration/RemoveOrphanEventsAndContacts.php',
'OCA\\DAV\\Migration\\Version1004Date20170825134824' => $baseDir . '/../lib/Migration/Version1004Date20170825134824.php',
'OCA\\DAV\\Migration\\Version1004Date20170919104507' => $baseDir . '/../lib/Migration/Version1004Date20170919104507.php',
@@ -273,31 +347,59 @@ return array(
'OCA\\DAV\\Migration\\Version1016Date20201109085907' => $baseDir . '/../lib/Migration/Version1016Date20201109085907.php',
'OCA\\DAV\\Migration\\Version1017Date20210216083742' => $baseDir . '/../lib/Migration/Version1017Date20210216083742.php',
'OCA\\DAV\\Migration\\Version1018Date20210312100735' => $baseDir . '/../lib/Migration/Version1018Date20210312100735.php',
+ 'OCA\\DAV\\Migration\\Version1024Date20211221144219' => $baseDir . '/../lib/Migration/Version1024Date20211221144219.php',
+ 'OCA\\DAV\\Migration\\Version1025Date20240308063933' => $baseDir . '/../lib/Migration/Version1025Date20240308063933.php',
+ 'OCA\\DAV\\Migration\\Version1027Date20230504122946' => $baseDir . '/../lib/Migration/Version1027Date20230504122946.php',
+ 'OCA\\DAV\\Migration\\Version1029Date20221114151721' => $baseDir . '/../lib/Migration/Version1029Date20221114151721.php',
+ 'OCA\\DAV\\Migration\\Version1029Date20231004091403' => $baseDir . '/../lib/Migration/Version1029Date20231004091403.php',
+ 'OCA\\DAV\\Migration\\Version1030Date20240205103243' => $baseDir . '/../lib/Migration/Version1030Date20240205103243.php',
+ 'OCA\\DAV\\Migration\\Version1031Date20240610134258' => $baseDir . '/../lib/Migration/Version1031Date20240610134258.php',
+ 'OCA\\DAV\\Model\\ExampleEvent' => $baseDir . '/../lib/Model/ExampleEvent.php',
+ 'OCA\\DAV\\Paginate\\LimitedCopyIterator' => $baseDir . '/../lib/Paginate/LimitedCopyIterator.php',
+ 'OCA\\DAV\\Paginate\\PaginateCache' => $baseDir . '/../lib/Paginate/PaginateCache.php',
+ 'OCA\\DAV\\Paginate\\PaginatePlugin' => $baseDir . '/../lib/Paginate/PaginatePlugin.php',
'OCA\\DAV\\Profiler\\ProfilerPlugin' => $baseDir . '/../lib/Profiler/ProfilerPlugin.php',
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningNode.php',
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php',
+ 'OCA\\DAV\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php',
'OCA\\DAV\\RootCollection' => $baseDir . '/../lib/RootCollection.php',
'OCA\\DAV\\Search\\ACalendarSearchProvider' => $baseDir . '/../lib/Search/ACalendarSearchProvider.php',
'OCA\\DAV\\Search\\ContactsSearchProvider' => $baseDir . '/../lib/Search/ContactsSearchProvider.php',
'OCA\\DAV\\Search\\EventsSearchProvider' => $baseDir . '/../lib/Search/EventsSearchProvider.php',
'OCA\\DAV\\Search\\TasksSearchProvider' => $baseDir . '/../lib/Search/TasksSearchProvider.php',
'OCA\\DAV\\Server' => $baseDir . '/../lib/Server.php',
+ 'OCA\\DAV\\ServerFactory' => $baseDir . '/../lib/ServerFactory.php',
+ 'OCA\\DAV\\Service\\AbsenceService' => $baseDir . '/../lib/Service/AbsenceService.php',
+ 'OCA\\DAV\\Service\\ExampleContactService' => $baseDir . '/../lib/Service/ExampleContactService.php',
+ 'OCA\\DAV\\Service\\ExampleEventService' => $baseDir . '/../lib/Service/ExampleEventService.php',
+ 'OCA\\DAV\\Settings\\Admin\\SystemAddressBookSettings' => $baseDir . '/../lib/Settings/Admin/SystemAddressBookSettings.php',
'OCA\\DAV\\Settings\\AvailabilitySettings' => $baseDir . '/../lib/Settings/AvailabilitySettings.php',
'OCA\\DAV\\Settings\\CalDAVSettings' => $baseDir . '/../lib/Settings/CalDAVSettings.php',
+ 'OCA\\DAV\\Settings\\ExampleContentSettings' => $baseDir . '/../lib/Settings/ExampleContentSettings.php',
+ 'OCA\\DAV\\SetupChecks\\NeedsSystemAddressBookSync' => $baseDir . '/../lib/SetupChecks/NeedsSystemAddressBookSync.php',
+ 'OCA\\DAV\\SetupChecks\\WebdavEndpoint' => $baseDir . '/../lib/SetupChecks/WebdavEndpoint.php',
'OCA\\DAV\\Storage\\PublicOwnerWrapper' => $baseDir . '/../lib/Storage/PublicOwnerWrapper.php',
+ 'OCA\\DAV\\Storage\\PublicShareWrapper' => $baseDir . '/../lib/Storage/PublicShareWrapper.php',
+ 'OCA\\DAV\\SystemTag\\SystemTagList' => $baseDir . '/../lib/SystemTag/SystemTagList.php',
'OCA\\DAV\\SystemTag\\SystemTagMappingNode' => $baseDir . '/../lib/SystemTag/SystemTagMappingNode.php',
'OCA\\DAV\\SystemTag\\SystemTagNode' => $baseDir . '/../lib/SystemTag/SystemTagNode.php',
+ 'OCA\\DAV\\SystemTag\\SystemTagObjectType' => $baseDir . '/../lib/SystemTag/SystemTagObjectType.php',
'OCA\\DAV\\SystemTag\\SystemTagPlugin' => $baseDir . '/../lib/SystemTag/SystemTagPlugin.php',
'OCA\\DAV\\SystemTag\\SystemTagsByIdCollection' => $baseDir . '/../lib/SystemTag/SystemTagsByIdCollection.php',
+ 'OCA\\DAV\\SystemTag\\SystemTagsInUseCollection' => $baseDir . '/../lib/SystemTag/SystemTagsInUseCollection.php',
+ 'OCA\\DAV\\SystemTag\\SystemTagsObjectList' => $baseDir . '/../lib/SystemTag/SystemTagsObjectList.php',
'OCA\\DAV\\SystemTag\\SystemTagsObjectMappingCollection' => $baseDir . '/../lib/SystemTag/SystemTagsObjectMappingCollection.php',
'OCA\\DAV\\SystemTag\\SystemTagsObjectTypeCollection' => $baseDir . '/../lib/SystemTag/SystemTagsObjectTypeCollection.php',
'OCA\\DAV\\SystemTag\\SystemTagsRelationsCollection' => $baseDir . '/../lib/SystemTag/SystemTagsRelationsCollection.php',
'OCA\\DAV\\Traits\\PrincipalProxyTrait' => $baseDir . '/../lib/Traits/PrincipalProxyTrait.php',
'OCA\\DAV\\Upload\\AssemblyStream' => $baseDir . '/../lib/Upload/AssemblyStream.php',
'OCA\\DAV\\Upload\\ChunkingPlugin' => $baseDir . '/../lib/Upload/ChunkingPlugin.php',
+ 'OCA\\DAV\\Upload\\ChunkingV2Plugin' => $baseDir . '/../lib/Upload/ChunkingV2Plugin.php',
'OCA\\DAV\\Upload\\CleanupService' => $baseDir . '/../lib/Upload/CleanupService.php',
'OCA\\DAV\\Upload\\FutureFile' => $baseDir . '/../lib/Upload/FutureFile.php',
+ 'OCA\\DAV\\Upload\\PartFile' => $baseDir . '/../lib/Upload/PartFile.php',
'OCA\\DAV\\Upload\\RootCollection' => $baseDir . '/../lib/Upload/RootCollection.php',
+ 'OCA\\DAV\\Upload\\UploadAutoMkcolPlugin' => $baseDir . '/../lib/Upload/UploadAutoMkcolPlugin.php',
'OCA\\DAV\\Upload\\UploadFile' => $baseDir . '/../lib/Upload/UploadFile.php',
'OCA\\DAV\\Upload\\UploadFolder' => $baseDir . '/../lib/Upload/UploadFolder.php',
'OCA\\DAV\\Upload\\UploadHome' => $baseDir . '/../lib/Upload/UploadHome.php',
diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php
index 1f57b8b043a..75ac3350160 100644
--- a/apps/dav/composer/composer/autoload_static.php
+++ b/apps/dav/composer/composer/autoload_static.php
@@ -31,12 +31,17 @@ class ComposerStaticInitDAV
'OCA\\DAV\\BackgroundJob\\CalendarRetentionJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CalendarRetentionJob.php',
'OCA\\DAV\\BackgroundJob\\CleanupDirectLinksJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupDirectLinksJob.php',
'OCA\\DAV\\BackgroundJob\\CleanupInvitationTokenJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupInvitationTokenJob.php',
+ 'OCA\\DAV\\BackgroundJob\\CleanupOrphanedChildrenJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupOrphanedChildrenJob.php',
+ 'OCA\\DAV\\BackgroundJob\\DeleteOutdatedSchedulingObjects' => __DIR__ . '/..' . '/../lib/BackgroundJob/DeleteOutdatedSchedulingObjects.php',
'OCA\\DAV\\BackgroundJob\\EventReminderJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/EventReminderJob.php',
'OCA\\DAV\\BackgroundJob\\GenerateBirthdayCalendarBackgroundJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php',
+ 'OCA\\DAV\\BackgroundJob\\OutOfOfficeEventDispatcherJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php',
+ 'OCA\\DAV\\BackgroundJob\\PruneOutdatedSyncTokensJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/PruneOutdatedSyncTokensJob.php',
'OCA\\DAV\\BackgroundJob\\RefreshWebcalJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/RefreshWebcalJob.php',
'OCA\\DAV\\BackgroundJob\\RegisterRegenerateBirthdayCalendars' => __DIR__ . '/..' . '/../lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php',
'OCA\\DAV\\BackgroundJob\\UpdateCalendarResourcesRoomsBackgroundJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php',
'OCA\\DAV\\BackgroundJob\\UploadCleanup' => __DIR__ . '/..' . '/../lib/BackgroundJob/UploadCleanup.php',
+ 'OCA\\DAV\\BackgroundJob\\UserStatusAutomation' => __DIR__ . '/..' . '/../lib/BackgroundJob/UserStatusAutomation.php',
'OCA\\DAV\\BulkUpload\\BulkUploadPlugin' => __DIR__ . '/..' . '/../lib/BulkUpload/BulkUploadPlugin.php',
'OCA\\DAV\\BulkUpload\\MultipartRequestParser' => __DIR__ . '/..' . '/../lib/BulkUpload/MultipartRequestParser.php',
'OCA\\DAV\\CalDAV\\Activity\\Backend' => __DIR__ . '/..' . '/../lib/CalDAV/Activity/Backend.php',
@@ -50,12 +55,17 @@ class ComposerStaticInitDAV
'OCA\\DAV\\CalDAV\\Activity\\Setting\\Calendar' => __DIR__ . '/..' . '/../lib/CalDAV/Activity/Setting/Calendar.php',
'OCA\\DAV\\CalDAV\\Activity\\Setting\\Event' => __DIR__ . '/..' . '/../lib/CalDAV/Activity/Setting/Event.php',
'OCA\\DAV\\CalDAV\\Activity\\Setting\\Todo' => __DIR__ . '/..' . '/../lib/CalDAV/Activity/Setting/Todo.php',
+ 'OCA\\DAV\\CalDAV\\AppCalendar\\AppCalendar' => __DIR__ . '/..' . '/../lib/CalDAV/AppCalendar/AppCalendar.php',
+ 'OCA\\DAV\\CalDAV\\AppCalendar\\AppCalendarPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/AppCalendar/AppCalendarPlugin.php',
+ 'OCA\\DAV\\CalDAV\\AppCalendar\\CalendarObject' => __DIR__ . '/..' . '/../lib/CalDAV/AppCalendar/CalendarObject.php',
'OCA\\DAV\\CalDAV\\Auth\\CustomPrincipalPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/Auth/CustomPrincipalPlugin.php',
'OCA\\DAV\\CalDAV\\Auth\\PublicPrincipalPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/Auth/PublicPrincipalPlugin.php',
'OCA\\DAV\\CalDAV\\BirthdayCalendar\\EnablePlugin' => __DIR__ . '/..' . '/../lib/CalDAV/BirthdayCalendar/EnablePlugin.php',
'OCA\\DAV\\CalDAV\\BirthdayService' => __DIR__ . '/..' . '/../lib/CalDAV/BirthdayService.php',
'OCA\\DAV\\CalDAV\\CachedSubscription' => __DIR__ . '/..' . '/../lib/CalDAV/CachedSubscription.php',
+ 'OCA\\DAV\\CalDAV\\CachedSubscriptionImpl' => __DIR__ . '/..' . '/../lib/CalDAV/CachedSubscriptionImpl.php',
'OCA\\DAV\\CalDAV\\CachedSubscriptionObject' => __DIR__ . '/..' . '/../lib/CalDAV/CachedSubscriptionObject.php',
+ 'OCA\\DAV\\CalDAV\\CachedSubscriptionProvider' => __DIR__ . '/..' . '/../lib/CalDAV/CachedSubscriptionProvider.php',
'OCA\\DAV\\CalDAV\\CalDavBackend' => __DIR__ . '/..' . '/../lib/CalDAV/CalDavBackend.php',
'OCA\\DAV\\CalDAV\\Calendar' => __DIR__ . '/..' . '/../lib/CalDAV/Calendar.php',
'OCA\\DAV\\CalDAV\\CalendarHome' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarHome.php',
@@ -64,6 +74,14 @@ class ComposerStaticInitDAV
'OCA\\DAV\\CalDAV\\CalendarObject' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarObject.php',
'OCA\\DAV\\CalDAV\\CalendarProvider' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarProvider.php',
'OCA\\DAV\\CalDAV\\CalendarRoot' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarRoot.php',
+ 'OCA\\DAV\\CalDAV\\DefaultCalendarValidator' => __DIR__ . '/..' . '/../lib/CalDAV/DefaultCalendarValidator.php',
+ 'OCA\\DAV\\CalDAV\\EmbeddedCalDavServer' => __DIR__ . '/..' . '/../lib/CalDAV/EmbeddedCalDavServer.php',
+ 'OCA\\DAV\\CalDAV\\EventComparisonService' => __DIR__ . '/..' . '/../lib/CalDAV/EventComparisonService.php',
+ 'OCA\\DAV\\CalDAV\\EventReader' => __DIR__ . '/..' . '/../lib/CalDAV/EventReader.php',
+ 'OCA\\DAV\\CalDAV\\EventReaderRDate' => __DIR__ . '/..' . '/../lib/CalDAV/EventReaderRDate.php',
+ 'OCA\\DAV\\CalDAV\\EventReaderRRule' => __DIR__ . '/..' . '/../lib/CalDAV/EventReaderRRule.php',
+ 'OCA\\DAV\\CalDAV\\Export\\ExportService' => __DIR__ . '/..' . '/../lib/CalDAV/Export/ExportService.php',
+ 'OCA\\DAV\\CalDAV\\FreeBusy\\FreeBusyGenerator' => __DIR__ . '/..' . '/../lib/CalDAV/FreeBusy/FreeBusyGenerator.php',
'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php',
'OCA\\DAV\\CalDAV\\IRestorable' => __DIR__ . '/..' . '/../lib/CalDAV/IRestorable.php',
'OCA\\DAV\\CalDAV\\Integration\\ExternalCalendar' => __DIR__ . '/..' . '/../lib/CalDAV/Integration/ExternalCalendar.php',
@@ -96,6 +114,7 @@ class ComposerStaticInitDAV
'OCA\\DAV\\CalDAV\\ResourceBooking\\RoomPrincipalBackend' => __DIR__ . '/..' . '/../lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php',
'OCA\\DAV\\CalDAV\\RetentionService' => __DIR__ . '/..' . '/../lib/CalDAV/RetentionService.php',
'OCA\\DAV\\CalDAV\\Schedule\\IMipPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/Schedule/IMipPlugin.php',
+ 'OCA\\DAV\\CalDAV\\Schedule\\IMipService' => __DIR__ . '/..' . '/../lib/CalDAV/Schedule/IMipService.php',
'OCA\\DAV\\CalDAV\\Schedule\\Plugin' => __DIR__ . '/..' . '/../lib/CalDAV/Schedule/Plugin.php',
'OCA\\DAV\\CalDAV\\Search\\SearchPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/Search/SearchPlugin.php',
'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\CompFilter' => __DIR__ . '/..' . '/../lib/CalDAV/Search/Xml/Filter/CompFilter.php',
@@ -105,11 +124,22 @@ class ComposerStaticInitDAV
'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\PropFilter' => __DIR__ . '/..' . '/../lib/CalDAV/Search/Xml/Filter/PropFilter.php',
'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\SearchTermFilter' => __DIR__ . '/..' . '/../lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php',
'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport' => __DIR__ . '/..' . '/../lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php',
+ 'OCA\\DAV\\CalDAV\\Security\\RateLimitingPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/Security/RateLimitingPlugin.php',
+ 'OCA\\DAV\\CalDAV\\Sharing\\Backend' => __DIR__ . '/..' . '/../lib/CalDAV/Sharing/Backend.php',
+ 'OCA\\DAV\\CalDAV\\Sharing\\Service' => __DIR__ . '/..' . '/../lib/CalDAV/Sharing/Service.php',
+ 'OCA\\DAV\\CalDAV\\Status\\StatusService' => __DIR__ . '/..' . '/../lib/CalDAV/Status/StatusService.php',
+ 'OCA\\DAV\\CalDAV\\TimeZoneFactory' => __DIR__ . '/..' . '/../lib/CalDAV/TimeZoneFactory.php',
+ 'OCA\\DAV\\CalDAV\\TimezoneService' => __DIR__ . '/..' . '/../lib/CalDAV/TimezoneService.php',
+ 'OCA\\DAV\\CalDAV\\TipBroker' => __DIR__ . '/..' . '/../lib/CalDAV/TipBroker.php',
'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObject' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/DeletedCalendarObject.php',
'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObjectsCollection' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php',
'OCA\\DAV\\CalDAV\\Trashbin\\Plugin' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/Plugin.php',
'OCA\\DAV\\CalDAV\\Trashbin\\RestoreTarget' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/RestoreTarget.php',
'OCA\\DAV\\CalDAV\\Trashbin\\TrashbinHome' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/TrashbinHome.php',
+ 'OCA\\DAV\\CalDAV\\UpcomingEvent' => __DIR__ . '/..' . '/../lib/CalDAV/UpcomingEvent.php',
+ 'OCA\\DAV\\CalDAV\\UpcomingEventsService' => __DIR__ . '/..' . '/../lib/CalDAV/UpcomingEventsService.php',
+ 'OCA\\DAV\\CalDAV\\Validation\\CalDavValidatePlugin' => __DIR__ . '/..' . '/../lib/CalDAV/Validation/CalDavValidatePlugin.php',
+ 'OCA\\DAV\\CalDAV\\WebcalCaching\\Connection' => __DIR__ . '/..' . '/../lib/CalDAV/WebcalCaching/Connection.php',
'OCA\\DAV\\CalDAV\\WebcalCaching\\Plugin' => __DIR__ . '/..' . '/../lib/CalDAV/WebcalCaching/Plugin.php',
'OCA\\DAV\\CalDAV\\WebcalCaching\\RefreshWebcalService' => __DIR__ . '/..' . '/../lib/CalDAV/WebcalCaching/RefreshWebcalService.php',
'OCA\\DAV\\Capabilities' => __DIR__ . '/..' . '/../lib/Capabilities.php',
@@ -122,6 +152,7 @@ class ComposerStaticInitDAV
'OCA\\DAV\\CardDAV\\AddressBook' => __DIR__ . '/..' . '/../lib/CardDAV/AddressBook.php',
'OCA\\DAV\\CardDAV\\AddressBookImpl' => __DIR__ . '/..' . '/../lib/CardDAV/AddressBookImpl.php',
'OCA\\DAV\\CardDAV\\AddressBookRoot' => __DIR__ . '/..' . '/../lib/CardDAV/AddressBookRoot.php',
+ 'OCA\\DAV\\CardDAV\\Card' => __DIR__ . '/..' . '/../lib/CardDAV/Card.php',
'OCA\\DAV\\CardDAV\\CardDavBackend' => __DIR__ . '/..' . '/../lib/CardDAV/CardDavBackend.php',
'OCA\\DAV\\CardDAV\\ContactsManager' => __DIR__ . '/..' . '/../lib/CardDAV/ContactsManager.php',
'OCA\\DAV\\CardDAV\\Converter' => __DIR__ . '/..' . '/../lib/CardDAV/Converter.php',
@@ -132,18 +163,33 @@ class ComposerStaticInitDAV
'OCA\\DAV\\CardDAV\\MultiGetExportPlugin' => __DIR__ . '/..' . '/../lib/CardDAV/MultiGetExportPlugin.php',
'OCA\\DAV\\CardDAV\\PhotoCache' => __DIR__ . '/..' . '/../lib/CardDAV/PhotoCache.php',
'OCA\\DAV\\CardDAV\\Plugin' => __DIR__ . '/..' . '/../lib/CardDAV/Plugin.php',
+ 'OCA\\DAV\\CardDAV\\Security\\CardDavRateLimitingPlugin' => __DIR__ . '/..' . '/../lib/CardDAV/Security/CardDavRateLimitingPlugin.php',
+ 'OCA\\DAV\\CardDAV\\Sharing\\Backend' => __DIR__ . '/..' . '/../lib/CardDAV/Sharing/Backend.php',
+ 'OCA\\DAV\\CardDAV\\Sharing\\Service' => __DIR__ . '/..' . '/../lib/CardDAV/Sharing/Service.php',
'OCA\\DAV\\CardDAV\\SyncService' => __DIR__ . '/..' . '/../lib/CardDAV/SyncService.php',
'OCA\\DAV\\CardDAV\\SystemAddressbook' => __DIR__ . '/..' . '/../lib/CardDAV/SystemAddressbook.php',
'OCA\\DAV\\CardDAV\\UserAddressBooks' => __DIR__ . '/..' . '/../lib/CardDAV/UserAddressBooks.php',
+ 'OCA\\DAV\\CardDAV\\Validation\\CardDavValidatePlugin' => __DIR__ . '/..' . '/../lib/CardDAV/Validation/CardDavValidatePlugin.php',
'OCA\\DAV\\CardDAV\\Xml\\Groups' => __DIR__ . '/..' . '/../lib/CardDAV/Xml/Groups.php',
+ 'OCA\\DAV\\Command\\ClearCalendarUnshares' => __DIR__ . '/..' . '/../lib/Command/ClearCalendarUnshares.php',
+ 'OCA\\DAV\\Command\\ClearContactsPhotoCache' => __DIR__ . '/..' . '/../lib/Command/ClearContactsPhotoCache.php',
'OCA\\DAV\\Command\\CreateAddressBook' => __DIR__ . '/..' . '/../lib/Command/CreateAddressBook.php',
'OCA\\DAV\\Command\\CreateCalendar' => __DIR__ . '/..' . '/../lib/Command/CreateCalendar.php',
+ 'OCA\\DAV\\Command\\CreateSubscription' => __DIR__ . '/..' . '/../lib/Command/CreateSubscription.php',
'OCA\\DAV\\Command\\DeleteCalendar' => __DIR__ . '/..' . '/../lib/Command/DeleteCalendar.php',
+ 'OCA\\DAV\\Command\\DeleteSubscription' => __DIR__ . '/..' . '/../lib/Command/DeleteSubscription.php',
+ 'OCA\\DAV\\Command\\ExportCalendar' => __DIR__ . '/..' . '/../lib/Command/ExportCalendar.php',
+ 'OCA\\DAV\\Command\\FixCalendarSyncCommand' => __DIR__ . '/..' . '/../lib/Command/FixCalendarSyncCommand.php',
+ 'OCA\\DAV\\Command\\GetAbsenceCommand' => __DIR__ . '/..' . '/../lib/Command/GetAbsenceCommand.php',
+ 'OCA\\DAV\\Command\\ListAddressbooks' => __DIR__ . '/..' . '/../lib/Command/ListAddressbooks.php',
+ 'OCA\\DAV\\Command\\ListCalendarShares' => __DIR__ . '/..' . '/../lib/Command/ListCalendarShares.php',
'OCA\\DAV\\Command\\ListCalendars' => __DIR__ . '/..' . '/../lib/Command/ListCalendars.php',
+ 'OCA\\DAV\\Command\\ListSubscriptions' => __DIR__ . '/..' . '/../lib/Command/ListSubscriptions.php',
'OCA\\DAV\\Command\\MoveCalendar' => __DIR__ . '/..' . '/../lib/Command/MoveCalendar.php',
'OCA\\DAV\\Command\\RemoveInvalidShares' => __DIR__ . '/..' . '/../lib/Command/RemoveInvalidShares.php',
'OCA\\DAV\\Command\\RetentionCleanupCommand' => __DIR__ . '/..' . '/../lib/Command/RetentionCleanupCommand.php',
'OCA\\DAV\\Command\\SendEventReminders' => __DIR__ . '/..' . '/../lib/Command/SendEventReminders.php',
+ 'OCA\\DAV\\Command\\SetAbsenceCommand' => __DIR__ . '/..' . '/../lib/Command/SetAbsenceCommand.php',
'OCA\\DAV\\Command\\SyncBirthdayCalendar' => __DIR__ . '/..' . '/../lib/Command/SyncBirthdayCalendar.php',
'OCA\\DAV\\Command\\SyncSystemAddressBook' => __DIR__ . '/..' . '/../lib/Command/SyncSystemAddressBook.php',
'OCA\\DAV\\Comments\\CommentNode' => __DIR__ . '/..' . '/../lib/Comments/CommentNode.php',
@@ -152,9 +198,9 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Comments\\EntityTypeCollection' => __DIR__ . '/..' . '/../lib/Comments/EntityTypeCollection.php',
'OCA\\DAV\\Comments\\RootCollection' => __DIR__ . '/..' . '/../lib/Comments/RootCollection.php',
'OCA\\DAV\\Connector\\LegacyDAVACL' => __DIR__ . '/..' . '/../lib/Connector/LegacyDAVACL.php',
- 'OCA\\DAV\\Connector\\PublicAuth' => __DIR__ . '/..' . '/../lib/Connector/PublicAuth.php',
+ 'OCA\\DAV\\Connector\\LegacyPublicAuth' => __DIR__ . '/..' . '/../lib/Connector/LegacyPublicAuth.php',
'OCA\\DAV\\Connector\\Sabre\\AnonymousOptionsPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/AnonymousOptionsPlugin.php',
- 'OCA\\DAV\\Connector\\Sabre\\AppEnabledPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/AppEnabledPlugin.php',
+ 'OCA\\DAV\\Connector\\Sabre\\AppleQuirksPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/AppleQuirksPlugin.php',
'OCA\\DAV\\Connector\\Sabre\\Auth' => __DIR__ . '/..' . '/../lib/Connector/Sabre/Auth.php',
'OCA\\DAV\\Connector\\Sabre\\BearerAuth' => __DIR__ . '/..' . '/../lib/Connector/Sabre/BearerAuth.php',
'OCA\\DAV\\Connector\\Sabre\\BlockLegacyClientPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/BlockLegacyClientPlugin.php',
@@ -173,6 +219,7 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Connector\\Sabre\\Exception\\Forbidden' => __DIR__ . '/..' . '/../lib/Connector/Sabre/Exception/Forbidden.php',
'OCA\\DAV\\Connector\\Sabre\\Exception\\InvalidPath' => __DIR__ . '/..' . '/../lib/Connector/Sabre/Exception/InvalidPath.php',
'OCA\\DAV\\Connector\\Sabre\\Exception\\PasswordLoginForbidden' => __DIR__ . '/..' . '/../lib/Connector/Sabre/Exception/PasswordLoginForbidden.php',
+ 'OCA\\DAV\\Connector\\Sabre\\Exception\\TooManyRequests' => __DIR__ . '/..' . '/../lib/Connector/Sabre/Exception/TooManyRequests.php',
'OCA\\DAV\\Connector\\Sabre\\Exception\\UnsupportedMediaType' => __DIR__ . '/..' . '/../lib/Connector/Sabre/Exception/UnsupportedMediaType.php',
'OCA\\DAV\\Connector\\Sabre\\FakeLockerPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/FakeLockerPlugin.php',
'OCA\\DAV\\Connector\\Sabre\\File' => __DIR__ . '/..' . '/../lib/Connector/Sabre/File.php',
@@ -184,7 +231,9 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Connector\\Sabre\\Node' => __DIR__ . '/..' . '/../lib/Connector/Sabre/Node.php',
'OCA\\DAV\\Connector\\Sabre\\ObjectTree' => __DIR__ . '/..' . '/../lib/Connector/Sabre/ObjectTree.php',
'OCA\\DAV\\Connector\\Sabre\\Principal' => __DIR__ . '/..' . '/../lib/Connector/Sabre/Principal.php',
+ 'OCA\\DAV\\Connector\\Sabre\\PropFindMonitorPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/PropFindMonitorPlugin.php',
'OCA\\DAV\\Connector\\Sabre\\PropfindCompressionPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/PropfindCompressionPlugin.php',
+ 'OCA\\DAV\\Connector\\Sabre\\PublicAuth' => __DIR__ . '/..' . '/../lib/Connector/Sabre/PublicAuth.php',
'OCA\\DAV\\Connector\\Sabre\\QuotaPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/QuotaPlugin.php',
'OCA\\DAV\\Connector\\Sabre\\RequestIdHeaderPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/RequestIdHeaderPlugin.php',
'OCA\\DAV\\Connector\\Sabre\\Server' => __DIR__ . '/..' . '/../lib/Connector/Sabre/Server.php',
@@ -194,20 +243,31 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Connector\\Sabre\\SharesPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/SharesPlugin.php',
'OCA\\DAV\\Connector\\Sabre\\TagList' => __DIR__ . '/..' . '/../lib/Connector/Sabre/TagList.php',
'OCA\\DAV\\Connector\\Sabre\\TagsPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/TagsPlugin.php',
+ 'OCA\\DAV\\Connector\\Sabre\\ZipFolderPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/ZipFolderPlugin.php',
'OCA\\DAV\\Controller\\BirthdayCalendarController' => __DIR__ . '/..' . '/../lib/Controller/BirthdayCalendarController.php',
'OCA\\DAV\\Controller\\DirectController' => __DIR__ . '/..' . '/../lib/Controller/DirectController.php',
+ 'OCA\\DAV\\Controller\\ExampleContentController' => __DIR__ . '/..' . '/../lib/Controller/ExampleContentController.php',
'OCA\\DAV\\Controller\\InvitationResponseController' => __DIR__ . '/..' . '/../lib/Controller/InvitationResponseController.php',
+ 'OCA\\DAV\\Controller\\OutOfOfficeController' => __DIR__ . '/..' . '/../lib/Controller/OutOfOfficeController.php',
+ 'OCA\\DAV\\Controller\\UpcomingEventsController' => __DIR__ . '/..' . '/../lib/Controller/UpcomingEventsController.php',
'OCA\\DAV\\DAV\\CustomPropertiesBackend' => __DIR__ . '/..' . '/../lib/DAV/CustomPropertiesBackend.php',
'OCA\\DAV\\DAV\\GroupPrincipalBackend' => __DIR__ . '/..' . '/../lib/DAV/GroupPrincipalBackend.php',
'OCA\\DAV\\DAV\\PublicAuth' => __DIR__ . '/..' . '/../lib/DAV/PublicAuth.php',
'OCA\\DAV\\DAV\\Sharing\\Backend' => __DIR__ . '/..' . '/../lib/DAV/Sharing/Backend.php',
'OCA\\DAV\\DAV\\Sharing\\IShareable' => __DIR__ . '/..' . '/../lib/DAV/Sharing/IShareable.php',
'OCA\\DAV\\DAV\\Sharing\\Plugin' => __DIR__ . '/..' . '/../lib/DAV/Sharing/Plugin.php',
+ 'OCA\\DAV\\DAV\\Sharing\\SharingMapper' => __DIR__ . '/..' . '/../lib/DAV/Sharing/SharingMapper.php',
+ 'OCA\\DAV\\DAV\\Sharing\\SharingService' => __DIR__ . '/..' . '/../lib/DAV/Sharing/SharingService.php',
'OCA\\DAV\\DAV\\Sharing\\Xml\\Invite' => __DIR__ . '/..' . '/../lib/DAV/Sharing/Xml/Invite.php',
'OCA\\DAV\\DAV\\Sharing\\Xml\\ShareRequest' => __DIR__ . '/..' . '/../lib/DAV/Sharing/Xml/ShareRequest.php',
'OCA\\DAV\\DAV\\SystemPrincipalBackend' => __DIR__ . '/..' . '/../lib/DAV/SystemPrincipalBackend.php',
+ 'OCA\\DAV\\DAV\\ViewOnlyPlugin' => __DIR__ . '/..' . '/../lib/DAV/ViewOnlyPlugin.php',
+ 'OCA\\DAV\\Db\\Absence' => __DIR__ . '/..' . '/../lib/Db/Absence.php',
+ 'OCA\\DAV\\Db\\AbsenceMapper' => __DIR__ . '/..' . '/../lib/Db/AbsenceMapper.php',
'OCA\\DAV\\Db\\Direct' => __DIR__ . '/..' . '/../lib/Db/Direct.php',
'OCA\\DAV\\Db\\DirectMapper' => __DIR__ . '/..' . '/../lib/Db/DirectMapper.php',
+ 'OCA\\DAV\\Db\\Property' => __DIR__ . '/..' . '/../lib/Db/Property.php',
+ 'OCA\\DAV\\Db\\PropertyMapper' => __DIR__ . '/..' . '/../lib/Db/PropertyMapper.php',
'OCA\\DAV\\Direct\\DirectFile' => __DIR__ . '/..' . '/../lib/Direct/DirectFile.php',
'OCA\\DAV\\Direct\\DirectHome' => __DIR__ . '/..' . '/../lib/Direct/DirectHome.php',
'OCA\\DAV\\Direct\\Server' => __DIR__ . '/..' . '/../lib/Direct/Server.php',
@@ -223,11 +283,6 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Events\\CalendarCreatedEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarCreatedEvent.php',
'OCA\\DAV\\Events\\CalendarDeletedEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarDeletedEvent.php',
'OCA\\DAV\\Events\\CalendarMovedToTrashEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarMovedToTrashEvent.php',
- 'OCA\\DAV\\Events\\CalendarObjectCreatedEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarObjectCreatedEvent.php',
- 'OCA\\DAV\\Events\\CalendarObjectDeletedEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarObjectDeletedEvent.php',
- 'OCA\\DAV\\Events\\CalendarObjectMovedToTrashEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarObjectMovedToTrashEvent.php',
- 'OCA\\DAV\\Events\\CalendarObjectRestoredEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarObjectRestoredEvent.php',
- 'OCA\\DAV\\Events\\CalendarObjectUpdatedEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarObjectUpdatedEvent.php',
'OCA\\DAV\\Events\\CalendarPublishedEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarPublishedEvent.php',
'OCA\\DAV\\Events\\CalendarRestoredEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarRestoredEvent.php',
'OCA\\DAV\\Events\\CalendarShareUpdatedEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarShareUpdatedEvent.php',
@@ -235,11 +290,15 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Events\\CalendarUpdatedEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarUpdatedEvent.php',
'OCA\\DAV\\Events\\CardCreatedEvent' => __DIR__ . '/..' . '/../lib/Events/CardCreatedEvent.php',
'OCA\\DAV\\Events\\CardDeletedEvent' => __DIR__ . '/..' . '/../lib/Events/CardDeletedEvent.php',
+ 'OCA\\DAV\\Events\\CardMovedEvent' => __DIR__ . '/..' . '/../lib/Events/CardMovedEvent.php',
'OCA\\DAV\\Events\\CardUpdatedEvent' => __DIR__ . '/..' . '/../lib/Events/CardUpdatedEvent.php',
+ 'OCA\\DAV\\Events\\SabrePluginAddEvent' => __DIR__ . '/..' . '/../lib/Events/SabrePluginAddEvent.php',
'OCA\\DAV\\Events\\SabrePluginAuthInitEvent' => __DIR__ . '/..' . '/../lib/Events/SabrePluginAuthInitEvent.php',
'OCA\\DAV\\Events\\SubscriptionCreatedEvent' => __DIR__ . '/..' . '/../lib/Events/SubscriptionCreatedEvent.php',
'OCA\\DAV\\Events\\SubscriptionDeletedEvent' => __DIR__ . '/..' . '/../lib/Events/SubscriptionDeletedEvent.php',
'OCA\\DAV\\Events\\SubscriptionUpdatedEvent' => __DIR__ . '/..' . '/../lib/Events/SubscriptionUpdatedEvent.php',
+ 'OCA\\DAV\\Exception\\ExampleEventException' => __DIR__ . '/..' . '/../lib/Exception/ExampleEventException.php',
+ 'OCA\\DAV\\Exception\\ServerMaintenanceMode' => __DIR__ . '/..' . '/../lib/Exception/ServerMaintenanceMode.php',
'OCA\\DAV\\Exception\\UnsupportedLimitOnInitialSyncException' => __DIR__ . '/..' . '/../lib/Exception/UnsupportedLimitOnInitialSyncException.php',
'OCA\\DAV\\Files\\BrowserErrorPagePlugin' => __DIR__ . '/..' . '/../lib/Files/BrowserErrorPagePlugin.php',
'OCA\\DAV\\Files\\FileSearchBackend' => __DIR__ . '/..' . '/../lib/Files/FileSearchBackend.php',
@@ -248,25 +307,40 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Files\\RootCollection' => __DIR__ . '/..' . '/../lib/Files/RootCollection.php',
'OCA\\DAV\\Files\\Sharing\\FilesDropPlugin' => __DIR__ . '/..' . '/../lib/Files/Sharing/FilesDropPlugin.php',
'OCA\\DAV\\Files\\Sharing\\PublicLinkCheckPlugin' => __DIR__ . '/..' . '/../lib/Files/Sharing/PublicLinkCheckPlugin.php',
- 'OCA\\DAV\\HookManager' => __DIR__ . '/..' . '/../lib/HookManager.php',
+ 'OCA\\DAV\\Files\\Sharing\\RootCollection' => __DIR__ . '/..' . '/../lib/Files/Sharing/RootCollection.php',
'OCA\\DAV\\Listener\\ActivityUpdaterListener' => __DIR__ . '/..' . '/../lib/Listener/ActivityUpdaterListener.php',
+ 'OCA\\DAV\\Listener\\AddMissingIndicesListener' => __DIR__ . '/..' . '/../lib/Listener/AddMissingIndicesListener.php',
'OCA\\DAV\\Listener\\AddressbookListener' => __DIR__ . '/..' . '/../lib/Listener/AddressbookListener.php',
+ 'OCA\\DAV\\Listener\\BirthdayListener' => __DIR__ . '/..' . '/../lib/Listener/BirthdayListener.php',
'OCA\\DAV\\Listener\\CalendarContactInteractionListener' => __DIR__ . '/..' . '/../lib/Listener/CalendarContactInteractionListener.php',
'OCA\\DAV\\Listener\\CalendarDeletionDefaultUpdaterListener' => __DIR__ . '/..' . '/../lib/Listener/CalendarDeletionDefaultUpdaterListener.php',
'OCA\\DAV\\Listener\\CalendarObjectReminderUpdaterListener' => __DIR__ . '/..' . '/../lib/Listener/CalendarObjectReminderUpdaterListener.php',
+ 'OCA\\DAV\\Listener\\CalendarPublicationListener' => __DIR__ . '/..' . '/../lib/Listener/CalendarPublicationListener.php',
+ 'OCA\\DAV\\Listener\\CalendarShareUpdateListener' => __DIR__ . '/..' . '/../lib/Listener/CalendarShareUpdateListener.php',
'OCA\\DAV\\Listener\\CardListener' => __DIR__ . '/..' . '/../lib/Listener/CardListener.php',
+ 'OCA\\DAV\\Listener\\ClearPhotoCacheListener' => __DIR__ . '/..' . '/../lib/Listener/ClearPhotoCacheListener.php',
+ 'OCA\\DAV\\Listener\\DavAdminSettingsListener' => __DIR__ . '/..' . '/../lib/Listener/DavAdminSettingsListener.php',
+ 'OCA\\DAV\\Listener\\OutOfOfficeListener' => __DIR__ . '/..' . '/../lib/Listener/OutOfOfficeListener.php',
+ 'OCA\\DAV\\Listener\\SubscriptionListener' => __DIR__ . '/..' . '/../lib/Listener/SubscriptionListener.php',
+ 'OCA\\DAV\\Listener\\TrustedServerRemovedListener' => __DIR__ . '/..' . '/../lib/Listener/TrustedServerRemovedListener.php',
+ 'OCA\\DAV\\Listener\\UserEventsListener' => __DIR__ . '/..' . '/../lib/Listener/UserEventsListener.php',
+ 'OCA\\DAV\\Listener\\UserPreferenceListener' => __DIR__ . '/..' . '/../lib/Listener/UserPreferenceListener.php',
'OCA\\DAV\\Migration\\BuildCalendarSearchIndex' => __DIR__ . '/..' . '/../lib/Migration/BuildCalendarSearchIndex.php',
'OCA\\DAV\\Migration\\BuildCalendarSearchIndexBackgroundJob' => __DIR__ . '/..' . '/../lib/Migration/BuildCalendarSearchIndexBackgroundJob.php',
'OCA\\DAV\\Migration\\BuildSocialSearchIndex' => __DIR__ . '/..' . '/../lib/Migration/BuildSocialSearchIndex.php',
'OCA\\DAV\\Migration\\BuildSocialSearchIndexBackgroundJob' => __DIR__ . '/..' . '/../lib/Migration/BuildSocialSearchIndexBackgroundJob.php',
'OCA\\DAV\\Migration\\CalDAVRemoveEmptyValue' => __DIR__ . '/..' . '/../lib/Migration/CalDAVRemoveEmptyValue.php',
'OCA\\DAV\\Migration\\ChunkCleanup' => __DIR__ . '/..' . '/../lib/Migration/ChunkCleanup.php',
+ 'OCA\\DAV\\Migration\\CreateSystemAddressBookStep' => __DIR__ . '/..' . '/../lib/Migration/CreateSystemAddressBookStep.php',
+ 'OCA\\DAV\\Migration\\DeleteSchedulingObjects' => __DIR__ . '/..' . '/../lib/Migration/DeleteSchedulingObjects.php',
'OCA\\DAV\\Migration\\FixBirthdayCalendarComponent' => __DIR__ . '/..' . '/../lib/Migration/FixBirthdayCalendarComponent.php',
'OCA\\DAV\\Migration\\RefreshWebcalJobRegistrar' => __DIR__ . '/..' . '/../lib/Migration/RefreshWebcalJobRegistrar.php',
'OCA\\DAV\\Migration\\RegenerateBirthdayCalendars' => __DIR__ . '/..' . '/../lib/Migration/RegenerateBirthdayCalendars.php',
'OCA\\DAV\\Migration\\RegisterBuildReminderIndexBackgroundJob' => __DIR__ . '/..' . '/../lib/Migration/RegisterBuildReminderIndexBackgroundJob.php',
+ 'OCA\\DAV\\Migration\\RegisterUpdateCalendarResourcesRoomBackgroundJob' => __DIR__ . '/..' . '/../lib/Migration/RegisterUpdateCalendarResourcesRoomBackgroundJob.php',
'OCA\\DAV\\Migration\\RemoveClassifiedEventActivity' => __DIR__ . '/..' . '/../lib/Migration/RemoveClassifiedEventActivity.php',
'OCA\\DAV\\Migration\\RemoveDeletedUsersCalendarSubscriptions' => __DIR__ . '/..' . '/../lib/Migration/RemoveDeletedUsersCalendarSubscriptions.php',
+ 'OCA\\DAV\\Migration\\RemoveObjectProperties' => __DIR__ . '/..' . '/../lib/Migration/RemoveObjectProperties.php',
'OCA\\DAV\\Migration\\RemoveOrphanEventsAndContacts' => __DIR__ . '/..' . '/../lib/Migration/RemoveOrphanEventsAndContacts.php',
'OCA\\DAV\\Migration\\Version1004Date20170825134824' => __DIR__ . '/..' . '/../lib/Migration/Version1004Date20170825134824.php',
'OCA\\DAV\\Migration\\Version1004Date20170919104507' => __DIR__ . '/..' . '/../lib/Migration/Version1004Date20170919104507.php',
@@ -288,31 +362,59 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Migration\\Version1016Date20201109085907' => __DIR__ . '/..' . '/../lib/Migration/Version1016Date20201109085907.php',
'OCA\\DAV\\Migration\\Version1017Date20210216083742' => __DIR__ . '/..' . '/../lib/Migration/Version1017Date20210216083742.php',
'OCA\\DAV\\Migration\\Version1018Date20210312100735' => __DIR__ . '/..' . '/../lib/Migration/Version1018Date20210312100735.php',
+ 'OCA\\DAV\\Migration\\Version1024Date20211221144219' => __DIR__ . '/..' . '/../lib/Migration/Version1024Date20211221144219.php',
+ 'OCA\\DAV\\Migration\\Version1025Date20240308063933' => __DIR__ . '/..' . '/../lib/Migration/Version1025Date20240308063933.php',
+ 'OCA\\DAV\\Migration\\Version1027Date20230504122946' => __DIR__ . '/..' . '/../lib/Migration/Version1027Date20230504122946.php',
+ 'OCA\\DAV\\Migration\\Version1029Date20221114151721' => __DIR__ . '/..' . '/../lib/Migration/Version1029Date20221114151721.php',
+ 'OCA\\DAV\\Migration\\Version1029Date20231004091403' => __DIR__ . '/..' . '/../lib/Migration/Version1029Date20231004091403.php',
+ 'OCA\\DAV\\Migration\\Version1030Date20240205103243' => __DIR__ . '/..' . '/../lib/Migration/Version1030Date20240205103243.php',
+ 'OCA\\DAV\\Migration\\Version1031Date20240610134258' => __DIR__ . '/..' . '/../lib/Migration/Version1031Date20240610134258.php',
+ 'OCA\\DAV\\Model\\ExampleEvent' => __DIR__ . '/..' . '/../lib/Model/ExampleEvent.php',
+ 'OCA\\DAV\\Paginate\\LimitedCopyIterator' => __DIR__ . '/..' . '/../lib/Paginate/LimitedCopyIterator.php',
+ 'OCA\\DAV\\Paginate\\PaginateCache' => __DIR__ . '/..' . '/../lib/Paginate/PaginateCache.php',
+ 'OCA\\DAV\\Paginate\\PaginatePlugin' => __DIR__ . '/..' . '/../lib/Paginate/PaginatePlugin.php',
'OCA\\DAV\\Profiler\\ProfilerPlugin' => __DIR__ . '/..' . '/../lib/Profiler/ProfilerPlugin.php',
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningNode.php',
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php',
+ 'OCA\\DAV\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php',
'OCA\\DAV\\RootCollection' => __DIR__ . '/..' . '/../lib/RootCollection.php',
'OCA\\DAV\\Search\\ACalendarSearchProvider' => __DIR__ . '/..' . '/../lib/Search/ACalendarSearchProvider.php',
'OCA\\DAV\\Search\\ContactsSearchProvider' => __DIR__ . '/..' . '/../lib/Search/ContactsSearchProvider.php',
'OCA\\DAV\\Search\\EventsSearchProvider' => __DIR__ . '/..' . '/../lib/Search/EventsSearchProvider.php',
'OCA\\DAV\\Search\\TasksSearchProvider' => __DIR__ . '/..' . '/../lib/Search/TasksSearchProvider.php',
'OCA\\DAV\\Server' => __DIR__ . '/..' . '/../lib/Server.php',
+ 'OCA\\DAV\\ServerFactory' => __DIR__ . '/..' . '/../lib/ServerFactory.php',
+ 'OCA\\DAV\\Service\\AbsenceService' => __DIR__ . '/..' . '/../lib/Service/AbsenceService.php',
+ 'OCA\\DAV\\Service\\ExampleContactService' => __DIR__ . '/..' . '/../lib/Service/ExampleContactService.php',
+ 'OCA\\DAV\\Service\\ExampleEventService' => __DIR__ . '/..' . '/../lib/Service/ExampleEventService.php',
+ 'OCA\\DAV\\Settings\\Admin\\SystemAddressBookSettings' => __DIR__ . '/..' . '/../lib/Settings/Admin/SystemAddressBookSettings.php',
'OCA\\DAV\\Settings\\AvailabilitySettings' => __DIR__ . '/..' . '/../lib/Settings/AvailabilitySettings.php',
'OCA\\DAV\\Settings\\CalDAVSettings' => __DIR__ . '/..' . '/../lib/Settings/CalDAVSettings.php',
+ 'OCA\\DAV\\Settings\\ExampleContentSettings' => __DIR__ . '/..' . '/../lib/Settings/ExampleContentSettings.php',
+ 'OCA\\DAV\\SetupChecks\\NeedsSystemAddressBookSync' => __DIR__ . '/..' . '/../lib/SetupChecks/NeedsSystemAddressBookSync.php',
+ 'OCA\\DAV\\SetupChecks\\WebdavEndpoint' => __DIR__ . '/..' . '/../lib/SetupChecks/WebdavEndpoint.php',
'OCA\\DAV\\Storage\\PublicOwnerWrapper' => __DIR__ . '/..' . '/../lib/Storage/PublicOwnerWrapper.php',
+ 'OCA\\DAV\\Storage\\PublicShareWrapper' => __DIR__ . '/..' . '/../lib/Storage/PublicShareWrapper.php',
+ 'OCA\\DAV\\SystemTag\\SystemTagList' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagList.php',
'OCA\\DAV\\SystemTag\\SystemTagMappingNode' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagMappingNode.php',
'OCA\\DAV\\SystemTag\\SystemTagNode' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagNode.php',
+ 'OCA\\DAV\\SystemTag\\SystemTagObjectType' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagObjectType.php',
'OCA\\DAV\\SystemTag\\SystemTagPlugin' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagPlugin.php',
'OCA\\DAV\\SystemTag\\SystemTagsByIdCollection' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagsByIdCollection.php',
+ 'OCA\\DAV\\SystemTag\\SystemTagsInUseCollection' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagsInUseCollection.php',
+ 'OCA\\DAV\\SystemTag\\SystemTagsObjectList' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagsObjectList.php',
'OCA\\DAV\\SystemTag\\SystemTagsObjectMappingCollection' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagsObjectMappingCollection.php',
'OCA\\DAV\\SystemTag\\SystemTagsObjectTypeCollection' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagsObjectTypeCollection.php',
'OCA\\DAV\\SystemTag\\SystemTagsRelationsCollection' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagsRelationsCollection.php',
'OCA\\DAV\\Traits\\PrincipalProxyTrait' => __DIR__ . '/..' . '/../lib/Traits/PrincipalProxyTrait.php',
'OCA\\DAV\\Upload\\AssemblyStream' => __DIR__ . '/..' . '/../lib/Upload/AssemblyStream.php',
'OCA\\DAV\\Upload\\ChunkingPlugin' => __DIR__ . '/..' . '/../lib/Upload/ChunkingPlugin.php',
+ 'OCA\\DAV\\Upload\\ChunkingV2Plugin' => __DIR__ . '/..' . '/../lib/Upload/ChunkingV2Plugin.php',
'OCA\\DAV\\Upload\\CleanupService' => __DIR__ . '/..' . '/../lib/Upload/CleanupService.php',
'OCA\\DAV\\Upload\\FutureFile' => __DIR__ . '/..' . '/../lib/Upload/FutureFile.php',
+ 'OCA\\DAV\\Upload\\PartFile' => __DIR__ . '/..' . '/../lib/Upload/PartFile.php',
'OCA\\DAV\\Upload\\RootCollection' => __DIR__ . '/..' . '/../lib/Upload/RootCollection.php',
+ 'OCA\\DAV\\Upload\\UploadAutoMkcolPlugin' => __DIR__ . '/..' . '/../lib/Upload/UploadAutoMkcolPlugin.php',
'OCA\\DAV\\Upload\\UploadFile' => __DIR__ . '/..' . '/../lib/Upload/UploadFile.php',
'OCA\\DAV\\Upload\\UploadFolder' => __DIR__ . '/..' . '/../lib/Upload/UploadFolder.php',
'OCA\\DAV\\Upload\\UploadHome' => __DIR__ . '/..' . '/../lib/Upload/UploadHome.php',
diff --git a/apps/dav/composer/composer/installed.php b/apps/dav/composer/composer/installed.php
index 628db5d793b..1a66c7f2416 100644
--- a/apps/dav/composer/composer/installed.php
+++ b/apps/dav/composer/composer/installed.php
@@ -1,22 +1,22 @@
<?php return array(
'root' => array(
+ 'name' => '__root__',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
+ 'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b',
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
- 'reference' => '9586920c0ec4016864a2219e838fb272127822d8',
- 'name' => '__root__',
'dev' => false,
),
'versions' => array(
'__root__' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
+ 'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b',
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
- 'reference' => '9586920c0ec4016864a2219e838fb272127822d8',
'dev_requirement' => false,
),
),
diff --git a/apps/dav/css/schedule-response.css b/apps/dav/css/schedule-response.css
index 789ea16df7a..85a03cb63e1 100644
--- a/apps/dav/css/schedule-response.css
+++ b/apps/dav/css/schedule-response.css
@@ -1,3 +1,7 @@
+/*
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
/* Database selector on install page */
form #selectPartStatForm {
text-align:center;
@@ -37,13 +41,13 @@ form #selectPartStatForm label {
}
form #selectPartStatForm label:first-of-type {
- border-top-left-radius: 4px;
- border-bottom-left-radius: 4px;
+ border-start-start-radius: 4px;
+ border-end-start-radius: 4px;
}
form #selectPartStatForm label:last-of-type {
- border-top-right-radius: 4px;
- border-bottom-right-radius: 4px;
+ border-start-end-radius: 4px;
+ border-end-end-radius: 4px;
}
form #selectPartStatForm label span {
@@ -52,6 +56,7 @@ form #selectPartStatForm label span {
display: block;
line-height: normal;
}
+
form #selectPartStatForm label.ui-state-hover,
form #selectPartStatForm label.ui-state-active {
color:#000;
@@ -75,4 +80,4 @@ form input[type="submit"] {
display: block;
margin: 0 auto;
padding: 11px 20px 9px
-} \ No newline at end of file
+}
diff --git a/apps/dav/img/calendar.svg b/apps/dav/img/calendar.svg
new file mode 100644
index 00000000000..5a4a2bca4ef
--- /dev/null
+++ b/apps/dav/img/calendar.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 16 16" width="16" height="16" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><path d="M5 22c-.55 0-1.021-.196-1.412-.587A1.927 1.927 0 0 1 3 20V6c0-.55.196-1.021.588-1.413A1.926 1.926 0 0 1 5 4h1V2h2v2h8V2h2v2h1c.55 0 1.021.196 1.413.587.391.392.587.863.587 1.413v14a1.93 1.93 0 0 1-.587 1.413A1.93 1.93 0 0 1 19 22H5Zm0-2h14V10H5v10Z" style="fill-rule:nonzero" transform="matrix(.7 0 0 .7 -.404 -.39)"/></svg> \ No newline at end of file
diff --git a/apps/dav/img/calendar.svg.license b/apps/dav/img/calendar.svg.license
new file mode 100644
index 00000000000..be55c910183
--- /dev/null
+++ b/apps/dav/img/calendar.svg.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2018-2024 Google LLC.
+SPDX-License-Identifier: Apache-2.0 \ No newline at end of file
diff --git a/apps/dav/img/schedule.svg b/apps/dav/img/schedule.svg
new file mode 100644
index 00000000000..23de1d12381
--- /dev/null
+++ b/apps/dav/img/schedule.svg
@@ -0,0 +1 @@
+<svg width="24px" height="24px" fill="#000000" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/><path d="M12.5 7H11v6l5.25 3.15.75-1.23-4.5-2.67z"/></svg>
diff --git a/apps/dav/img/schedule.svg.license b/apps/dav/img/schedule.svg.license
new file mode 100644
index 00000000000..be55c910183
--- /dev/null
+++ b/apps/dav/img/schedule.svg.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2018-2024 Google LLC.
+SPDX-License-Identifier: Apache-2.0 \ No newline at end of file
diff --git a/apps/dav/l10n/ar.js b/apps/dav/l10n/ar.js
new file mode 100644
index 00000000000..a058786dd54
--- /dev/null
+++ b/apps/dav/l10n/ar.js
@@ -0,0 +1,326 @@
+OC.L10N.register(
+ "dav",
+ {
+ "Calendar" : "التقويم",
+ "Tasks" : "المهام",
+ "Personal" : "خاص",
+ "{actor} created calendar {calendar}" : "{actor} قام بإنشاء تقويم {calendar}",
+ "You created calendar {calendar}" : "قمت بإنشاء التقويم {calendar}",
+ "{actor} deleted calendar {calendar}" : "{actor} قام بفسخ التقويم {calendar}",
+ "You deleted calendar {calendar}" : "لقد قمت بحذف التقويم {calendar}",
+ "{actor} updated calendar {calendar}" : "{actor} حدّث التقويم {calendar}",
+ "You updated calendar {calendar}" : "لقد قمت بتحديث التقويم {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} استعاد التقويم {calendar}",
+ "You restored calendar {calendar}" : "أنت استعدت التقويم {calendar}",
+ "You shared calendar {calendar} as public link" : "أنت شاركت التقويم {calendar} كرابطٍ عموميٍ public link",
+ "You removed public link for calendar {calendar}" : "أنت ألغيب الرابط العمومي للتقويم {calendar}",
+ "{actor} shared calendar {calendar} with you" : "{actor} قام بمشاركة التقويم {calendar} معك",
+ "You shared calendar {calendar} with {user}" : "لقد قمت بمشاركة التقويم {calendar} مع {user}",
+ "{actor} shared calendar {calendar} with {user}" : "{actor} قام بمشاركة التقويم {calendar} مع {user}",
+ "{actor} unshared calendar {calendar} from you" : "{actor} أزال مشاركة التقويم {calendar} منك",
+ "You unshared calendar {calendar} from {user}" : "لقد أزلت مشاركة التقويم {calendar} من {user}",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor} أزال مشاركة التقويم {calendar} من {user}",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} أزال مشاركة التقويم {calendar} من نفسه",
+ "You shared calendar {calendar} with group {group}" : "أنت شاركت التقويم {calendar} مع المجموعة {group}",
+ "{actor} shared calendar {calendar} with group {group}" : "{actor} شارك التقويم {calendar} مع المجموعة {group}",
+ "You unshared calendar {calendar} from group {group}" : "أنت أزلت مشاركة التقويم {calendar} من المجموعة {group}",
+ "{actor} unshared calendar {calendar} from group {group}" : "{actor} أزال مشاركة التقويم {calendar} من المجموعة {group}",
+ "Untitled event" : "حدث بدون اسم",
+ "{actor} created event {event} in calendar {calendar}" : "{actor} أنشأ الحدث {event} في التقويم {calendar}",
+ "You created event {event} in calendar {calendar}" : "أنت أنشأت الحدث {event} في التقويم {calendar}",
+ "{actor} deleted event {event} from calendar {calendar}" : "{actor} حذف الحدث {event} من التقويم {calendar}",
+ "You deleted event {event} from calendar {calendar}" : "أنت حذفت الحدث {event} من التقويم {calendar}",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} عدّل الحدث {event} في التقويم {calendar}",
+ "You updated event {event} in calendar {calendar}" : "أنت عدّلت الحدث {event} في التقويم {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} نقل الحدث {event} من التقويم {sourceCalendar} إلى التقويم {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "أنت نقلت الحدث {event} من التقويم {sourceCalendar} إلى التقويم {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} قام باستعادة الحدث {event} للتقويم {calendar}",
+ "You restored event {event} of calendar {calendar}" : "أنت قمت باستعادة الحدث {event} للتقويم {calendar}",
+ "Busy" : "مشغول",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} قائمة بإنشاء مهمة {todo} في القائمة {calendar}",
+ "You created to-do {todo} in list {calendar}" : "أنت قمت بإنشاء مهمة {todo} في القائمة {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} قام بحذف مهمة {todo} من القائمة {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "أنت قمت بحذف مهمة {todo} من القائمة {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} قام بتعديل مهمة {todo} في القائمة {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "أنت قمت بتعديل مهمة {todo} في القائمة {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} أنجز المهمة {todo} في القائمة{calendar}",
+ "You solved to-do {todo} in list {calendar}" : "أنت أنجزت المهمة {todo} في القائمة {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} أعاد فتح المهمة {todo} في القائمة {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "أنت أعدت فتح المهمة {todo} في القائمة {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} قام بنقل المهمة {todo} من القائمة {sourceCalendar} إلى القائمة {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "أنت قمت بنقل المهمة {todo} من القائمة {sourceCalendar} إلى القائمة {targetCalendar}",
+ "Calendar, contacts and tasks" : "التقويم، و جهات الاتصال، و قائمة المهام",
+ "A <strong>calendar</strong> was modified" : " <strong>تقويم</strong> تمّ تعديله",
+ "A calendar <strong>event</strong> was modified" : "<strong>حدث</strong> في تقويم تمّ تعديله",
+ "A calendar <strong>to-do</strong> was modified" : " <strong>مهمة</strong> في تقويم تمّ تعديلها",
+ "Contact birthdays" : "أعياد ميلاد جهات الاتصال",
+ "Death of %s" : "وفاة %s",
+ "Untitled calendar" : "تقويم بدون اسم",
+ "Calendar:" : "التقويم:",
+ "Date:" : "التاريخ:",
+ "Where:" : "المكان:",
+ "Description:" : "الوصف:",
+ "_%n year_::_%n years_" : ["في %n أعوام","في %nعام","في %nأعوام","في %nأعوام","في %n أعوام","في %nأعوام"],
+ "_%n month_::_%n months_" : [" %n شهور","%n شهر","%n شهور","%n شهور","%n شهور","%n شهور"],
+ "_%n day_::_%n days_" : ["%n أيام","%n يوم","%nأيام","%nأيام","%n أيام","%n أيام"],
+ "_%n hour_::_%n hours_" : ["%nساعات","%n ساعة","%n ساعات","%n ساعات","%n ساعات","%n ساعات"],
+ "_%n minute_::_%n minutes_" : ["%n دقيقة","%n دقيقة","%n دقيقتيْن","%n دقائق","%n دقائق","%n دقائق"],
+ "%s (in %s)" : "%s (في %s)",
+ "%s (%s ago)" : "%s (%s مضت)",
+ "Calendar: %s" : "التقويم: %s",
+ "Date: %s" : "التاريخ: %s",
+ "Description: %s" : "الوصف: %s",
+ "Where: %s" : "المكان: %s",
+ "%1$s via %2$s" : "%1$s عبر %2$s",
+ "In the past on %1$s for the entire day" : "في الماضي في %1$s لكامل اليوم",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["في %n دقيقة في %1$s لكامل اليوم","في %1$s for the entire day","في %n دقيقة في %1$s لكامل اليوم","في %n دقائق في %1$s لكامل اليوم","في %n دقيقة في %1$s لكامل اليوم","في %n دقيقة في %1$s لكامل اليوم"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["في %n ساعة في %1$s لكامل اليوم","في ساعة واحدة في %1$s لكامل اليوم","في %n ساعة في %1$s لكامل اليوم","في %n ساعات في %1$s لكامل اليوم","في %n ساعة في %1$s لكامل اليوم","في %n ساعة في %1$s لكل اليوم"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["في%n يوم في %1$s لكامل اليوم","في يوم واحد في %1$s لكامل اليوم","في %n يوم في %1$s لكامل اليوم","في %n أيام في %1$s لكامل ايوم","في %n يوم في %1$s لكامل اليوم","في %n يوم في %1$s لكامل اليوم"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["في %n أسبوع في %1$s طيلة اليوم","في أسبوع في %1$s طيلة اليوم","في %n أسبوع في %1$s طيلة اليوم","في %n أسابيع في %1$s طيلة اليوم","في %n أسبوع في %1$s طيلة اليوم","في %n أسبوع في %1$s طيلة اليوم"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["في %n شهر في %1$s طيلة اليوم","في شهر في %1$s طيلة اليوم","في %n شهر في %1$s طيلة اليوم","في %n أشهر في %1$s طيلة اليوم","في %n شهر في %1$s طيلة اليوم","في %n شهر في %1$s طيلة اليوم"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["في %n سنة في %1$s طيلة اليوم","في سنة في %1$s طيلة اليوم","في %n سنة في %1$s طيلة اليوم","في %n سنوات في %1$s طيلة اليوم","في %n سنة في %1$s طيلة اليوم","في %n سنة في %1$s طيلة اليوم"],
+ "In the past on %1$s between %2$s - %3$s" : "في الماضي في %1$s بين %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["في %n دقيقة في %1$s بين %2$s - %3$s","في دقيقة فقي %1$s بين %2$s - %3$s","في %n دقيقة فقي %1$s بين %2$s - %3$s","في %n دقائق في %1$s بين %2$s - %3$s","في %n دقيقة في %1$s بين %2$s - %3$s","في %n دقيقة في %1$s بين %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["في %n ساعة في %1$s بين %2$s - %3$s","في ساعة واحدة %1$s بين %2$s - %3$s","في %n ساعة في %1$s بين %2$s - %3$s","في %n ساعات في %1$s بين %2$s - %3$s","في %n ساعة في %1$s بين %2$s - %3$s","في %n ساعة في %1$s بين %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["في%n يوم في %1$s بين %2$s - %3$s","في يوم واحد في %1$s بين %2$s - %3$s","في %n يوم %1$s بين %2$s - %3$s","في %n أيام في %1$s بين %2$s - %3$s","في %n يوم في %1$s بين %2$s - %3$s","في %n يوم في %1$s بين %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["في %n أسبوع في %1$s بين %2$s - %3$s","في أسبوع واحد في %1$s بين %2$s - %3$s","في %n أسبوع في %1$s بين %2$s - %3$s","في %n أسابيع في %1$s في %2$s - %3$s","في %n أسبوع في %1$s بين %2$s - %3$s","في %n أسبوع في %1$s بين %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["في %n شهر في %1$s بين %2$s - %3$s","في شهر واحد في %1$s بين %2$s - %3$s","في %n شهر في %1$s بين %2$s - %3$s","في %n شهور في %1$s بين %2$s - %3$s","في %n شهر في %1$s بين %2$s - %3$s","In %n months on %1$s between %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["في %n سنة في %1$s بين %2$s - %3$s","في سنة واحدة في %1$s بين %2$s - %3$s","في %n سنة في %1$s بين %2$s - %3$s","في %n سنوات في %1$s بين %2$s - %3$s","في %n سنة في %1$s بين %2$s - %3$s","في %n سنة في %1$s بين %2$s - %3$s"],
+ "Could not generate when statement" : "يتعذّر تكوين عبارة \"متى\"",
+ "Every Day for the entire day" : "كل يوم لكامل اليوم",
+ "Every Day for the entire day until %1$s" : "كل يوم كامل اليوم حتى %1$s",
+ "Every Day between %1$s - %2$s" : "كل يوم بين%1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "كل يوم بين %1$s - %2$s حتى %3$s",
+ "Every %1$d Days for the entire day" : "كل أيام %1$d لكامل اليوم",
+ "Every %1$d Days for the entire day until %2$s" : "كل أيام %1$d كل اليوم حتى %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "كل أيام %1$d بين %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "كل أيام %1$d بين %2$s - %3$s حتى %4$s",
+ "Could not generate event recurrence statement" : "يتعذّر توليد عبارة تكرار الحدث",
+ "Every Week on %1$s for the entire day" : "كل أسبوع أيام %1$s كامل اليوم",
+ "Every Week on %1$s for the entire day until %2$s" : "كل أسبوع أيام %1$s كامل اليوم حتى %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "كل أسبوع أيام %1$s بين %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "كل أسبوع أيام %1$s بين %2$s - %3$s حتى %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "كل%1$d أسبوع أيام %2$s كامل اليوم",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "كل %1$d أسبوع أيام %2$s كامل اليوم حتى %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "كل %1$d أسبوع أيام %2$s بين %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "كل %1$d أسبوع %2$s بين %3$s - %4$s حتى %5$s",
+ "Every Month on the %1$s for the entire day" : "كل شهر أيام %1$s كامل اليوم",
+ "Every Month on the %1$s for the entire day until %2$s" : "كل شهر أيام %1$s كامل اليوم حتى %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "كل شهر أيام %1$s بين %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "كل شهر أيام %1$s بين %2$s - %3$s حتى %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "كل %1$d شهر أيام %2$s كامل اليوم",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "كل %1$d شهر أيام %2$s كامل اليوم حتى %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "كل %1$d شهر أيام %2$s بين %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "كل %1$d شهر أيام %2$s بين %3$s - %4$s حتى %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "كل سنة في %1$s أيام %2$s كامل اليوم",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "كل سنة في %1$s أيام %2$s كامل اليوم حتى %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "كل سنة في %1$s أيام %2$s بين %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "كل سنة في %1$s أيام %2$s بين %3$s - %4$s حتى %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "كل %1$d سنة في %2$s أيام %3$s كامل اليوم",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "كل %1$d سنة في %2$s أيام %3$s كامل اليوم حتى %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "كل %1$d سنة في %2$s أيام %3$s بين %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "كل %1$d سنة في %2$s أيام %3$s بين %4$s - %5$s حتى %6$s",
+ "On specific dates for the entire day until %1$s" : "في تورايخ محددة كامل اليوم حتى%1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "في تواريخ محددة بين %1$s - %2$s حتى %3$s",
+ "In the past on %1$s" : "في الماضي في %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["في %n دقيقة في %1$s","في دقيقة واحدة في %1$s","في %n دقيقة في %1$s","في %n دقائق في %1$s","في %n دقيقة في %1$s","في %n دقيقة في %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["في %n ساعة في %1$s","في ساعة واحدة في%1$s","في %n ساعة في %1$s","في %n ساعات في %1$s","في %n ساعة في %1$s","في %n ساعة في %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["في %n يوم في %1$s","في يوم واحد في%1$s","في %n يوم في %1$s","في %n أيام في %1$s","في %n يوم في%1$s","في %n يوم في %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["في %n أسبوع في %1$s","في أسبوع واحد في %1$s","في %n أسبوع في %1$s","في %n أسابيع في %1$s","في %n أسبوع في %1$s","في %n أسبوع في %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["في %n شهر في %1$s","في شهر واحد في %1$s","في %n شهر في %1$s","في %n أشهر في %1$s","في %n شهر في %1$s","في %n شهر في %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["في %n سنة في %1$s","في سنة واحدة في %1$s","في %n سنة في %1$s","في %n سنوات في %1$s","في %n سنة في %1$s","في %n سنة في %1$s"],
+ "In the past on %1$s then on %2$s" : "في الماضي في %1$s ثم في %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["في %n دقيقة في %1$s ثم في %2$s","في دقيقة واحدة في %1$s ثم في %2$s","في %n دقيقة في %1$s ثم في %2$s","فيب %n دقائق في %1$s ثم في %2$s","في %n دقيقة في %1$s ثم في %2$s","في %n دقيقة في %1$s ثم في %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["في %n ساعة في %1$s ثم في %2$s","في ساعة واحدة في %1$s ثم في %2$s","في %n ساعة في %1$s ثم في %2$s","في %n ساعات في %1$s ثم في %2$s","في %n ساعة في %1$s ثم في %2$s","في %n ساعة في %1$s ثم في %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["في %n يوم في %1$s ثم في %2$s","في يوم واحد في %1$s ثم في %2$s","في %n يوم في %1$s ثم في %2$s","في %n أيام في %1$s ثم في %2$s","في %n يوم في %1$s ثم في %2$s","في %n يوم في %1$s ثم في %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["في %n أسبوع في %1$s ثم في %2$s","في أسبوع واحد في %1$s ثم في %2$s","في %n أسبوع في %1$s ثم في %2$s","في %n أسابيع في %1$s ثم في %2$s","في %n أسبوع في %1$s ثم في %2$s","في %n أسبوع في %1$s ثم في %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["في %n شهر في %1$s ثم في %2$s","في شهر واحد في %1$s ثم في %2$s","في %n شهر في %1$s ثم في %2$s","في %n شهور في %1$s ثم في %2$s","في %n شهر في %1$sثم في %2$s","في %n شهر في %1$s ثم في %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["في %n سنة في %1$s ثم في %2$s","في سنة واحدة في %1$s ثم في %2$s","في %n سنة في %1$s ثم في %2$s","في %n سنوات في %1$s ثم في %2$s","في %n سنة في %1$s ثم في %2$s","في %n سنة في %1$s ثم في %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "في الماضي في %1$s ثم في %2$s و %3$s",
+ "_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_" : ["في %n دقيقة في %1$s ثم في %2$s و %3$s","في دقيقة واحدة في %1$s ثم في %2$s و %3$s","في %n دقيقة في %1$s ثم في %2$s و %3$s","في %n دقائق في %1$s ثم في %2$s و %3$s","في %n دقيقة في %1$s ثم في %2$s و %3$s","في %n دقيقة في %1$s ثم في %2$s و %3$s"],
+ "_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_" : ["في %n ساعة في %1$s ثم في %2$s و %3$s","في ساعة واحدة في %1$s ثم في %2$s و %3$s","في %n ساعة في %1$s ثم في %2$s و %3$s","في %n ساعات في %1$s ثم في %2$s و %3$s","في %n ساعة في %1$s ثم في %2$s و %3$s","في %n ساعة في %1$s ثم في %2$s و %3$s"],
+ "_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_" : ["في %n يوم في %1$s ثم في %2$s و %3$s","في يوم واحد في %1$s ثم ف %2$s و %3$s","في %n يوم في %1$s ثم في %2$s و %3$s","في %n أيام في %1$s ثم في %2$s و %3$s","في %n يوم في %1$s ثم في %2$s و %3$s","في %n يوم في %1$s ثم في %2$s و %3$s"],
+ "_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_" : ["في %n أسبوع في %1$s ثم في %2$s و %3$s","في أسبوع واحد في %1$s ثم في %2$s و %3$s","في %n أسبوع في %1$s ثم في %2$s و %3$s","في %n أسابيع في %1$s ثم في %2$s و %3$s","في %n أسبوع في %1$s ثم في %2$s و %3$s","في %n أسبوع في %1$s ثم في %2$s و %3$s"],
+ "_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_" : ["في %n شهر في %1$s ثم في %2$s و %3$s","في شهر واحد في %1$s ثم في %2$s و %3$s","في %n شهر في %1$s ثم في %2$s و %3$s","في %n شهور في %1$s ثم في %2$s و %3$s","في %n شهر في %1$s ثم في %2$s و %3$s","في %n شهر في %1$s ثم في %2$s و %3$s"],
+ "_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_" : ["في%n سنة في %1$s ثم في %2$s و %3$s","في سنة واحدة في %1$s ثم في %2$s و %3$s","في %n سنة في %1$s ثم في %2$s و %3$s","في %n سنوات في %1$s ثم في %2$s و %3$s","في %n سنة في %1$s ثم في %2$s و %3$s","في %n سنة في %1$s ثم في %2$s و %3$s"],
+ "Could not generate next recurrence statement" : "يتعذّر توليد عبارة التكرار التالي",
+ "Cancelled: %1$s" : "مُلغىً: %1$s",
+ "\"%1$s\" has been canceled" : "\"%1$s\" تمّ إلغاؤه",
+ "Re: %1$s" : "إعادة: %1$s",
+ "%1$s has accepted your invitation" : "%1$s قبل دعوتك",
+ "%1$s has tentatively accepted your invitation" : "%1$s قبل دعوتك بشكل مبدئي",
+ "%1$s has declined your invitation" : "%1$s لم يقبل دعوتك",
+ "%1$s has responded to your invitation" : "%1$s استجاب لدعوتك",
+ "Invitation updated: %1$s" : "تحديث الدعوة: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s تحديث الحدث \"%2$s\"",
+ "Invitation: %1$s" : "دعوة: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s يرغب في دعوتكم إلى \"%2$s\"",
+ "Organizer:" : "تنظيم:",
+ "Attendees:" : "الحُضُور:",
+ "Title:" : "العنوان:",
+ "When:" : "متى:",
+ "Location:" : "المكان :",
+ "Link:" : "الرابط:",
+ "Occurring:" : "التكرار:",
+ "Accept" : "قبول",
+ "Decline" : "رفض",
+ "More options …" : "مزيد مِن الخيارات…",
+ "More options at %s" : "خيارات أخرى في %s",
+ "Monday" : "الإثنين",
+ "Tuesday" : "الثلاثاء",
+ "Wednesday" : "الأربعاء",
+ "Thursday" : "الخميس",
+ "Friday" : "الجمعة",
+ "Saturday" : "السبت",
+ "Sunday" : "الأحد",
+ "January" : "يناير",
+ "February" : "فبراير",
+ "March" : "مارس",
+ "April" : "أبريل",
+ "May" : "مايو",
+ "June" : "يونيو",
+ "July" : "يوليو",
+ "August" : "اغسطس",
+ "September" : "سبتمبر",
+ "October" : "أكتوبر",
+ "November" : "نوفمبر",
+ "December" : "ديسمبر",
+ "First" : "أول",
+ "Second" : "ثاني",
+ "Third" : "ثالث",
+ "Fourth" : "رابع",
+ "Fifth" : "خامس",
+ "Last" : "آخِر",
+ "Second Last" : "ما قبل الأخير",
+ "Third Last" : "الثالث من الآخِر",
+ "Fourth Last" : "الرابع من الآخِر",
+ "Fifth Last" : "خامس من الأخير",
+ "Contacts" : "جهات الاتصال",
+ "{actor} created address book {addressbook}" : "{actor} أنشأ دفتر العناوين {addressbook}",
+ "You created address book {addressbook}" : "أنت أنشأت دفتر العناوين {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} قام بحذف دفتر العناوين {addressbook}",
+ "You deleted address book {addressbook}" : "أنت قمت بحذف دفتر العناوين {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} قام بتعديل دفتر العناوين {addressbook}",
+ "You updated address book {addressbook}" : "أنت قمت بتعديل دفتر العناوين {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} قام بمشاركة دفتر العناوين {addressbook} معك",
+ "You shared address book {addressbook} with {user}" : "أنت قمت بمشاركة دفتر العناوين {addressbook} مع {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} قام بمشاركة دفتر العناوين {addressbook} مع {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} قام بإلغاء مشاركة دفتر العناوين {addressbook} معك",
+ "You unshared address book {addressbook} from {user}" : "أنت قمت بإلغاء مشاركة دفتر العناوين {addressbook} مع {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} قام بإلغاء مشاركة دفتر العناوين {addressbook} مع {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} قام بإلغاء مشاركته في دفتر العناوين {addressbook} ",
+ "You shared address book {addressbook} with group {group}" : "أنت قمت بمشاركة دفتر العناوين {addressbook} مع المجموعة {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} قام بمشاركة دفتر العناوين {addressbook} مع المجموعة {group}",
+ "You unshared address book {addressbook} from group {group}" : "أنت قمت بإلغاء مشاركة دفتر العناوين {addressbook} مع المجموعة {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} قام بإلغاء مشاركة دفتر العناوين {addressbook} مع المجموعة {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} قام بإنشاء جهة اتصال {card} في دفتر العناوين {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "أنت قمت بإنشاء جهة اتصال {card} في دفتر العناوين {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} قام بحذف جهة الاتصال {card} من دفتر العناوين {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "أنت قمت بحذف جهة الاتصال {card} من دفتر العناوين {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} قام بتحديث جهة الاتصال {card} في دفتر العناوين {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "أنت قمت بتحديث جهة الاتصال {card} في دفتر العناوين {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "تمّ تعديل <strong>جهة الاتصال</strong> أو <strong>دفتر العناوين</strong> ",
+ "Accounts" : "الحسابات",
+ "System address book which holds all accounts" : "دفتر عناوين النظام الذي يحتوي على جميع الحسابات",
+ "File is not updatable: %1$s" : "ملف غير قابل للتعديل: %1$s",
+ "Failed to get storage for file" : "تعذّر الحصول على مكان لتخزين الملف",
+ "Could not write to final file, canceled by hook" : "تعذرت الكتابة إلى الملف النهائي، تم إلغاؤه بواسطة خطّاف hook",
+ "Could not write file contents" : "تعذرت كتابة محتويات الملف",
+ "_%n byte_::_%n bytes_" : ["بايت","بايت","بايت","بايت","بايت","%n بايت"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "حدث خطأ أثناء نسخ الملف إلى الموقع الهدف (تمّ نسخه: %1$s, حجم الملف المتوقع: %2$s)",
+ "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." : "الحجم المتوقع للملف هو %1$s؛ بينما المقروء من ( الجهاز العميل لنكست كلاود) و المكتوب في (تخزين نكست كلاود) هو %2$s. يمكن أن يكون السبب إمّا مشكلة اتصال في جانب الجهاز العميل أو مشكلة في الكتابة في وحدة التخزين في جانب خادم نكست كلاود .",
+ "Could not rename part file to final file, canceled by hook" : "تعذّرت إعادة تسمية ملف جزئي إلى ملف نهائي. تمّ الإلغاء من قِبَل الخطّاف hook.",
+ "Could not rename part file to final file" : "تعذّرت إعادة تسمية ملف جزئي إلى ملف نهائي",
+ "Failed to check file size: %1$s" : "فشل في تحديد حجم الملف: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "يتعذّر فتح الملف: %1$s, يبدو أن الملف غير موجود",
+ "Could not open file: %1$s, file doesn't seem to exist" : "يتعذّر فتح الملف: %1$s, يبدو أن الملف غير موجود",
+ "Encryption not ready: %1$s" : "التشفير غير جاهز: %1$s",
+ "Failed to open file: %1$s" : "تعذّر فتح الملف: %1$s",
+ "Failed to unlink: %1$s" : "تعذّر فك الارتباط: %1$s",
+ "Failed to write file contents: %1$s" : "فشل في كتابة محتويات الملف: %1$s",
+ "File not found: %1$s" : "ملف غير موجود: %1$s",
+ "Invalid target path" : "المسار الهدف غير صحيح",
+ "System is in maintenance mode." : "النظام في حالة صيانة.",
+ "Upgrade needed" : "يجب التحديث",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "%s الخاص بك يجب تهيئته لاستخدام HTTPS حتى يمكن استعمال CalDAV و CardDAV في نظام التشغيل iOS/macOS. ",
+ "Configures a CalDAV account" : "تكوين حساب CalDAV",
+ "Configures a CardDAV account" : "تكوين حساب CardDAV",
+ "Events" : "أحداث",
+ "Untitled task" : "مهمة بدون اسم",
+ "Completed on %s" : "تمّ الانتهاء منه في %s",
+ "Due on %s by %s" : "مطلوبٌ في %s من قِبَل%s",
+ "Due on %s" : "مطلوبٌ في %s",
+ "System Address Book" : "دفتر عناوين النظام",
+ "The system address book contains contact information for all users in your instance." : "دفتر عناوين النظام يحتوي على معلومات الاتصال لجميع المستخدِمين على خادومك. ",
+ "Enable System Address Book" : "تمكين دفتر عناوين النظام",
+ "DAV system address book" : "دفتر عناوين نظام DAV",
+ "No outstanding DAV system address book sync." : "لا توجد أي مزامنات معلقة لدفتر عناوني نظام DAV.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "لم يتم تشغيل مزامنة دفتر عناوين نظام DAV حتى الآن بسبب أن الخادم الخاص بك يحتوي على أكثر من 1000 مستخدم أو بسبب حدوث خطأ. يرجى تشغيل المزامنة يدويًا عن طريق الأمر السطري:\n\"occ dav:sync-system-addressbook\"",
+ "WebDAV endpoint" : "النقطة النهائية endpoint لـ WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "تعذر التحقُّق من إعداد خادم الويب عندك بالشكل الصحيح للسماح بمزامنة الملفات عبر WebDAV. يرجى التحقُّق يدوياً.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : " لم يتم تعيين السماح لخادمك السحابي بتزامن الملف، بسبب واجهة التأليف الموزع على الويب وتعيين الإصدار WebDAV غير متصلة.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "خادم الويب عندك مُهيّءٌ بالشكل الصحيح للسماح بمزامنة الملفات عبر WebDAV.",
+ "Migrated calendar (%1$s)" : "تقويم مُرحّل (%1$s)",
+ "Calendars including events, details and attendees" : "تحوي التقاويم الأحداث، و تفاصيلها، و الحُضُور",
+ "Contacts and groups" : "جهات الاتصال والمجموعات",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "تمّ حفظ الغياب",
+ "Failed to save your absence settings" : "تعذّر حفظ إعداداتك للغياب",
+ "Absence cleared" : "تمّ محو الغياب",
+ "Failed to clear your absence settings" : "تعذّر محو إعداداتك للغياب",
+ "First day" : "أول يوم",
+ "Last day (inclusive)" : "آخر يوم (متضمن)",
+ "Out of office replacement (optional)" : "البديل لمن هو خارج المكتب (إختياري)",
+ "Name of the replacement" : "اسم البديل",
+ "No results." : "لا نتائج",
+ "Start typing." : "أبدا الكتابة",
+ "Short absence status" : "حالة الغياب القصير",
+ "Long absence Message" : "رسالة الغياب الطويل",
+ "Save" : "حفظ",
+ "Disable absence" : "تعطيل الغياب",
+ "Failed to load availability" : "فشل في تحميل أوقات التواجد",
+ "Saved availability" : "تمّ حفظ أوقات التواجد",
+ "Failed to save availability" : "تعذّر حفظ أوقات التواجد",
+ "Time zone:" : "منطقة زمنية:",
+ "to" : "إلى",
+ "Delete slot" : "حذف الخانة الزمنية",
+ "No working hours set" : "لم يتم تحديد ساعات العمل",
+ "Add slot" : "إضافة خانة زمنية",
+ "Weekdays" : "أيام الأسبوع",
+ "Pick a start time for {dayName}" : "إختَر وقت البدء ليوم {dayName}",
+ "Pick a end time for {dayName}" : "إختَر وقت الانتهاء ليوم {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "قم بتعيين حالة المستخدم تلقائيًا على \"عدم الإزعاج\" خارج نطاق أوقات التواجد لكتم جميع الإشعارات",
+ "Cancel" : "إلغاء",
+ "Import" : "إستيراد",
+ "Error while saving settings" : "خطأ أثناء حفظ الإعدادات",
+ "Contact reset successfully" : "تمّت إعادة تعيين جهة الاتصال بنجاحٍ",
+ "Error while resetting contact" : "خطأ أثناء إعادة تعيين جهة الاتصال",
+ "Contact imported successfully" : "تمّ استيراد جهة الاتصال بنجاحٍ",
+ "Error while importing contact" : "خطأ أثناء استيراد جهة الاتصال",
+ "Import contact" : "استيراد جهة اتصال",
+ "Reset to default" : "اعادة تعيين الافتراضيات",
+ "Import contacts" : "استيراد جهات اتصال",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "استيراد ملف .cvf جديد سوف يؤدي إلى حذف جهات الاتصال التلقائية الحالية واستبدالها بالجديدة. هل ترغب في الاستمرار؟",
+ "Availability" : "أوقات التواجد ",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "إذا قمت بضبط ساعات عملك، سيرى الآخرون متى تكون خارج المكتب عندما يقومون بحجز اجتماع معك.",
+ "Absence" : "غياب",
+ "Configure your next absence period." : "تهيئة فترة غيابك القادمة.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "قم أيضاً بتنصيب {calendarappstoreopen} تطبيق التقويم {linkclose}, أو {calendardocopen} أوصل جهازك و موبايلك للمُزامنة ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "رجاءُ، تأكّد من الإعداد الصحيح لـ {emailopen} خادم البريد الالكتروني {linkclose}.",
+ "Calendar server" : "خادم التقويم",
+ "Send invitations to attendees" : "إرسال دعوات للمطلوب حضورهم",
+ "Automatically generate a birthday calendar" : "تجاهل تقويم أعياد الميلاد تلقائيّاً",
+ "Birthday calendars will be generated by a background job." : "تقويم أعياد الميلاد سيتم توليده من قِبَل مهمةٍ في الخلفية.",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "ومن ثمّ لن تكون متاحةً فور التفعيل بل ستظهر بعد مرور بعض الوقت.",
+ "Send notifications for events" : "إرسال إشعارات حول الأحداث",
+ "Notifications are sent via background jobs, so these must occur often enough." : "يتمّ إرسال الإشعارات من قِبَل مهمّةٍ في الخلفية. لذا سيتكرر عمل هذا المهام حسب الحاجة.",
+ "Send reminder notifications to calendar sharees as well" : "أرسل إشعارات للتذكير إلى المشتركين بالتقويم كذلك",
+ "Reminders are always sent to organizers and attendees." : "إشعارات التذكير يتم إرسالها دائماً إلى مُنظّم أو مُنظّمي الحدث و المستهدفين بحضوره.",
+ "Enable notifications for events via push" : "تمكين الإشعارات حول الأحداث عن طريق أسلوب دفع الإشعارات Push",
+ "There was an error updating your attendance status." : "حدث خطأ في تحديث حالة حضورك.",
+ "Please contact the organizer directly." : "يرجى الاتصال بالمنظم مباشرةً",
+ "Are you accepting the invitation?" : "هل تقبل الدعوة؟",
+ "Tentative" : "مبدئي",
+ "Your attendance was updated successfully." : "حضورك تم تحديثه بنجاحٍ"
+},
+"nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;");
diff --git a/apps/dav/l10n/ar.json b/apps/dav/l10n/ar.json
new file mode 100644
index 00000000000..212dc54ee03
--- /dev/null
+++ b/apps/dav/l10n/ar.json
@@ -0,0 +1,324 @@
+{ "translations": {
+ "Calendar" : "التقويم",
+ "Tasks" : "المهام",
+ "Personal" : "خاص",
+ "{actor} created calendar {calendar}" : "{actor} قام بإنشاء تقويم {calendar}",
+ "You created calendar {calendar}" : "قمت بإنشاء التقويم {calendar}",
+ "{actor} deleted calendar {calendar}" : "{actor} قام بفسخ التقويم {calendar}",
+ "You deleted calendar {calendar}" : "لقد قمت بحذف التقويم {calendar}",
+ "{actor} updated calendar {calendar}" : "{actor} حدّث التقويم {calendar}",
+ "You updated calendar {calendar}" : "لقد قمت بتحديث التقويم {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} استعاد التقويم {calendar}",
+ "You restored calendar {calendar}" : "أنت استعدت التقويم {calendar}",
+ "You shared calendar {calendar} as public link" : "أنت شاركت التقويم {calendar} كرابطٍ عموميٍ public link",
+ "You removed public link for calendar {calendar}" : "أنت ألغيب الرابط العمومي للتقويم {calendar}",
+ "{actor} shared calendar {calendar} with you" : "{actor} قام بمشاركة التقويم {calendar} معك",
+ "You shared calendar {calendar} with {user}" : "لقد قمت بمشاركة التقويم {calendar} مع {user}",
+ "{actor} shared calendar {calendar} with {user}" : "{actor} قام بمشاركة التقويم {calendar} مع {user}",
+ "{actor} unshared calendar {calendar} from you" : "{actor} أزال مشاركة التقويم {calendar} منك",
+ "You unshared calendar {calendar} from {user}" : "لقد أزلت مشاركة التقويم {calendar} من {user}",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor} أزال مشاركة التقويم {calendar} من {user}",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} أزال مشاركة التقويم {calendar} من نفسه",
+ "You shared calendar {calendar} with group {group}" : "أنت شاركت التقويم {calendar} مع المجموعة {group}",
+ "{actor} shared calendar {calendar} with group {group}" : "{actor} شارك التقويم {calendar} مع المجموعة {group}",
+ "You unshared calendar {calendar} from group {group}" : "أنت أزلت مشاركة التقويم {calendar} من المجموعة {group}",
+ "{actor} unshared calendar {calendar} from group {group}" : "{actor} أزال مشاركة التقويم {calendar} من المجموعة {group}",
+ "Untitled event" : "حدث بدون اسم",
+ "{actor} created event {event} in calendar {calendar}" : "{actor} أنشأ الحدث {event} في التقويم {calendar}",
+ "You created event {event} in calendar {calendar}" : "أنت أنشأت الحدث {event} في التقويم {calendar}",
+ "{actor} deleted event {event} from calendar {calendar}" : "{actor} حذف الحدث {event} من التقويم {calendar}",
+ "You deleted event {event} from calendar {calendar}" : "أنت حذفت الحدث {event} من التقويم {calendar}",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} عدّل الحدث {event} في التقويم {calendar}",
+ "You updated event {event} in calendar {calendar}" : "أنت عدّلت الحدث {event} في التقويم {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} نقل الحدث {event} من التقويم {sourceCalendar} إلى التقويم {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "أنت نقلت الحدث {event} من التقويم {sourceCalendar} إلى التقويم {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} قام باستعادة الحدث {event} للتقويم {calendar}",
+ "You restored event {event} of calendar {calendar}" : "أنت قمت باستعادة الحدث {event} للتقويم {calendar}",
+ "Busy" : "مشغول",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} قائمة بإنشاء مهمة {todo} في القائمة {calendar}",
+ "You created to-do {todo} in list {calendar}" : "أنت قمت بإنشاء مهمة {todo} في القائمة {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} قام بحذف مهمة {todo} من القائمة {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "أنت قمت بحذف مهمة {todo} من القائمة {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} قام بتعديل مهمة {todo} في القائمة {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "أنت قمت بتعديل مهمة {todo} في القائمة {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} أنجز المهمة {todo} في القائمة{calendar}",
+ "You solved to-do {todo} in list {calendar}" : "أنت أنجزت المهمة {todo} في القائمة {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} أعاد فتح المهمة {todo} في القائمة {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "أنت أعدت فتح المهمة {todo} في القائمة {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} قام بنقل المهمة {todo} من القائمة {sourceCalendar} إلى القائمة {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "أنت قمت بنقل المهمة {todo} من القائمة {sourceCalendar} إلى القائمة {targetCalendar}",
+ "Calendar, contacts and tasks" : "التقويم، و جهات الاتصال، و قائمة المهام",
+ "A <strong>calendar</strong> was modified" : " <strong>تقويم</strong> تمّ تعديله",
+ "A calendar <strong>event</strong> was modified" : "<strong>حدث</strong> في تقويم تمّ تعديله",
+ "A calendar <strong>to-do</strong> was modified" : " <strong>مهمة</strong> في تقويم تمّ تعديلها",
+ "Contact birthdays" : "أعياد ميلاد جهات الاتصال",
+ "Death of %s" : "وفاة %s",
+ "Untitled calendar" : "تقويم بدون اسم",
+ "Calendar:" : "التقويم:",
+ "Date:" : "التاريخ:",
+ "Where:" : "المكان:",
+ "Description:" : "الوصف:",
+ "_%n year_::_%n years_" : ["في %n أعوام","في %nعام","في %nأعوام","في %nأعوام","في %n أعوام","في %nأعوام"],
+ "_%n month_::_%n months_" : [" %n شهور","%n شهر","%n شهور","%n شهور","%n شهور","%n شهور"],
+ "_%n day_::_%n days_" : ["%n أيام","%n يوم","%nأيام","%nأيام","%n أيام","%n أيام"],
+ "_%n hour_::_%n hours_" : ["%nساعات","%n ساعة","%n ساعات","%n ساعات","%n ساعات","%n ساعات"],
+ "_%n minute_::_%n minutes_" : ["%n دقيقة","%n دقيقة","%n دقيقتيْن","%n دقائق","%n دقائق","%n دقائق"],
+ "%s (in %s)" : "%s (في %s)",
+ "%s (%s ago)" : "%s (%s مضت)",
+ "Calendar: %s" : "التقويم: %s",
+ "Date: %s" : "التاريخ: %s",
+ "Description: %s" : "الوصف: %s",
+ "Where: %s" : "المكان: %s",
+ "%1$s via %2$s" : "%1$s عبر %2$s",
+ "In the past on %1$s for the entire day" : "في الماضي في %1$s لكامل اليوم",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["في %n دقيقة في %1$s لكامل اليوم","في %1$s for the entire day","في %n دقيقة في %1$s لكامل اليوم","في %n دقائق في %1$s لكامل اليوم","في %n دقيقة في %1$s لكامل اليوم","في %n دقيقة في %1$s لكامل اليوم"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["في %n ساعة في %1$s لكامل اليوم","في ساعة واحدة في %1$s لكامل اليوم","في %n ساعة في %1$s لكامل اليوم","في %n ساعات في %1$s لكامل اليوم","في %n ساعة في %1$s لكامل اليوم","في %n ساعة في %1$s لكل اليوم"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["في%n يوم في %1$s لكامل اليوم","في يوم واحد في %1$s لكامل اليوم","في %n يوم في %1$s لكامل اليوم","في %n أيام في %1$s لكامل ايوم","في %n يوم في %1$s لكامل اليوم","في %n يوم في %1$s لكامل اليوم"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["في %n أسبوع في %1$s طيلة اليوم","في أسبوع في %1$s طيلة اليوم","في %n أسبوع في %1$s طيلة اليوم","في %n أسابيع في %1$s طيلة اليوم","في %n أسبوع في %1$s طيلة اليوم","في %n أسبوع في %1$s طيلة اليوم"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["في %n شهر في %1$s طيلة اليوم","في شهر في %1$s طيلة اليوم","في %n شهر في %1$s طيلة اليوم","في %n أشهر في %1$s طيلة اليوم","في %n شهر في %1$s طيلة اليوم","في %n شهر في %1$s طيلة اليوم"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["في %n سنة في %1$s طيلة اليوم","في سنة في %1$s طيلة اليوم","في %n سنة في %1$s طيلة اليوم","في %n سنوات في %1$s طيلة اليوم","في %n سنة في %1$s طيلة اليوم","في %n سنة في %1$s طيلة اليوم"],
+ "In the past on %1$s between %2$s - %3$s" : "في الماضي في %1$s بين %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["في %n دقيقة في %1$s بين %2$s - %3$s","في دقيقة فقي %1$s بين %2$s - %3$s","في %n دقيقة فقي %1$s بين %2$s - %3$s","في %n دقائق في %1$s بين %2$s - %3$s","في %n دقيقة في %1$s بين %2$s - %3$s","في %n دقيقة في %1$s بين %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["في %n ساعة في %1$s بين %2$s - %3$s","في ساعة واحدة %1$s بين %2$s - %3$s","في %n ساعة في %1$s بين %2$s - %3$s","في %n ساعات في %1$s بين %2$s - %3$s","في %n ساعة في %1$s بين %2$s - %3$s","في %n ساعة في %1$s بين %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["في%n يوم في %1$s بين %2$s - %3$s","في يوم واحد في %1$s بين %2$s - %3$s","في %n يوم %1$s بين %2$s - %3$s","في %n أيام في %1$s بين %2$s - %3$s","في %n يوم في %1$s بين %2$s - %3$s","في %n يوم في %1$s بين %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["في %n أسبوع في %1$s بين %2$s - %3$s","في أسبوع واحد في %1$s بين %2$s - %3$s","في %n أسبوع في %1$s بين %2$s - %3$s","في %n أسابيع في %1$s في %2$s - %3$s","في %n أسبوع في %1$s بين %2$s - %3$s","في %n أسبوع في %1$s بين %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["في %n شهر في %1$s بين %2$s - %3$s","في شهر واحد في %1$s بين %2$s - %3$s","في %n شهر في %1$s بين %2$s - %3$s","في %n شهور في %1$s بين %2$s - %3$s","في %n شهر في %1$s بين %2$s - %3$s","In %n months on %1$s between %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["في %n سنة في %1$s بين %2$s - %3$s","في سنة واحدة في %1$s بين %2$s - %3$s","في %n سنة في %1$s بين %2$s - %3$s","في %n سنوات في %1$s بين %2$s - %3$s","في %n سنة في %1$s بين %2$s - %3$s","في %n سنة في %1$s بين %2$s - %3$s"],
+ "Could not generate when statement" : "يتعذّر تكوين عبارة \"متى\"",
+ "Every Day for the entire day" : "كل يوم لكامل اليوم",
+ "Every Day for the entire day until %1$s" : "كل يوم كامل اليوم حتى %1$s",
+ "Every Day between %1$s - %2$s" : "كل يوم بين%1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "كل يوم بين %1$s - %2$s حتى %3$s",
+ "Every %1$d Days for the entire day" : "كل أيام %1$d لكامل اليوم",
+ "Every %1$d Days for the entire day until %2$s" : "كل أيام %1$d كل اليوم حتى %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "كل أيام %1$d بين %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "كل أيام %1$d بين %2$s - %3$s حتى %4$s",
+ "Could not generate event recurrence statement" : "يتعذّر توليد عبارة تكرار الحدث",
+ "Every Week on %1$s for the entire day" : "كل أسبوع أيام %1$s كامل اليوم",
+ "Every Week on %1$s for the entire day until %2$s" : "كل أسبوع أيام %1$s كامل اليوم حتى %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "كل أسبوع أيام %1$s بين %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "كل أسبوع أيام %1$s بين %2$s - %3$s حتى %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "كل%1$d أسبوع أيام %2$s كامل اليوم",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "كل %1$d أسبوع أيام %2$s كامل اليوم حتى %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "كل %1$d أسبوع أيام %2$s بين %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "كل %1$d أسبوع %2$s بين %3$s - %4$s حتى %5$s",
+ "Every Month on the %1$s for the entire day" : "كل شهر أيام %1$s كامل اليوم",
+ "Every Month on the %1$s for the entire day until %2$s" : "كل شهر أيام %1$s كامل اليوم حتى %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "كل شهر أيام %1$s بين %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "كل شهر أيام %1$s بين %2$s - %3$s حتى %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "كل %1$d شهر أيام %2$s كامل اليوم",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "كل %1$d شهر أيام %2$s كامل اليوم حتى %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "كل %1$d شهر أيام %2$s بين %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "كل %1$d شهر أيام %2$s بين %3$s - %4$s حتى %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "كل سنة في %1$s أيام %2$s كامل اليوم",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "كل سنة في %1$s أيام %2$s كامل اليوم حتى %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "كل سنة في %1$s أيام %2$s بين %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "كل سنة في %1$s أيام %2$s بين %3$s - %4$s حتى %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "كل %1$d سنة في %2$s أيام %3$s كامل اليوم",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "كل %1$d سنة في %2$s أيام %3$s كامل اليوم حتى %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "كل %1$d سنة في %2$s أيام %3$s بين %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "كل %1$d سنة في %2$s أيام %3$s بين %4$s - %5$s حتى %6$s",
+ "On specific dates for the entire day until %1$s" : "في تورايخ محددة كامل اليوم حتى%1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "في تواريخ محددة بين %1$s - %2$s حتى %3$s",
+ "In the past on %1$s" : "في الماضي في %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["في %n دقيقة في %1$s","في دقيقة واحدة في %1$s","في %n دقيقة في %1$s","في %n دقائق في %1$s","في %n دقيقة في %1$s","في %n دقيقة في %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["في %n ساعة في %1$s","في ساعة واحدة في%1$s","في %n ساعة في %1$s","في %n ساعات في %1$s","في %n ساعة في %1$s","في %n ساعة في %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["في %n يوم في %1$s","في يوم واحد في%1$s","في %n يوم في %1$s","في %n أيام في %1$s","في %n يوم في%1$s","في %n يوم في %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["في %n أسبوع في %1$s","في أسبوع واحد في %1$s","في %n أسبوع في %1$s","في %n أسابيع في %1$s","في %n أسبوع في %1$s","في %n أسبوع في %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["في %n شهر في %1$s","في شهر واحد في %1$s","في %n شهر في %1$s","في %n أشهر في %1$s","في %n شهر في %1$s","في %n شهر في %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["في %n سنة في %1$s","في سنة واحدة في %1$s","في %n سنة في %1$s","في %n سنوات في %1$s","في %n سنة في %1$s","في %n سنة في %1$s"],
+ "In the past on %1$s then on %2$s" : "في الماضي في %1$s ثم في %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["في %n دقيقة في %1$s ثم في %2$s","في دقيقة واحدة في %1$s ثم في %2$s","في %n دقيقة في %1$s ثم في %2$s","فيب %n دقائق في %1$s ثم في %2$s","في %n دقيقة في %1$s ثم في %2$s","في %n دقيقة في %1$s ثم في %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["في %n ساعة في %1$s ثم في %2$s","في ساعة واحدة في %1$s ثم في %2$s","في %n ساعة في %1$s ثم في %2$s","في %n ساعات في %1$s ثم في %2$s","في %n ساعة في %1$s ثم في %2$s","في %n ساعة في %1$s ثم في %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["في %n يوم في %1$s ثم في %2$s","في يوم واحد في %1$s ثم في %2$s","في %n يوم في %1$s ثم في %2$s","في %n أيام في %1$s ثم في %2$s","في %n يوم في %1$s ثم في %2$s","في %n يوم في %1$s ثم في %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["في %n أسبوع في %1$s ثم في %2$s","في أسبوع واحد في %1$s ثم في %2$s","في %n أسبوع في %1$s ثم في %2$s","في %n أسابيع في %1$s ثم في %2$s","في %n أسبوع في %1$s ثم في %2$s","في %n أسبوع في %1$s ثم في %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["في %n شهر في %1$s ثم في %2$s","في شهر واحد في %1$s ثم في %2$s","في %n شهر في %1$s ثم في %2$s","في %n شهور في %1$s ثم في %2$s","في %n شهر في %1$sثم في %2$s","في %n شهر في %1$s ثم في %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["في %n سنة في %1$s ثم في %2$s","في سنة واحدة في %1$s ثم في %2$s","في %n سنة في %1$s ثم في %2$s","في %n سنوات في %1$s ثم في %2$s","في %n سنة في %1$s ثم في %2$s","في %n سنة في %1$s ثم في %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "في الماضي في %1$s ثم في %2$s و %3$s",
+ "_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_" : ["في %n دقيقة في %1$s ثم في %2$s و %3$s","في دقيقة واحدة في %1$s ثم في %2$s و %3$s","في %n دقيقة في %1$s ثم في %2$s و %3$s","في %n دقائق في %1$s ثم في %2$s و %3$s","في %n دقيقة في %1$s ثم في %2$s و %3$s","في %n دقيقة في %1$s ثم في %2$s و %3$s"],
+ "_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_" : ["في %n ساعة في %1$s ثم في %2$s و %3$s","في ساعة واحدة في %1$s ثم في %2$s و %3$s","في %n ساعة في %1$s ثم في %2$s و %3$s","في %n ساعات في %1$s ثم في %2$s و %3$s","في %n ساعة في %1$s ثم في %2$s و %3$s","في %n ساعة في %1$s ثم في %2$s و %3$s"],
+ "_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_" : ["في %n يوم في %1$s ثم في %2$s و %3$s","في يوم واحد في %1$s ثم ف %2$s و %3$s","في %n يوم في %1$s ثم في %2$s و %3$s","في %n أيام في %1$s ثم في %2$s و %3$s","في %n يوم في %1$s ثم في %2$s و %3$s","في %n يوم في %1$s ثم في %2$s و %3$s"],
+ "_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_" : ["في %n أسبوع في %1$s ثم في %2$s و %3$s","في أسبوع واحد في %1$s ثم في %2$s و %3$s","في %n أسبوع في %1$s ثم في %2$s و %3$s","في %n أسابيع في %1$s ثم في %2$s و %3$s","في %n أسبوع في %1$s ثم في %2$s و %3$s","في %n أسبوع في %1$s ثم في %2$s و %3$s"],
+ "_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_" : ["في %n شهر في %1$s ثم في %2$s و %3$s","في شهر واحد في %1$s ثم في %2$s و %3$s","في %n شهر في %1$s ثم في %2$s و %3$s","في %n شهور في %1$s ثم في %2$s و %3$s","في %n شهر في %1$s ثم في %2$s و %3$s","في %n شهر في %1$s ثم في %2$s و %3$s"],
+ "_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_" : ["في%n سنة في %1$s ثم في %2$s و %3$s","في سنة واحدة في %1$s ثم في %2$s و %3$s","في %n سنة في %1$s ثم في %2$s و %3$s","في %n سنوات في %1$s ثم في %2$s و %3$s","في %n سنة في %1$s ثم في %2$s و %3$s","في %n سنة في %1$s ثم في %2$s و %3$s"],
+ "Could not generate next recurrence statement" : "يتعذّر توليد عبارة التكرار التالي",
+ "Cancelled: %1$s" : "مُلغىً: %1$s",
+ "\"%1$s\" has been canceled" : "\"%1$s\" تمّ إلغاؤه",
+ "Re: %1$s" : "إعادة: %1$s",
+ "%1$s has accepted your invitation" : "%1$s قبل دعوتك",
+ "%1$s has tentatively accepted your invitation" : "%1$s قبل دعوتك بشكل مبدئي",
+ "%1$s has declined your invitation" : "%1$s لم يقبل دعوتك",
+ "%1$s has responded to your invitation" : "%1$s استجاب لدعوتك",
+ "Invitation updated: %1$s" : "تحديث الدعوة: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s تحديث الحدث \"%2$s\"",
+ "Invitation: %1$s" : "دعوة: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s يرغب في دعوتكم إلى \"%2$s\"",
+ "Organizer:" : "تنظيم:",
+ "Attendees:" : "الحُضُور:",
+ "Title:" : "العنوان:",
+ "When:" : "متى:",
+ "Location:" : "المكان :",
+ "Link:" : "الرابط:",
+ "Occurring:" : "التكرار:",
+ "Accept" : "قبول",
+ "Decline" : "رفض",
+ "More options …" : "مزيد مِن الخيارات…",
+ "More options at %s" : "خيارات أخرى في %s",
+ "Monday" : "الإثنين",
+ "Tuesday" : "الثلاثاء",
+ "Wednesday" : "الأربعاء",
+ "Thursday" : "الخميس",
+ "Friday" : "الجمعة",
+ "Saturday" : "السبت",
+ "Sunday" : "الأحد",
+ "January" : "يناير",
+ "February" : "فبراير",
+ "March" : "مارس",
+ "April" : "أبريل",
+ "May" : "مايو",
+ "June" : "يونيو",
+ "July" : "يوليو",
+ "August" : "اغسطس",
+ "September" : "سبتمبر",
+ "October" : "أكتوبر",
+ "November" : "نوفمبر",
+ "December" : "ديسمبر",
+ "First" : "أول",
+ "Second" : "ثاني",
+ "Third" : "ثالث",
+ "Fourth" : "رابع",
+ "Fifth" : "خامس",
+ "Last" : "آخِر",
+ "Second Last" : "ما قبل الأخير",
+ "Third Last" : "الثالث من الآخِر",
+ "Fourth Last" : "الرابع من الآخِر",
+ "Fifth Last" : "خامس من الأخير",
+ "Contacts" : "جهات الاتصال",
+ "{actor} created address book {addressbook}" : "{actor} أنشأ دفتر العناوين {addressbook}",
+ "You created address book {addressbook}" : "أنت أنشأت دفتر العناوين {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} قام بحذف دفتر العناوين {addressbook}",
+ "You deleted address book {addressbook}" : "أنت قمت بحذف دفتر العناوين {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} قام بتعديل دفتر العناوين {addressbook}",
+ "You updated address book {addressbook}" : "أنت قمت بتعديل دفتر العناوين {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} قام بمشاركة دفتر العناوين {addressbook} معك",
+ "You shared address book {addressbook} with {user}" : "أنت قمت بمشاركة دفتر العناوين {addressbook} مع {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} قام بمشاركة دفتر العناوين {addressbook} مع {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} قام بإلغاء مشاركة دفتر العناوين {addressbook} معك",
+ "You unshared address book {addressbook} from {user}" : "أنت قمت بإلغاء مشاركة دفتر العناوين {addressbook} مع {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} قام بإلغاء مشاركة دفتر العناوين {addressbook} مع {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} قام بإلغاء مشاركته في دفتر العناوين {addressbook} ",
+ "You shared address book {addressbook} with group {group}" : "أنت قمت بمشاركة دفتر العناوين {addressbook} مع المجموعة {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} قام بمشاركة دفتر العناوين {addressbook} مع المجموعة {group}",
+ "You unshared address book {addressbook} from group {group}" : "أنت قمت بإلغاء مشاركة دفتر العناوين {addressbook} مع المجموعة {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} قام بإلغاء مشاركة دفتر العناوين {addressbook} مع المجموعة {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} قام بإنشاء جهة اتصال {card} في دفتر العناوين {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "أنت قمت بإنشاء جهة اتصال {card} في دفتر العناوين {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} قام بحذف جهة الاتصال {card} من دفتر العناوين {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "أنت قمت بحذف جهة الاتصال {card} من دفتر العناوين {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} قام بتحديث جهة الاتصال {card} في دفتر العناوين {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "أنت قمت بتحديث جهة الاتصال {card} في دفتر العناوين {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "تمّ تعديل <strong>جهة الاتصال</strong> أو <strong>دفتر العناوين</strong> ",
+ "Accounts" : "الحسابات",
+ "System address book which holds all accounts" : "دفتر عناوين النظام الذي يحتوي على جميع الحسابات",
+ "File is not updatable: %1$s" : "ملف غير قابل للتعديل: %1$s",
+ "Failed to get storage for file" : "تعذّر الحصول على مكان لتخزين الملف",
+ "Could not write to final file, canceled by hook" : "تعذرت الكتابة إلى الملف النهائي، تم إلغاؤه بواسطة خطّاف hook",
+ "Could not write file contents" : "تعذرت كتابة محتويات الملف",
+ "_%n byte_::_%n bytes_" : ["بايت","بايت","بايت","بايت","بايت","%n بايت"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "حدث خطأ أثناء نسخ الملف إلى الموقع الهدف (تمّ نسخه: %1$s, حجم الملف المتوقع: %2$s)",
+ "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." : "الحجم المتوقع للملف هو %1$s؛ بينما المقروء من ( الجهاز العميل لنكست كلاود) و المكتوب في (تخزين نكست كلاود) هو %2$s. يمكن أن يكون السبب إمّا مشكلة اتصال في جانب الجهاز العميل أو مشكلة في الكتابة في وحدة التخزين في جانب خادم نكست كلاود .",
+ "Could not rename part file to final file, canceled by hook" : "تعذّرت إعادة تسمية ملف جزئي إلى ملف نهائي. تمّ الإلغاء من قِبَل الخطّاف hook.",
+ "Could not rename part file to final file" : "تعذّرت إعادة تسمية ملف جزئي إلى ملف نهائي",
+ "Failed to check file size: %1$s" : "فشل في تحديد حجم الملف: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "يتعذّر فتح الملف: %1$s, يبدو أن الملف غير موجود",
+ "Could not open file: %1$s, file doesn't seem to exist" : "يتعذّر فتح الملف: %1$s, يبدو أن الملف غير موجود",
+ "Encryption not ready: %1$s" : "التشفير غير جاهز: %1$s",
+ "Failed to open file: %1$s" : "تعذّر فتح الملف: %1$s",
+ "Failed to unlink: %1$s" : "تعذّر فك الارتباط: %1$s",
+ "Failed to write file contents: %1$s" : "فشل في كتابة محتويات الملف: %1$s",
+ "File not found: %1$s" : "ملف غير موجود: %1$s",
+ "Invalid target path" : "المسار الهدف غير صحيح",
+ "System is in maintenance mode." : "النظام في حالة صيانة.",
+ "Upgrade needed" : "يجب التحديث",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "%s الخاص بك يجب تهيئته لاستخدام HTTPS حتى يمكن استعمال CalDAV و CardDAV في نظام التشغيل iOS/macOS. ",
+ "Configures a CalDAV account" : "تكوين حساب CalDAV",
+ "Configures a CardDAV account" : "تكوين حساب CardDAV",
+ "Events" : "أحداث",
+ "Untitled task" : "مهمة بدون اسم",
+ "Completed on %s" : "تمّ الانتهاء منه في %s",
+ "Due on %s by %s" : "مطلوبٌ في %s من قِبَل%s",
+ "Due on %s" : "مطلوبٌ في %s",
+ "System Address Book" : "دفتر عناوين النظام",
+ "The system address book contains contact information for all users in your instance." : "دفتر عناوين النظام يحتوي على معلومات الاتصال لجميع المستخدِمين على خادومك. ",
+ "Enable System Address Book" : "تمكين دفتر عناوين النظام",
+ "DAV system address book" : "دفتر عناوين نظام DAV",
+ "No outstanding DAV system address book sync." : "لا توجد أي مزامنات معلقة لدفتر عناوني نظام DAV.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "لم يتم تشغيل مزامنة دفتر عناوين نظام DAV حتى الآن بسبب أن الخادم الخاص بك يحتوي على أكثر من 1000 مستخدم أو بسبب حدوث خطأ. يرجى تشغيل المزامنة يدويًا عن طريق الأمر السطري:\n\"occ dav:sync-system-addressbook\"",
+ "WebDAV endpoint" : "النقطة النهائية endpoint لـ WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "تعذر التحقُّق من إعداد خادم الويب عندك بالشكل الصحيح للسماح بمزامنة الملفات عبر WebDAV. يرجى التحقُّق يدوياً.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : " لم يتم تعيين السماح لخادمك السحابي بتزامن الملف، بسبب واجهة التأليف الموزع على الويب وتعيين الإصدار WebDAV غير متصلة.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "خادم الويب عندك مُهيّءٌ بالشكل الصحيح للسماح بمزامنة الملفات عبر WebDAV.",
+ "Migrated calendar (%1$s)" : "تقويم مُرحّل (%1$s)",
+ "Calendars including events, details and attendees" : "تحوي التقاويم الأحداث، و تفاصيلها، و الحُضُور",
+ "Contacts and groups" : "جهات الاتصال والمجموعات",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "تمّ حفظ الغياب",
+ "Failed to save your absence settings" : "تعذّر حفظ إعداداتك للغياب",
+ "Absence cleared" : "تمّ محو الغياب",
+ "Failed to clear your absence settings" : "تعذّر محو إعداداتك للغياب",
+ "First day" : "أول يوم",
+ "Last day (inclusive)" : "آخر يوم (متضمن)",
+ "Out of office replacement (optional)" : "البديل لمن هو خارج المكتب (إختياري)",
+ "Name of the replacement" : "اسم البديل",
+ "No results." : "لا نتائج",
+ "Start typing." : "أبدا الكتابة",
+ "Short absence status" : "حالة الغياب القصير",
+ "Long absence Message" : "رسالة الغياب الطويل",
+ "Save" : "حفظ",
+ "Disable absence" : "تعطيل الغياب",
+ "Failed to load availability" : "فشل في تحميل أوقات التواجد",
+ "Saved availability" : "تمّ حفظ أوقات التواجد",
+ "Failed to save availability" : "تعذّر حفظ أوقات التواجد",
+ "Time zone:" : "منطقة زمنية:",
+ "to" : "إلى",
+ "Delete slot" : "حذف الخانة الزمنية",
+ "No working hours set" : "لم يتم تحديد ساعات العمل",
+ "Add slot" : "إضافة خانة زمنية",
+ "Weekdays" : "أيام الأسبوع",
+ "Pick a start time for {dayName}" : "إختَر وقت البدء ليوم {dayName}",
+ "Pick a end time for {dayName}" : "إختَر وقت الانتهاء ليوم {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "قم بتعيين حالة المستخدم تلقائيًا على \"عدم الإزعاج\" خارج نطاق أوقات التواجد لكتم جميع الإشعارات",
+ "Cancel" : "إلغاء",
+ "Import" : "إستيراد",
+ "Error while saving settings" : "خطأ أثناء حفظ الإعدادات",
+ "Contact reset successfully" : "تمّت إعادة تعيين جهة الاتصال بنجاحٍ",
+ "Error while resetting contact" : "خطأ أثناء إعادة تعيين جهة الاتصال",
+ "Contact imported successfully" : "تمّ استيراد جهة الاتصال بنجاحٍ",
+ "Error while importing contact" : "خطأ أثناء استيراد جهة الاتصال",
+ "Import contact" : "استيراد جهة اتصال",
+ "Reset to default" : "اعادة تعيين الافتراضيات",
+ "Import contacts" : "استيراد جهات اتصال",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "استيراد ملف .cvf جديد سوف يؤدي إلى حذف جهات الاتصال التلقائية الحالية واستبدالها بالجديدة. هل ترغب في الاستمرار؟",
+ "Availability" : "أوقات التواجد ",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "إذا قمت بضبط ساعات عملك، سيرى الآخرون متى تكون خارج المكتب عندما يقومون بحجز اجتماع معك.",
+ "Absence" : "غياب",
+ "Configure your next absence period." : "تهيئة فترة غيابك القادمة.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "قم أيضاً بتنصيب {calendarappstoreopen} تطبيق التقويم {linkclose}, أو {calendardocopen} أوصل جهازك و موبايلك للمُزامنة ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "رجاءُ، تأكّد من الإعداد الصحيح لـ {emailopen} خادم البريد الالكتروني {linkclose}.",
+ "Calendar server" : "خادم التقويم",
+ "Send invitations to attendees" : "إرسال دعوات للمطلوب حضورهم",
+ "Automatically generate a birthday calendar" : "تجاهل تقويم أعياد الميلاد تلقائيّاً",
+ "Birthday calendars will be generated by a background job." : "تقويم أعياد الميلاد سيتم توليده من قِبَل مهمةٍ في الخلفية.",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "ومن ثمّ لن تكون متاحةً فور التفعيل بل ستظهر بعد مرور بعض الوقت.",
+ "Send notifications for events" : "إرسال إشعارات حول الأحداث",
+ "Notifications are sent via background jobs, so these must occur often enough." : "يتمّ إرسال الإشعارات من قِبَل مهمّةٍ في الخلفية. لذا سيتكرر عمل هذا المهام حسب الحاجة.",
+ "Send reminder notifications to calendar sharees as well" : "أرسل إشعارات للتذكير إلى المشتركين بالتقويم كذلك",
+ "Reminders are always sent to organizers and attendees." : "إشعارات التذكير يتم إرسالها دائماً إلى مُنظّم أو مُنظّمي الحدث و المستهدفين بحضوره.",
+ "Enable notifications for events via push" : "تمكين الإشعارات حول الأحداث عن طريق أسلوب دفع الإشعارات Push",
+ "There was an error updating your attendance status." : "حدث خطأ في تحديث حالة حضورك.",
+ "Please contact the organizer directly." : "يرجى الاتصال بالمنظم مباشرةً",
+ "Are you accepting the invitation?" : "هل تقبل الدعوة؟",
+ "Tentative" : "مبدئي",
+ "Your attendance was updated successfully." : "حضورك تم تحديثه بنجاحٍ"
+},"pluralForm" :"nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;"
+} \ No newline at end of file
diff --git a/apps/dav/l10n/ast.js b/apps/dav/l10n/ast.js
new file mode 100644
index 00000000000..b0cba5d934f
--- /dev/null
+++ b/apps/dav/l10n/ast.js
@@ -0,0 +1,224 @@
+OC.L10N.register(
+ "dav",
+ {
+ "Calendar" : "Calendariu",
+ "Tasks" : "Xeres",
+ "Personal" : "Personal",
+ "{actor} created calendar {calendar}" : "{actor} creó'l calendariu «{calendar}»",
+ "You created calendar {calendar}" : "Creesti'l calendariu «{calendar}»",
+ "{actor} deleted calendar {calendar}" : "{actor} desanició'l calendariu «{calendar}»",
+ "You deleted calendar {calendar}" : "Desaniciesti'l calendariu «{calendar}»",
+ "{actor} updated calendar {calendar}" : "{actor} anovó'l calendariu «{calendar}»",
+ "You updated calendar {calendar}" : "Anovesti'l calendariu «{calendar}»",
+ "{actor} restored calendar {calendar}" : "{actor} restauró'l calendariu «{calendar}»",
+ "You restored calendar {calendar}" : "Restauresti'l calendariu «{calendar}»",
+ "You shared calendar {calendar} as public link" : "Compartiesti'l calendariu «{calendar}» con un enllaz públicu",
+ "You removed public link for calendar {calendar}" : "Desaniciesti l'enllaz públicu del calendariu «{calendar}»",
+ "{actor} shared calendar {calendar} with you" : "{actor} compartió'l calendariu «{calendar}» contigo",
+ "You shared calendar {calendar} with {user}" : "Compartiesti'l calendariu «{calendar}» con {user}",
+ "{actor} shared calendar {calendar} with {user}" : "{actor} compartió'l calendariu «{calendar}» con {user}",
+ "{actor} unshared calendar {calendar} from you" : "{actor} dexó de compartir el calendariu «{calendar}» contigo",
+ "You unshared calendar {calendar} from {user}" : "Dexesti de compartir el calendariu «{calendar}» con «{user}»",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor} dexó de compartir el calendariu «{calendar}» con «{user}»",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} dexó de compartir el calendariu «{calendar}» con sigo",
+ "You shared calendar {calendar} with group {group}" : "Compartiesti'l calendariu «{calendar}» col grupu «{group}»",
+ "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió'l calendariu «{calendar}» col grupu «{group}»",
+ "You unshared calendar {calendar} from group {group}" : "Dexesti de compartir el calendariu «{calendar}» col grupu «{group}»",
+ "{actor} unshared calendar {calendar} from group {group}" : "{actor} dexó de compartir el calendariu «{calendar}» col grupu «{group}»",
+ "Untitled event" : "Eventu ensin títulu",
+ "{actor} created event {event} in calendar {calendar}" : "{actor} creó l'eventu «{event}» nel calendariu «{calendar}»",
+ "You created event {event} in calendar {calendar}" : "Creesti l'eventu «{event}» nel calendariu «{calendar}»",
+ "{actor} deleted event {event} from calendar {calendar}" : "{actor} desanició l'eventu «{event}» del calendariu «{calendar}»",
+ "You deleted event {event} from calendar {calendar}" : "Desaniciesti l'eventu «{event}» del calendariu «{calendar}»",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} anovó l'eventu {event} nel calendariu {calendar}",
+ "You updated event {event} in calendar {calendar}" : "Anovesti l'eventu {event} nel calendariu {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} movió l'eventu «{event}» del calendariu «{sourceCalendar}» al calendariu «{targetCalendar}»",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Moviesti l'eventu «{event}» del calendariu «{sourceCalendar}» al calendariu «{targetCalendar}»",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} restauró l'eventu «{event}» del calendariu «{calendar}»",
+ "You restored event {event} of calendar {calendar}" : "Restauresti l'eventu «{event}» del calendariu «{calendar}»",
+ "Busy" : "Ocupáu",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} creó la xera pendiente «{todo}» na llista «{calendar}»",
+ "You created to-do {todo} in list {calendar}" : "Creesti la xera pendiente «{todo}» na llista «{calendar}»",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} desanició la xera pendiente «{todo}» de la llista «{calendar}»",
+ "You deleted to-do {todo} from list {calendar}" : "Desaniciesti la xera pendiente «{todo}» de la llista «{calendar}»",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} anovó la xera pendiente «{todo}» de la llista «{calendar}»",
+ "You updated to-do {todo} in list {calendar}" : "Anovesti la xera pendiente «{todo}» de la llista «{calendar}»",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} resolvió la xera pendiente «{todo}» de la llista «{calendar}»",
+ "You solved to-do {todo} in list {calendar}" : "Resolviesti la xera pendiente «{todo}» de la llista «{calendar}»",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} volvió abrir la xera pendiente «{todo}» de la llista «{calendar}»",
+ "You reopened to-do {todo} in list {calendar}" : "Volviesti abrir la xera pendiente «{todo}» de la llista «{calendar}»",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} movió la xera pendiente «{todo}» de la llista «{sourceCalendar}» a la llista «{targetCalendar}»",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Moviesti la xera pendiente «{todo}» de la llista «{sourceCalendar}» a la llista «{targetCalendar}»",
+ "Calendar, contacts and tasks" : "Calendariu, contautos y xeres",
+ "A <strong>calendar</strong> was modified" : "Modificóse un <strong>calendariu</strong>",
+ "A calendar <strong>event</strong> was modified" : "Modificóse un elementu del <strong>calendariu</strong>",
+ "A calendar <strong>to-do</strong> was modified" : "Modificóse una <strong>xera pendiente</strong> del calendariu",
+ "Contact birthdays" : "Cumpleaños de contautos",
+ "Death of %s" : "Muerte de: %s",
+ "Untitled calendar" : "Calendariu ensin títulu",
+ "Calendar:" : "Calendariu:",
+ "Date:" : "Data:",
+ "Where:" : "Ónde:",
+ "Description:" : "Descripción:",
+ "_%n year_::_%n years_" : ["%n añu","%n años"],
+ "_%n month_::_%n months_" : ["%n mes","%n meses"],
+ "_%n day_::_%n days_" : ["%n día","%n díes"],
+ "_%n hour_::_%n hours_" : ["%n hora","%n hores"],
+ "_%n minute_::_%n minutes_" : ["%n minutu","%n minutos"],
+ "%s (in %s)" : "%s (en %s)",
+ "%s (%s ago)" : "%s (hai %s)",
+ "Calendar: %s" : "Calendariu: %s",
+ "Date: %s" : "Data: %s",
+ "Description: %s" : "Descripción: %s",
+ "Where: %s" : "Ónde: %s",
+ "%1$s via %2$s" : "%1$s per %2$s",
+ "Cancelled: %1$s" : "Anulóse: %1$s",
+ "\"%1$s\" has been canceled" : "Anulóse «%1$s»",
+ "Re: %1$s" : "Re: %1$s",
+ "%1$s has accepted your invitation" : "%1$s aceptó la to invitación",
+ "%1$s has tentatively accepted your invitation" : "%1$s aceptó provisionalmente la to invitación",
+ "%1$s has declined your invitation" : "%1$s refugó la to invitación",
+ "%1$s has responded to your invitation" : "%1$s respondió a la to invitación",
+ "Invitation updated: %1$s" : "Anovóse la invitación: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s anovó l'eventu «%2$s»",
+ "Invitation: %1$s" : "Invitación: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s quier convidate a «%2$s»",
+ "Organizer:" : "Organizador:",
+ "Attendees:" : "Asistentes:",
+ "Title:" : "Títulu:",
+ "Location:" : "Llugar:",
+ "Link:" : "Enllaz:",
+ "Accept" : "Aceptar",
+ "Decline" : "Refugar",
+ "More options …" : "Más opciones…",
+ "More options at %s" : "Más opciones en «%s»",
+ "Monday" : "Llunes",
+ "Tuesday" : "Martes",
+ "Wednesday" : "Miércoles",
+ "Thursday" : "Xueves",
+ "Friday" : "Vienres",
+ "Saturday" : "Sábadu",
+ "Sunday" : "Domingu",
+ "January" : "Xineru",
+ "February" : "Febreru",
+ "March" : "Marzu",
+ "April" : "Abril",
+ "May" : "Mayu",
+ "June" : "Xunu",
+ "July" : "Xunetu",
+ "August" : "Agostu",
+ "September" : "Setiembre",
+ "October" : "Ochobre",
+ "November" : "Payares",
+ "December" : "Avientu",
+ "Contacts" : "Contautos",
+ "{actor} created address book {addressbook}" : "{actor} creó la llibreta de direiciones «{addressbook}»",
+ "You created address book {addressbook}" : "Creesti la llibreta de direiciones «{addressbook}»",
+ "{actor} deleted address book {addressbook}" : "{actor} desanició la llibreta de direiciones «{addressbook}»",
+ "You deleted address book {addressbook}" : "Desaniciesti la llibreta de direiciones «{addressbook}»",
+ "{actor} updated address book {addressbook}" : "{actor} anovó la llibreta de direiciones «{addressbook}»",
+ "You updated address book {addressbook}" : "Anovesti la llibreta de direiciones «{addressbook}»",
+ "{actor} shared address book {addressbook} with you" : "{actor} compartió la llibreta de direiciones «{addressbook}» contigo",
+ "You shared address book {addressbook} with {user}" : "Compartiesti la llibreta de direiciones «{addressbook}» con {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} compartió la llibreta de direiciones «{addressbook}» con {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} dexó de compartir la llibreta de direiciones «{addressbook}» contigo",
+ "You unshared address book {addressbook} from {user}" : "Dexesti de compartir la llibreta de direiciones «{addressbook}» con {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} dexó de compartir la llibreta de direiciones «{addressbook}» con {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} dexó de compartir la llibreta de direiciones «{addressbook}» con sigo mesmu",
+ "You shared address book {addressbook} with group {group}" : "Dexesti de compartir la llibreta de direiciones «{addressbook}» col grupu «{group}»",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} compartió la llibreta de direiciones «{addressbook}» col grupu «{group}»",
+ "You unshared address book {addressbook} from group {group}" : "Dexesti de compartir la llibreta de direiciones «{addressbook}» col grupu «{group}»",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} dexó de compartir la llibreta de direiciones «{addressbook}» col grupu {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} creó'l contautu «{card}» na llibreta de direiciones «{addressbook}»",
+ "You created contact {card} in address book {addressbook}" : "Creesti'l contautu «{card}» na llibreta de direiciones «{addressbook}»",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} desanició'l contautu «{card}» de la llibreta de direiciones «{addressbook}»",
+ "You deleted contact {card} from address book {addressbook}" : "Desaniciesti'l contautu «{card}» de la llibreta de direiciones «{addressbook}»",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} anovó'l contautu «{card}» de la llibreta de direiciones «{addressbook}»",
+ "You updated contact {card} in address book {addressbook}" : "Anovesti'l contautu «{card}» de la llibreta de direiciones «{addressbook}»",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Modificóse un <strong>contautu</strong> o una <strong>llibreta de direiciones</strong>",
+ "Accounts" : "Cuentes",
+ "System address book which holds all accounts" : "La llibreta de direiciones del sistema que contién toles cuentes",
+ "File is not updatable: %1$s" : "El ficheru nun se pue anovar: %1$s",
+ "Could not write to final file, canceled by hook" : "Nun se pudo escribir el ficheru final porque'l ganchu encaboxólo",
+ "Could not write file contents" : "Nun se pudo escribir el conteníu del ficheru",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Hebo un error mentanto se copiaba a la llocalización de destín (datos copiaos: %1$s, tamañu esperáu: %2$s)",
+ "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." : "Esperábase un tamañu de %1$s mas lleéronse (del veceru de Nextcloud) y escribiéronse (nel almacenamientu de Nextcloud) %2$s. Pues ser un problema de la rede na parte qu'unvia o un problema d'escritura nel almacenamientu del sirvidor.",
+ "Could not rename part file to final file, canceled by hook" : "Nun se pudo renomar el ficheru parcial al ficheru final, el ganchu encaboxó l'aición",
+ "Could not rename part file to final file" : "Nun se pudo renomar el ficheru parcial al ficheru final, el ganchu encaboxó l'aición",
+ "Failed to check file size: %1$s" : "Nun se pue comprobar el tamañu del ficheru: %1$s",
+ "Encryption not ready: %1$s" : "El cifráu nun ta preparáu: %1$s",
+ "Failed to open file: %1$s" : "Nun se pue abrir el ficheru: %1$s",
+ "Failed to unlink: %1$s" : "Nun se pue desenllaciar: %1$s",
+ "Failed to write file contents: %1$s" : "Nun se pue escribir el conteníu nel ficheru: %1$s",
+ "File not found: %1$s" : "Nun s'atopó'l ficheru: %1$s",
+ "System is in maintenance mode." : "El sistema ta nel mou de caltenimientu.",
+ "Upgrade needed" : "L'anovamientu ye necesariu",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Tienes de configurar «%s» pa usar HTTPS, CalDAV y CardDAV con iOS/macOS.",
+ "Configures a CalDAV account" : "Configura una cuenta CalDAV",
+ "Configures a CardDAV account" : "Configura una cuenta CardDAV",
+ "Events" : "Eventos",
+ "Untitled task" : "Xera ensin títulu",
+ "Completed on %s" : "Completóse'l %s",
+ "Due on %s by %s" : "Vence'l %s a la hora %s",
+ "Due on %s" : "Vence'l %s",
+ "DAV system address book" : "Llibreta de direiciones del sistema DAV",
+ "No outstanding DAV system address book sync." : "Nun hai nenguna sincronización pendiente de la llibreta de direiciones del sistema DAV.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Entá nun s'executó la sincronización de les llibretes de direiciones del sistema DAV darréu que la instancia tien más de 1000 usuarios o porque se produxo un error. Execútala manuelamente pente la llamada de «cc dav:sync-system-addressbook».",
+ "WebDAV endpoint" : "Estremu de WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Nun se pudo comprobar que'l sirvidor web se configurare afayadizamente pa permitir la configuración de ficheros per WebDAV. Compruébalo manualmente.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "El sirividor web nun ta configuráu afayadizamente pa permitir la sincronización de ficheros porque la interfaz WebDAV paez tar estropiada.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "El sirvidor web ta configuráu afayadizamente pa permitir la sincronización de ficheros per WebDAV.",
+ "Migrated calendar (%1$s)" : "Calendariu migráu (%1$s)",
+ "Calendars including events, details and attendees" : "Calendarios qu'inclúin eventos, detalles y asistentes",
+ "Contacts and groups" : "Contautos y grupos",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "Guardóse l'ausencia",
+ "Failed to save your absence settings" : "Nun se pue guardar la configuración d'ausencia",
+ "Absence cleared" : "Borróse l'ausencia",
+ "Failed to clear your absence settings" : "Nun se pue borrar la configuración de l'ausencia",
+ "First day" : "El primer día",
+ "Last day (inclusive)" : "L'últimu día (inclusive)",
+ "Short absence status" : "Estáu d'ausencia curtíu",
+ "Long absence Message" : "Mensaxe d'ausencia llongu",
+ "Save" : "Guardar",
+ "Disable absence" : "Desactivar l'ausencia",
+ "Failed to load availability" : "Nun se pue cargar la disponibilidá",
+ "Saved availability" : "Disponibilidá guardada",
+ "Failed to save availability" : "Nun se pue guardar la disponibilidá",
+ "Time zone:" : "Fusu horariu:",
+ "to" : "pa",
+ "Delete slot" : "Desaniciar la ralura",
+ "No working hours set" : "Nun s'afitó nenguna hora llaboral",
+ "Add slot" : "Amestar una ralura",
+ "Weekdays" : "Díes de la selmana",
+ "Pick a start time for {dayName}" : "Escueyi una hora de comienzu pal {dayName}",
+ "Pick a end time for {dayName}" : "Escueyi una hora de fin pal {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Afitar automáticamente l'estáu a «Nun molestar» fuera de la disponibilidá pa desactivar tolos avisos.",
+ "Cancel" : "Encaboxar",
+ "Import" : "Importa",
+ "Error while saving settings" : "Hebo un error mentanto se guardaba la configuración",
+ "Reset to default" : "Reafitar los valores",
+ "Availability" : "Disponibilidá",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Si configures les hores llaborales, les demás persones van ver cuando coles de la oficina al acutar una reunión.",
+ "Absence" : "Ausencia",
+ "Configure your next absence period." : "Configura'l próximu periodu d'ausencia",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instala tamién l'{calendarappstoreopen}aplicación Calendariu{linkclose} o {calendardocopen}conecta'l veceru pa ordenadores y/o móviles pa sincronizar ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Asegúrate de que configuresti afayadizamente'l {emailopen}sirvidor de corréu electrónicu{linkclose}.",
+ "Calendar server" : "Sirvidor de calendarios",
+ "Send invitations to attendees" : "Unviar invitaciones a los asistentes",
+ "Automatically generate a birthday calendar" : "Xenerar automáticamente un calendariu de cumpleaños",
+ "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños xenéralos un trabayu en segundu planu.",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "Polo tanto, nun van tar disponibles inmediatamente tres activalos mas van apaecer dempués d'un tiempu.",
+ "Send notifications for events" : "Unviar avisos de los eventos",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Los avisos únvienlos trabayos en segundu planu, polo que van producise davezu.",
+ "Send reminder notifications to calendar sharees as well" : "Unvia tamién avisos de recordatoriu pa les persones coles que se compartiere'l calendariu",
+ "Reminders are always sent to organizers and attendees." : "Los recordatorios únviense siempres a organizadores y asistentes",
+ "Enable notifications for events via push" : "Acriva los avisos automáticos pa los eventos",
+ "There was an error updating your attendance status." : "Hebo un error al anovar l'estáu de l'asistencia.",
+ "Please contact the organizer directly." : "Ponte en contautu direutamente cola organización.",
+ "Are you accepting the invitation?" : "¿Aceptes la invitación?",
+ "Tentative" : "Provisional",
+ "Your attendance was updated successfully." : "La to asistencia anovóse correutamente."
+},
+"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/ast.json b/apps/dav/l10n/ast.json
new file mode 100644
index 00000000000..519aa700951
--- /dev/null
+++ b/apps/dav/l10n/ast.json
@@ -0,0 +1,222 @@
+{ "translations": {
+ "Calendar" : "Calendariu",
+ "Tasks" : "Xeres",
+ "Personal" : "Personal",
+ "{actor} created calendar {calendar}" : "{actor} creó'l calendariu «{calendar}»",
+ "You created calendar {calendar}" : "Creesti'l calendariu «{calendar}»",
+ "{actor} deleted calendar {calendar}" : "{actor} desanició'l calendariu «{calendar}»",
+ "You deleted calendar {calendar}" : "Desaniciesti'l calendariu «{calendar}»",
+ "{actor} updated calendar {calendar}" : "{actor} anovó'l calendariu «{calendar}»",
+ "You updated calendar {calendar}" : "Anovesti'l calendariu «{calendar}»",
+ "{actor} restored calendar {calendar}" : "{actor} restauró'l calendariu «{calendar}»",
+ "You restored calendar {calendar}" : "Restauresti'l calendariu «{calendar}»",
+ "You shared calendar {calendar} as public link" : "Compartiesti'l calendariu «{calendar}» con un enllaz públicu",
+ "You removed public link for calendar {calendar}" : "Desaniciesti l'enllaz públicu del calendariu «{calendar}»",
+ "{actor} shared calendar {calendar} with you" : "{actor} compartió'l calendariu «{calendar}» contigo",
+ "You shared calendar {calendar} with {user}" : "Compartiesti'l calendariu «{calendar}» con {user}",
+ "{actor} shared calendar {calendar} with {user}" : "{actor} compartió'l calendariu «{calendar}» con {user}",
+ "{actor} unshared calendar {calendar} from you" : "{actor} dexó de compartir el calendariu «{calendar}» contigo",
+ "You unshared calendar {calendar} from {user}" : "Dexesti de compartir el calendariu «{calendar}» con «{user}»",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor} dexó de compartir el calendariu «{calendar}» con «{user}»",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} dexó de compartir el calendariu «{calendar}» con sigo",
+ "You shared calendar {calendar} with group {group}" : "Compartiesti'l calendariu «{calendar}» col grupu «{group}»",
+ "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió'l calendariu «{calendar}» col grupu «{group}»",
+ "You unshared calendar {calendar} from group {group}" : "Dexesti de compartir el calendariu «{calendar}» col grupu «{group}»",
+ "{actor} unshared calendar {calendar} from group {group}" : "{actor} dexó de compartir el calendariu «{calendar}» col grupu «{group}»",
+ "Untitled event" : "Eventu ensin títulu",
+ "{actor} created event {event} in calendar {calendar}" : "{actor} creó l'eventu «{event}» nel calendariu «{calendar}»",
+ "You created event {event} in calendar {calendar}" : "Creesti l'eventu «{event}» nel calendariu «{calendar}»",
+ "{actor} deleted event {event} from calendar {calendar}" : "{actor} desanició l'eventu «{event}» del calendariu «{calendar}»",
+ "You deleted event {event} from calendar {calendar}" : "Desaniciesti l'eventu «{event}» del calendariu «{calendar}»",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} anovó l'eventu {event} nel calendariu {calendar}",
+ "You updated event {event} in calendar {calendar}" : "Anovesti l'eventu {event} nel calendariu {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} movió l'eventu «{event}» del calendariu «{sourceCalendar}» al calendariu «{targetCalendar}»",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Moviesti l'eventu «{event}» del calendariu «{sourceCalendar}» al calendariu «{targetCalendar}»",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} restauró l'eventu «{event}» del calendariu «{calendar}»",
+ "You restored event {event} of calendar {calendar}" : "Restauresti l'eventu «{event}» del calendariu «{calendar}»",
+ "Busy" : "Ocupáu",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} creó la xera pendiente «{todo}» na llista «{calendar}»",
+ "You created to-do {todo} in list {calendar}" : "Creesti la xera pendiente «{todo}» na llista «{calendar}»",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} desanició la xera pendiente «{todo}» de la llista «{calendar}»",
+ "You deleted to-do {todo} from list {calendar}" : "Desaniciesti la xera pendiente «{todo}» de la llista «{calendar}»",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} anovó la xera pendiente «{todo}» de la llista «{calendar}»",
+ "You updated to-do {todo} in list {calendar}" : "Anovesti la xera pendiente «{todo}» de la llista «{calendar}»",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} resolvió la xera pendiente «{todo}» de la llista «{calendar}»",
+ "You solved to-do {todo} in list {calendar}" : "Resolviesti la xera pendiente «{todo}» de la llista «{calendar}»",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} volvió abrir la xera pendiente «{todo}» de la llista «{calendar}»",
+ "You reopened to-do {todo} in list {calendar}" : "Volviesti abrir la xera pendiente «{todo}» de la llista «{calendar}»",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} movió la xera pendiente «{todo}» de la llista «{sourceCalendar}» a la llista «{targetCalendar}»",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Moviesti la xera pendiente «{todo}» de la llista «{sourceCalendar}» a la llista «{targetCalendar}»",
+ "Calendar, contacts and tasks" : "Calendariu, contautos y xeres",
+ "A <strong>calendar</strong> was modified" : "Modificóse un <strong>calendariu</strong>",
+ "A calendar <strong>event</strong> was modified" : "Modificóse un elementu del <strong>calendariu</strong>",
+ "A calendar <strong>to-do</strong> was modified" : "Modificóse una <strong>xera pendiente</strong> del calendariu",
+ "Contact birthdays" : "Cumpleaños de contautos",
+ "Death of %s" : "Muerte de: %s",
+ "Untitled calendar" : "Calendariu ensin títulu",
+ "Calendar:" : "Calendariu:",
+ "Date:" : "Data:",
+ "Where:" : "Ónde:",
+ "Description:" : "Descripción:",
+ "_%n year_::_%n years_" : ["%n añu","%n años"],
+ "_%n month_::_%n months_" : ["%n mes","%n meses"],
+ "_%n day_::_%n days_" : ["%n día","%n díes"],
+ "_%n hour_::_%n hours_" : ["%n hora","%n hores"],
+ "_%n minute_::_%n minutes_" : ["%n minutu","%n minutos"],
+ "%s (in %s)" : "%s (en %s)",
+ "%s (%s ago)" : "%s (hai %s)",
+ "Calendar: %s" : "Calendariu: %s",
+ "Date: %s" : "Data: %s",
+ "Description: %s" : "Descripción: %s",
+ "Where: %s" : "Ónde: %s",
+ "%1$s via %2$s" : "%1$s per %2$s",
+ "Cancelled: %1$s" : "Anulóse: %1$s",
+ "\"%1$s\" has been canceled" : "Anulóse «%1$s»",
+ "Re: %1$s" : "Re: %1$s",
+ "%1$s has accepted your invitation" : "%1$s aceptó la to invitación",
+ "%1$s has tentatively accepted your invitation" : "%1$s aceptó provisionalmente la to invitación",
+ "%1$s has declined your invitation" : "%1$s refugó la to invitación",
+ "%1$s has responded to your invitation" : "%1$s respondió a la to invitación",
+ "Invitation updated: %1$s" : "Anovóse la invitación: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s anovó l'eventu «%2$s»",
+ "Invitation: %1$s" : "Invitación: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s quier convidate a «%2$s»",
+ "Organizer:" : "Organizador:",
+ "Attendees:" : "Asistentes:",
+ "Title:" : "Títulu:",
+ "Location:" : "Llugar:",
+ "Link:" : "Enllaz:",
+ "Accept" : "Aceptar",
+ "Decline" : "Refugar",
+ "More options …" : "Más opciones…",
+ "More options at %s" : "Más opciones en «%s»",
+ "Monday" : "Llunes",
+ "Tuesday" : "Martes",
+ "Wednesday" : "Miércoles",
+ "Thursday" : "Xueves",
+ "Friday" : "Vienres",
+ "Saturday" : "Sábadu",
+ "Sunday" : "Domingu",
+ "January" : "Xineru",
+ "February" : "Febreru",
+ "March" : "Marzu",
+ "April" : "Abril",
+ "May" : "Mayu",
+ "June" : "Xunu",
+ "July" : "Xunetu",
+ "August" : "Agostu",
+ "September" : "Setiembre",
+ "October" : "Ochobre",
+ "November" : "Payares",
+ "December" : "Avientu",
+ "Contacts" : "Contautos",
+ "{actor} created address book {addressbook}" : "{actor} creó la llibreta de direiciones «{addressbook}»",
+ "You created address book {addressbook}" : "Creesti la llibreta de direiciones «{addressbook}»",
+ "{actor} deleted address book {addressbook}" : "{actor} desanició la llibreta de direiciones «{addressbook}»",
+ "You deleted address book {addressbook}" : "Desaniciesti la llibreta de direiciones «{addressbook}»",
+ "{actor} updated address book {addressbook}" : "{actor} anovó la llibreta de direiciones «{addressbook}»",
+ "You updated address book {addressbook}" : "Anovesti la llibreta de direiciones «{addressbook}»",
+ "{actor} shared address book {addressbook} with you" : "{actor} compartió la llibreta de direiciones «{addressbook}» contigo",
+ "You shared address book {addressbook} with {user}" : "Compartiesti la llibreta de direiciones «{addressbook}» con {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} compartió la llibreta de direiciones «{addressbook}» con {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} dexó de compartir la llibreta de direiciones «{addressbook}» contigo",
+ "You unshared address book {addressbook} from {user}" : "Dexesti de compartir la llibreta de direiciones «{addressbook}» con {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} dexó de compartir la llibreta de direiciones «{addressbook}» con {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} dexó de compartir la llibreta de direiciones «{addressbook}» con sigo mesmu",
+ "You shared address book {addressbook} with group {group}" : "Dexesti de compartir la llibreta de direiciones «{addressbook}» col grupu «{group}»",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} compartió la llibreta de direiciones «{addressbook}» col grupu «{group}»",
+ "You unshared address book {addressbook} from group {group}" : "Dexesti de compartir la llibreta de direiciones «{addressbook}» col grupu «{group}»",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} dexó de compartir la llibreta de direiciones «{addressbook}» col grupu {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} creó'l contautu «{card}» na llibreta de direiciones «{addressbook}»",
+ "You created contact {card} in address book {addressbook}" : "Creesti'l contautu «{card}» na llibreta de direiciones «{addressbook}»",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} desanició'l contautu «{card}» de la llibreta de direiciones «{addressbook}»",
+ "You deleted contact {card} from address book {addressbook}" : "Desaniciesti'l contautu «{card}» de la llibreta de direiciones «{addressbook}»",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} anovó'l contautu «{card}» de la llibreta de direiciones «{addressbook}»",
+ "You updated contact {card} in address book {addressbook}" : "Anovesti'l contautu «{card}» de la llibreta de direiciones «{addressbook}»",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Modificóse un <strong>contautu</strong> o una <strong>llibreta de direiciones</strong>",
+ "Accounts" : "Cuentes",
+ "System address book which holds all accounts" : "La llibreta de direiciones del sistema que contién toles cuentes",
+ "File is not updatable: %1$s" : "El ficheru nun se pue anovar: %1$s",
+ "Could not write to final file, canceled by hook" : "Nun se pudo escribir el ficheru final porque'l ganchu encaboxólo",
+ "Could not write file contents" : "Nun se pudo escribir el conteníu del ficheru",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Hebo un error mentanto se copiaba a la llocalización de destín (datos copiaos: %1$s, tamañu esperáu: %2$s)",
+ "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." : "Esperábase un tamañu de %1$s mas lleéronse (del veceru de Nextcloud) y escribiéronse (nel almacenamientu de Nextcloud) %2$s. Pues ser un problema de la rede na parte qu'unvia o un problema d'escritura nel almacenamientu del sirvidor.",
+ "Could not rename part file to final file, canceled by hook" : "Nun se pudo renomar el ficheru parcial al ficheru final, el ganchu encaboxó l'aición",
+ "Could not rename part file to final file" : "Nun se pudo renomar el ficheru parcial al ficheru final, el ganchu encaboxó l'aición",
+ "Failed to check file size: %1$s" : "Nun se pue comprobar el tamañu del ficheru: %1$s",
+ "Encryption not ready: %1$s" : "El cifráu nun ta preparáu: %1$s",
+ "Failed to open file: %1$s" : "Nun se pue abrir el ficheru: %1$s",
+ "Failed to unlink: %1$s" : "Nun se pue desenllaciar: %1$s",
+ "Failed to write file contents: %1$s" : "Nun se pue escribir el conteníu nel ficheru: %1$s",
+ "File not found: %1$s" : "Nun s'atopó'l ficheru: %1$s",
+ "System is in maintenance mode." : "El sistema ta nel mou de caltenimientu.",
+ "Upgrade needed" : "L'anovamientu ye necesariu",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Tienes de configurar «%s» pa usar HTTPS, CalDAV y CardDAV con iOS/macOS.",
+ "Configures a CalDAV account" : "Configura una cuenta CalDAV",
+ "Configures a CardDAV account" : "Configura una cuenta CardDAV",
+ "Events" : "Eventos",
+ "Untitled task" : "Xera ensin títulu",
+ "Completed on %s" : "Completóse'l %s",
+ "Due on %s by %s" : "Vence'l %s a la hora %s",
+ "Due on %s" : "Vence'l %s",
+ "DAV system address book" : "Llibreta de direiciones del sistema DAV",
+ "No outstanding DAV system address book sync." : "Nun hai nenguna sincronización pendiente de la llibreta de direiciones del sistema DAV.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Entá nun s'executó la sincronización de les llibretes de direiciones del sistema DAV darréu que la instancia tien más de 1000 usuarios o porque se produxo un error. Execútala manuelamente pente la llamada de «cc dav:sync-system-addressbook».",
+ "WebDAV endpoint" : "Estremu de WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Nun se pudo comprobar que'l sirvidor web se configurare afayadizamente pa permitir la configuración de ficheros per WebDAV. Compruébalo manualmente.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "El sirividor web nun ta configuráu afayadizamente pa permitir la sincronización de ficheros porque la interfaz WebDAV paez tar estropiada.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "El sirvidor web ta configuráu afayadizamente pa permitir la sincronización de ficheros per WebDAV.",
+ "Migrated calendar (%1$s)" : "Calendariu migráu (%1$s)",
+ "Calendars including events, details and attendees" : "Calendarios qu'inclúin eventos, detalles y asistentes",
+ "Contacts and groups" : "Contautos y grupos",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "Guardóse l'ausencia",
+ "Failed to save your absence settings" : "Nun se pue guardar la configuración d'ausencia",
+ "Absence cleared" : "Borróse l'ausencia",
+ "Failed to clear your absence settings" : "Nun se pue borrar la configuración de l'ausencia",
+ "First day" : "El primer día",
+ "Last day (inclusive)" : "L'últimu día (inclusive)",
+ "Short absence status" : "Estáu d'ausencia curtíu",
+ "Long absence Message" : "Mensaxe d'ausencia llongu",
+ "Save" : "Guardar",
+ "Disable absence" : "Desactivar l'ausencia",
+ "Failed to load availability" : "Nun se pue cargar la disponibilidá",
+ "Saved availability" : "Disponibilidá guardada",
+ "Failed to save availability" : "Nun se pue guardar la disponibilidá",
+ "Time zone:" : "Fusu horariu:",
+ "to" : "pa",
+ "Delete slot" : "Desaniciar la ralura",
+ "No working hours set" : "Nun s'afitó nenguna hora llaboral",
+ "Add slot" : "Amestar una ralura",
+ "Weekdays" : "Díes de la selmana",
+ "Pick a start time for {dayName}" : "Escueyi una hora de comienzu pal {dayName}",
+ "Pick a end time for {dayName}" : "Escueyi una hora de fin pal {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Afitar automáticamente l'estáu a «Nun molestar» fuera de la disponibilidá pa desactivar tolos avisos.",
+ "Cancel" : "Encaboxar",
+ "Import" : "Importa",
+ "Error while saving settings" : "Hebo un error mentanto se guardaba la configuración",
+ "Reset to default" : "Reafitar los valores",
+ "Availability" : "Disponibilidá",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Si configures les hores llaborales, les demás persones van ver cuando coles de la oficina al acutar una reunión.",
+ "Absence" : "Ausencia",
+ "Configure your next absence period." : "Configura'l próximu periodu d'ausencia",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instala tamién l'{calendarappstoreopen}aplicación Calendariu{linkclose} o {calendardocopen}conecta'l veceru pa ordenadores y/o móviles pa sincronizar ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Asegúrate de que configuresti afayadizamente'l {emailopen}sirvidor de corréu electrónicu{linkclose}.",
+ "Calendar server" : "Sirvidor de calendarios",
+ "Send invitations to attendees" : "Unviar invitaciones a los asistentes",
+ "Automatically generate a birthday calendar" : "Xenerar automáticamente un calendariu de cumpleaños",
+ "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños xenéralos un trabayu en segundu planu.",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "Polo tanto, nun van tar disponibles inmediatamente tres activalos mas van apaecer dempués d'un tiempu.",
+ "Send notifications for events" : "Unviar avisos de los eventos",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Los avisos únvienlos trabayos en segundu planu, polo que van producise davezu.",
+ "Send reminder notifications to calendar sharees as well" : "Unvia tamién avisos de recordatoriu pa les persones coles que se compartiere'l calendariu",
+ "Reminders are always sent to organizers and attendees." : "Los recordatorios únviense siempres a organizadores y asistentes",
+ "Enable notifications for events via push" : "Acriva los avisos automáticos pa los eventos",
+ "There was an error updating your attendance status." : "Hebo un error al anovar l'estáu de l'asistencia.",
+ "Please contact the organizer directly." : "Ponte en contautu direutamente cola organización.",
+ "Are you accepting the invitation?" : "¿Aceptes la invitación?",
+ "Tentative" : "Provisional",
+ "Your attendance was updated successfully." : "La to asistencia anovóse correutamente."
+},"pluralForm" :"nplurals=2; plural=(n != 1);"
+} \ No newline at end of file
diff --git a/apps/dav/l10n/bg.js b/apps/dav/l10n/bg.js
index f38d2a5cd0b..d8d27bc7d4b 100644
--- a/apps/dav/l10n/bg.js
+++ b/apps/dav/l10n/bg.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Календар",
- "Todos" : "Задачи",
+ "Tasks" : "Задачи",
"Personal" : "Личен",
"{actor} created calendar {calendar}" : "{actor} направи календар {calendar}",
"You created calendar {calendar}" : "Създадохте календара {calendar}",
@@ -25,40 +25,45 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} сподели календар {calendar} с група {group}",
"You unshared calendar {calendar} from group {group}" : "Отказахте споделянето на календара {calendar} от група {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} отказа споделяне с календар {calendar} от група {group}",
+ "Untitled event" : "Събитие без заглавие",
"{actor} created event {event} in calendar {calendar}" : "{actor} създаде събитие {event} в календар {calendar}",
"You created event {event} in calendar {calendar}" : "Създадохте събитие {event} в календар {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} изтри събитие {event} от календар {calendar}",
"You deleted event {event} from calendar {calendar}" : "Изтрихте събитие {event} от календар {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} обнови събитие {event} в календар {calendar}",
"You updated event {event} in calendar {calendar}" : "Обновихте събитие {event} в календар {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} премести събитие {event} от календар {sourceCalendar} в календар {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Преместихте събитие {event} от календар {sourceCalendar} в календар {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} възстанови събитие {event} от календар {calendar}",
"You restored event {event} of calendar {calendar}" : "Вие възстановихте събитие {event} от календар {calendar}",
"Busy" : "Зает",
- "{actor} created todo {todo} in list {calendar}" : "{actor} създаде задача {todo} в списък {calendar}",
- "You created todo {todo} in list {calendar}" : "Създадохте задача {todo} в списък {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} изтри задача {todo} от списък {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Изтрихте задача {todo} от списък {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} актуализира задача {todo} в списък {calendar}",
- "You updated todo {todo} in list {calendar}" : "Променихте задача {todo} в списък {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} разреши задача {todo} в списък {calendar}",
- "You solved todo {todo} in list {calendar}" : "Разрешихте задача {todo} в списък {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} възобнови задача {todo} в списък {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Възобновихте задача {todo} в списък {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} създаде задача {todo} в списък {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Създадохте задача {todo} в списък {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} изтри задача {todo} от списък {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Изтрихте задача {todo} от списък {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} актуализира задача {todo} в списък {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Актуализирахте задачи {todo} в списъка {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} разреши задача {todo} в списък {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Разрешихте задача {todo} в списък {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} отвори отново задача {todo} в списък {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Отворихте отново задача {todo} в списък {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} премести задача {todo} от списък {sourceCalendar} в списък {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Преместихте задача {todo} от списък {sourceCalendar} в списък {targetCalendar}",
"Calendar, contacts and tasks" : "Календар, контакти и задачи",
"A <strong>calendar</strong> was modified" : "Промяна на <strong>календар</strong>",
"A calendar <strong>event</strong> was modified" : "Промяна на календарно <strong>събитие</strong>",
- "A calendar <strong>todo</strong> was modified" : "Промяна на календарна <strong>задача</strong>",
+ "A calendar <strong>to-do</strong> was modified" : "<strong>задача</strong>от календар беше променена",
"Contact birthdays" : "Рождени дни на контакти",
"Death of %s" : "Смърт на %s",
+ "Untitled calendar" : "Нов календар",
"Calendar:" : "Календар:",
"Date:" : "Дата:",
"Where:" : "Къде:",
"Description:" : "Описание:",
- "Untitled event" : "Събитие без заглавие",
- "_%n year_::_%n years_" : ["%n години","%d години"],
+ "_%n year_::_%n years_" : ["%n години","%n години"],
"_%n month_::_%n months_" : ["%n месеци","%n месеци"],
"_%n day_::_%n days_" : ["%n дни","%n дни"],
- "_%n hour_::_%n hours_" : ["%n часове","%n часове"],
+ "_%n hour_::_%n hours_" : ["%n часове","%n часа"],
"_%n minute_::_%n minutes_" : ["%n минути","%n минути"],
"%s (in %s)" : "%s (в %s)",
"%s (%s ago)" : "%s (преди %s)",
@@ -68,21 +73,46 @@ OC.L10N.register(
"Where: %s" : "Къде: %s",
"%1$s via %2$s" : "%1$s чрез %2$s",
"Cancelled: %1$s" : "Отказан: %1$s",
- "Invitation canceled" : "Поканата е отказана",
+ "\"%1$s\" has been canceled" : "„%1$s“ е отказано",
"Re: %1$s" : "Re: %1$s",
- "Invitation updated" : "Поканата е актуализирана",
+ "%1$s has accepted your invitation" : "%1$s е приел поканата ви",
+ "%1$s has tentatively accepted your invitation" : "%1$s е приел поканата ви условно",
+ "%1$s has declined your invitation" : "%1$s е отказал поканата ви",
+ "%1$s has responded to your invitation" : "%1$s отговори/ха на вашата покана",
+ "Invitation updated: %1$s" : "Поканата е актуализирана: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s актуализира събитието „%2$s“",
"Invitation: %1$s" : "Покана: %1$s",
- "Invitation" : "Покана",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s желае да ви покани на „%2$s“",
+ "Organizer:" : "Organizer/организатор/:",
+ "Attendees:" : "Участници:",
"Title:" : "Заглавие:",
- "Time:" : "Час:",
"Location:" : "Местоположение:",
"Link:" : "Връзка:",
- "Organizer:" : "Organizer/организатор/:",
- "Attendees:" : "Участници:",
"Accept" : "Приемане",
"Decline" : "Отхвърляне",
"More options …" : "Още опции ...",
"More options at %s" : "Още опции при %s",
+ "Monday" : "Понеделник",
+ "Tuesday" : "Вторник",
+ "Wednesday" : "Сряда",
+ "Thursday" : "Четвъртък",
+ "Friday" : "Петък",
+ "Saturday" : "Събота",
+ "Sunday" : "Неделя",
+ "January" : "Януари",
+ "February" : "Февруари",
+ "March" : "Март",
+ "April" : "Април",
+ "May" : "Май",
+ "June" : "Юни",
+ "July" : "Юли",
+ "August" : "Август",
+ "September" : "Септември",
+ "October" : "Октомври",
+ "November" : "Ноември",
+ "December" : "Декември",
+ "First" : "Първо",
+ "Last" : "Последно",
"Contacts" : "Контакти",
"{actor} created address book {addressbook}" : "{actor} създаде адресна книга {addressbook}",
"You created address book {addressbook}" : "Вие създадохте адресна книга {addressbook}",
@@ -108,6 +138,7 @@ OC.L10N.register(
"{actor} updated contact {card} in address book {addressbook}" : "{actor} актуализира контакт {card} в адресна книга {addressbook}",
"You updated contact {card} in address book {addressbook}" : "Вие актуализирахте контакт {card} в адресна книга {addressbook}",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "Един <strong>contact</strong> или <strong>address book</strong> са променени",
+ "Accounts" : "Профили",
"File is not updatable: %1$s" : "Файлът не подлежи на актуализиране: %1$s",
"Could not write to final file, canceled by hook" : "Не можа да се запише в крайния файл, анулирано от кука",
"Could not write file contents" : "Съдържанието на файла не можа да се запише",
@@ -117,12 +148,9 @@ OC.L10N.register(
"Could not rename part file to final file, canceled by hook" : "Не можа да се преименува частичен файл в краен файл, анулиран е от кука",
"Could not rename part file to final file" : "Не можа да се преименува частичен файл в краен файл",
"Failed to check file size: %1$s" : "Неуспешна проверка на размера на файла: %1$s",
- "Could not open file" : "Файлът не можа да се отвори",
"Encryption not ready: %1$s" : "Криптирането не е готово: %1$s",
"Failed to open file: %1$s" : "Неуспешно отваряне на файл: %1$s",
"Failed to unlink: %1$s" : "Неуспешно прекратяване на връзката: %1$s",
- "Invalid chunk name" : "Невалидно име на блок",
- "Could not rename part file assembled from chunks" : "Не можа да се преименува частичен файл, сглобен от блок",
"Failed to write file contents: %1$s" : "Неуспешно записване на съдържанието на файла: %1$s",
"File not found: %1$s" : "Файлът не е намерен: %1$s",
"System is in maintenance mode." : "Системата е в режим на поддръжка.",
@@ -131,31 +159,44 @@ OC.L10N.register(
"Configures a CalDAV account" : "Конфигурира профил в CalDAV",
"Configures a CardDAV account" : "Конфигурира профил в CalDAV",
"Events" : "Събития",
- "Tasks" : "Задачи",
"Untitled task" : "Задача без заглавие",
"Completed on %s" : "Завършен на %s",
"Due on %s by %s" : "Краен срок на %s от %s",
"Due on %s" : "Краен срок на %s",
+ "WebDAV endpoint" : "WebDAV крайна точка",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Вашият уеб сървър все още не е удачно настроен да позволява синхронизация на файлове, защото WebDAV интерфейсът не работи.",
"Migrated calendar (%1$s)" : "Мигриран календар (%1$s)",
"Calendars including events, details and attendees" : "Календари, включително събития, подробности и участници",
"Contacts and groups" : "Контакти и групи",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV крайна точка",
- "Availability" : "Наличност",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Ако конфигурирате работното си време, другите потребители ще виждат кога сте извън офиса, при резервиране на среща.",
+ "Absence saved" : "Отсъствието е запаметено",
+ "Failed to save your absence settings" : "Неуспешно запаметяване на настройките за отсъствие",
+ "Absence cleared" : "Отсъствието е изчистено",
+ "Failed to clear your absence settings" : "Неуспешно изчистване на настройките за отсъствие",
+ "First day" : "Първи ден",
+ "Last day (inclusive)" : "До (включително)",
+ "Short absence status" : "Кратко описание за периода на отсъствие",
+ "Long absence Message" : "Съобщение, използвано за периода на отсъствие.",
+ "Save" : "Запазване",
+ "Disable absence" : "Изключи известието за отсъствие",
+ "Failed to load availability" : "Неуспешно зареждане на наличност",
+ "Saved availability" : "Запазена наличност",
+ "Failed to save availability" : "Неуспешно записване на наличност",
"Time zone:" : "Часова зона",
"to" : "до",
"Delete slot" : "Изтриване на слот",
"No working hours set" : "Няма зададено работно време",
"Add slot" : "Добавяне на слот",
- "Monday" : "Понеделник",
- "Tuesday" : "Вторник",
- "Wednesday" : "Сряда",
- "Thursday" : "Четвъртък",
- "Friday" : "Петък",
- "Saturday" : "Събота",
- "Sunday" : "Неделя",
- "Save" : "Запазване",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Автоматично задаване на потребителският статус на „Не безпокойте“ извън достъпността, за заглушаване на всички известия.",
+ "Cancel" : "Отказ",
+ "Import" : "Импортиране /внасяне/",
+ "Error while saving settings" : "Грешка при запазване на настройките",
+ "Reset to default" : "Настройки по подразбиране",
+ "Availability" : "Работно време",
+ "Absence" : "Отсъствия",
+ "Configure your next absence period." : "Задай своето съобщение за отсъствие.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Също така инсталирайте приложението {calendarappstoreopen}Календар{linkclose} или {calendardocopen}, свържете вашия настолен компютър и мобилен телефон за синхронизиране ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Моля, уверете се, че сте настроили правилно {emailopen} имейл сървъра{linkclose}.",
"Calendar server" : "Сървър на календар",
"Send invitations to attendees" : "Изпращане на покани до участниците",
"Automatically generate a birthday calendar" : "Автоматично генериране на календар с рождени дни.",
@@ -166,15 +207,10 @@ OC.L10N.register(
"Send reminder notifications to calendar sharees as well" : "Изпращане на известия за напомняния и до споделящите календар",
"Reminders are always sent to organizers and attendees." : "Напомнянията винаги се изпращат до организаторите и присъстващите.",
"Enable notifications for events via push" : "Активиране на известията за събития чрез push",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Също така инсталирайте приложението {calendarappstoreopen}Календар{linkclose} или {calendardocopen}, свържете вашия настолен компютър и мобилен телефон за синхронизиране ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Моля, уверете се, че сте настроили правилно {emailopen} имейл сървъра{linkclose}.",
"There was an error updating your attendance status." : "Възникна грешка при актуализиране на състоянието на присъствието Ви.",
"Please contact the organizer directly." : "Моля, свържете се директно с организатора.",
"Are you accepting the invitation?" : "Приемате ли поканата?",
"Tentative" : "Несигурно",
- "Number of guests" : "Брой на гостите ",
- "Comment" : "Коментар",
- "Your attendance was updated successfully." : "Вашето присъствие е актуализирано успешно.",
- "Calendar and tasks" : "Календар и задачи"
+ "Your attendance was updated successfully." : "Вашето присъствие е актуализирано успешно."
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/bg.json b/apps/dav/l10n/bg.json
index 09bdfc2a03e..6ebc3b0f6ab 100644
--- a/apps/dav/l10n/bg.json
+++ b/apps/dav/l10n/bg.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Календар",
- "Todos" : "Задачи",
+ "Tasks" : "Задачи",
"Personal" : "Личен",
"{actor} created calendar {calendar}" : "{actor} направи календар {calendar}",
"You created calendar {calendar}" : "Създадохте календара {calendar}",
@@ -23,40 +23,45 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} сподели календар {calendar} с група {group}",
"You unshared calendar {calendar} from group {group}" : "Отказахте споделянето на календара {calendar} от група {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} отказа споделяне с календар {calendar} от група {group}",
+ "Untitled event" : "Събитие без заглавие",
"{actor} created event {event} in calendar {calendar}" : "{actor} създаде събитие {event} в календар {calendar}",
"You created event {event} in calendar {calendar}" : "Създадохте събитие {event} в календар {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} изтри събитие {event} от календар {calendar}",
"You deleted event {event} from calendar {calendar}" : "Изтрихте събитие {event} от календар {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} обнови събитие {event} в календар {calendar}",
"You updated event {event} in calendar {calendar}" : "Обновихте събитие {event} в календар {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} премести събитие {event} от календар {sourceCalendar} в календар {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Преместихте събитие {event} от календар {sourceCalendar} в календар {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} възстанови събитие {event} от календар {calendar}",
"You restored event {event} of calendar {calendar}" : "Вие възстановихте събитие {event} от календар {calendar}",
"Busy" : "Зает",
- "{actor} created todo {todo} in list {calendar}" : "{actor} създаде задача {todo} в списък {calendar}",
- "You created todo {todo} in list {calendar}" : "Създадохте задача {todo} в списък {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} изтри задача {todo} от списък {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Изтрихте задача {todo} от списък {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} актуализира задача {todo} в списък {calendar}",
- "You updated todo {todo} in list {calendar}" : "Променихте задача {todo} в списък {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} разреши задача {todo} в списък {calendar}",
- "You solved todo {todo} in list {calendar}" : "Разрешихте задача {todo} в списък {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} възобнови задача {todo} в списък {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Възобновихте задача {todo} в списък {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} създаде задача {todo} в списък {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Създадохте задача {todo} в списък {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} изтри задача {todo} от списък {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Изтрихте задача {todo} от списък {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} актуализира задача {todo} в списък {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Актуализирахте задачи {todo} в списъка {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} разреши задача {todo} в списък {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Разрешихте задача {todo} в списък {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} отвори отново задача {todo} в списък {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Отворихте отново задача {todo} в списък {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} премести задача {todo} от списък {sourceCalendar} в списък {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Преместихте задача {todo} от списък {sourceCalendar} в списък {targetCalendar}",
"Calendar, contacts and tasks" : "Календар, контакти и задачи",
"A <strong>calendar</strong> was modified" : "Промяна на <strong>календар</strong>",
"A calendar <strong>event</strong> was modified" : "Промяна на календарно <strong>събитие</strong>",
- "A calendar <strong>todo</strong> was modified" : "Промяна на календарна <strong>задача</strong>",
+ "A calendar <strong>to-do</strong> was modified" : "<strong>задача</strong>от календар беше променена",
"Contact birthdays" : "Рождени дни на контакти",
"Death of %s" : "Смърт на %s",
+ "Untitled calendar" : "Нов календар",
"Calendar:" : "Календар:",
"Date:" : "Дата:",
"Where:" : "Къде:",
"Description:" : "Описание:",
- "Untitled event" : "Събитие без заглавие",
- "_%n year_::_%n years_" : ["%n години","%d години"],
+ "_%n year_::_%n years_" : ["%n години","%n години"],
"_%n month_::_%n months_" : ["%n месеци","%n месеци"],
"_%n day_::_%n days_" : ["%n дни","%n дни"],
- "_%n hour_::_%n hours_" : ["%n часове","%n часове"],
+ "_%n hour_::_%n hours_" : ["%n часове","%n часа"],
"_%n minute_::_%n minutes_" : ["%n минути","%n минути"],
"%s (in %s)" : "%s (в %s)",
"%s (%s ago)" : "%s (преди %s)",
@@ -66,21 +71,46 @@
"Where: %s" : "Къде: %s",
"%1$s via %2$s" : "%1$s чрез %2$s",
"Cancelled: %1$s" : "Отказан: %1$s",
- "Invitation canceled" : "Поканата е отказана",
+ "\"%1$s\" has been canceled" : "„%1$s“ е отказано",
"Re: %1$s" : "Re: %1$s",
- "Invitation updated" : "Поканата е актуализирана",
+ "%1$s has accepted your invitation" : "%1$s е приел поканата ви",
+ "%1$s has tentatively accepted your invitation" : "%1$s е приел поканата ви условно",
+ "%1$s has declined your invitation" : "%1$s е отказал поканата ви",
+ "%1$s has responded to your invitation" : "%1$s отговори/ха на вашата покана",
+ "Invitation updated: %1$s" : "Поканата е актуализирана: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s актуализира събитието „%2$s“",
"Invitation: %1$s" : "Покана: %1$s",
- "Invitation" : "Покана",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s желае да ви покани на „%2$s“",
+ "Organizer:" : "Organizer/организатор/:",
+ "Attendees:" : "Участници:",
"Title:" : "Заглавие:",
- "Time:" : "Час:",
"Location:" : "Местоположение:",
"Link:" : "Връзка:",
- "Organizer:" : "Organizer/организатор/:",
- "Attendees:" : "Участници:",
"Accept" : "Приемане",
"Decline" : "Отхвърляне",
"More options …" : "Още опции ...",
"More options at %s" : "Още опции при %s",
+ "Monday" : "Понеделник",
+ "Tuesday" : "Вторник",
+ "Wednesday" : "Сряда",
+ "Thursday" : "Четвъртък",
+ "Friday" : "Петък",
+ "Saturday" : "Събота",
+ "Sunday" : "Неделя",
+ "January" : "Януари",
+ "February" : "Февруари",
+ "March" : "Март",
+ "April" : "Април",
+ "May" : "Май",
+ "June" : "Юни",
+ "July" : "Юли",
+ "August" : "Август",
+ "September" : "Септември",
+ "October" : "Октомври",
+ "November" : "Ноември",
+ "December" : "Декември",
+ "First" : "Първо",
+ "Last" : "Последно",
"Contacts" : "Контакти",
"{actor} created address book {addressbook}" : "{actor} създаде адресна книга {addressbook}",
"You created address book {addressbook}" : "Вие създадохте адресна книга {addressbook}",
@@ -106,6 +136,7 @@
"{actor} updated contact {card} in address book {addressbook}" : "{actor} актуализира контакт {card} в адресна книга {addressbook}",
"You updated contact {card} in address book {addressbook}" : "Вие актуализирахте контакт {card} в адресна книга {addressbook}",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "Един <strong>contact</strong> или <strong>address book</strong> са променени",
+ "Accounts" : "Профили",
"File is not updatable: %1$s" : "Файлът не подлежи на актуализиране: %1$s",
"Could not write to final file, canceled by hook" : "Не можа да се запише в крайния файл, анулирано от кука",
"Could not write file contents" : "Съдържанието на файла не можа да се запише",
@@ -115,12 +146,9 @@
"Could not rename part file to final file, canceled by hook" : "Не можа да се преименува частичен файл в краен файл, анулиран е от кука",
"Could not rename part file to final file" : "Не можа да се преименува частичен файл в краен файл",
"Failed to check file size: %1$s" : "Неуспешна проверка на размера на файла: %1$s",
- "Could not open file" : "Файлът не можа да се отвори",
"Encryption not ready: %1$s" : "Криптирането не е готово: %1$s",
"Failed to open file: %1$s" : "Неуспешно отваряне на файл: %1$s",
"Failed to unlink: %1$s" : "Неуспешно прекратяване на връзката: %1$s",
- "Invalid chunk name" : "Невалидно име на блок",
- "Could not rename part file assembled from chunks" : "Не можа да се преименува частичен файл, сглобен от блок",
"Failed to write file contents: %1$s" : "Неуспешно записване на съдържанието на файла: %1$s",
"File not found: %1$s" : "Файлът не е намерен: %1$s",
"System is in maintenance mode." : "Системата е в режим на поддръжка.",
@@ -129,31 +157,44 @@
"Configures a CalDAV account" : "Конфигурира профил в CalDAV",
"Configures a CardDAV account" : "Конфигурира профил в CalDAV",
"Events" : "Събития",
- "Tasks" : "Задачи",
"Untitled task" : "Задача без заглавие",
"Completed on %s" : "Завършен на %s",
"Due on %s by %s" : "Краен срок на %s от %s",
"Due on %s" : "Краен срок на %s",
+ "WebDAV endpoint" : "WebDAV крайна точка",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Вашият уеб сървър все още не е удачно настроен да позволява синхронизация на файлове, защото WebDAV интерфейсът не работи.",
"Migrated calendar (%1$s)" : "Мигриран календар (%1$s)",
"Calendars including events, details and attendees" : "Календари, включително събития, подробности и участници",
"Contacts and groups" : "Контакти и групи",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV крайна точка",
- "Availability" : "Наличност",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Ако конфигурирате работното си време, другите потребители ще виждат кога сте извън офиса, при резервиране на среща.",
+ "Absence saved" : "Отсъствието е запаметено",
+ "Failed to save your absence settings" : "Неуспешно запаметяване на настройките за отсъствие",
+ "Absence cleared" : "Отсъствието е изчистено",
+ "Failed to clear your absence settings" : "Неуспешно изчистване на настройките за отсъствие",
+ "First day" : "Първи ден",
+ "Last day (inclusive)" : "До (включително)",
+ "Short absence status" : "Кратко описание за периода на отсъствие",
+ "Long absence Message" : "Съобщение, използвано за периода на отсъствие.",
+ "Save" : "Запазване",
+ "Disable absence" : "Изключи известието за отсъствие",
+ "Failed to load availability" : "Неуспешно зареждане на наличност",
+ "Saved availability" : "Запазена наличност",
+ "Failed to save availability" : "Неуспешно записване на наличност",
"Time zone:" : "Часова зона",
"to" : "до",
"Delete slot" : "Изтриване на слот",
"No working hours set" : "Няма зададено работно време",
"Add slot" : "Добавяне на слот",
- "Monday" : "Понеделник",
- "Tuesday" : "Вторник",
- "Wednesday" : "Сряда",
- "Thursday" : "Четвъртък",
- "Friday" : "Петък",
- "Saturday" : "Събота",
- "Sunday" : "Неделя",
- "Save" : "Запазване",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Автоматично задаване на потребителският статус на „Не безпокойте“ извън достъпността, за заглушаване на всички известия.",
+ "Cancel" : "Отказ",
+ "Import" : "Импортиране /внасяне/",
+ "Error while saving settings" : "Грешка при запазване на настройките",
+ "Reset to default" : "Настройки по подразбиране",
+ "Availability" : "Работно време",
+ "Absence" : "Отсъствия",
+ "Configure your next absence period." : "Задай своето съобщение за отсъствие.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Също така инсталирайте приложението {calendarappstoreopen}Календар{linkclose} или {calendardocopen}, свържете вашия настолен компютър и мобилен телефон за синхронизиране ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Моля, уверете се, че сте настроили правилно {emailopen} имейл сървъра{linkclose}.",
"Calendar server" : "Сървър на календар",
"Send invitations to attendees" : "Изпращане на покани до участниците",
"Automatically generate a birthday calendar" : "Автоматично генериране на календар с рождени дни.",
@@ -164,15 +205,10 @@
"Send reminder notifications to calendar sharees as well" : "Изпращане на известия за напомняния и до споделящите календар",
"Reminders are always sent to organizers and attendees." : "Напомнянията винаги се изпращат до организаторите и присъстващите.",
"Enable notifications for events via push" : "Активиране на известията за събития чрез push",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Също така инсталирайте приложението {calendarappstoreopen}Календар{linkclose} или {calendardocopen}, свържете вашия настолен компютър и мобилен телефон за синхронизиране ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Моля, уверете се, че сте настроили правилно {emailopen} имейл сървъра{linkclose}.",
"There was an error updating your attendance status." : "Възникна грешка при актуализиране на състоянието на присъствието Ви.",
"Please contact the organizer directly." : "Моля, свържете се директно с организатора.",
"Are you accepting the invitation?" : "Приемате ли поканата?",
"Tentative" : "Несигурно",
- "Number of guests" : "Брой на гостите ",
- "Comment" : "Коментар",
- "Your attendance was updated successfully." : "Вашето присъствие е актуализирано успешно.",
- "Calendar and tasks" : "Календар и задачи"
+ "Your attendance was updated successfully." : "Вашето присъствие е актуализирано успешно."
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/dav/l10n/ca.js b/apps/dav/l10n/ca.js
index 3b105d418d1..6ed5523a0e5 100644
--- a/apps/dav/l10n/ca.js
+++ b/apps/dav/l10n/ca.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Calendari",
- "Todos" : "Tasques",
+ "Tasks" : "Tasques",
"Personal" : "Personal",
"{actor} created calendar {calendar}" : "{actor} ha creat el calendari {calendar}",
"You created calendar {calendar}" : "Heu creat el calendari {calendar}",
@@ -10,86 +10,162 @@ OC.L10N.register(
"You deleted calendar {calendar}" : "Heu suprimit el calendari {calendar}",
"{actor} updated calendar {calendar}" : "{actor} ha actualitzat el calendari {calendar}",
"You updated calendar {calendar}" : "Heu actualitzat el calendari {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} ha restaurat el calendari {calendar}",
+ "You restored calendar {calendar}" : "Heu restaurat el calendari {calendar}",
"You shared calendar {calendar} as public link" : "Heu compartit el calendari {calendar} amb un enllaç públic",
- "You removed public link for calendar {calendar}" : "Heu eliminat l'enllaç públic del calendari {calendar}",
+ "You removed public link for calendar {calendar}" : "Heu suprimit l'enllaç públic del calendari {calendar}",
"{actor} shared calendar {calendar} with you" : "{actor} ha compartit el calendari {calendar} amb vós",
"You shared calendar {calendar} with {user}" : "Heu compartit el calendari {calendar} amb {user}",
"{actor} shared calendar {calendar} with {user}" : "{actor} ha compartit el calendari {calendar} amb {user}",
"{actor} unshared calendar {calendar} from you" : "{actor} ha deixat de compartir el calendari {calendar} amb vós",
"You unshared calendar {calendar} from {user}" : "Heu deixat de compartir el calendari {calendar} amb {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} no ha compartit el calendari {calendar} amb {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} no comparteixen el calendari {calendar} de si mateixos",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor} ha deixat de compartir el calendari {calendar} amb {user}",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} ha deixat de compartir el calendari {calendar} amb si mateix",
"You shared calendar {calendar} with group {group}" : "Heu compartit el calendari {calendar} amb el grup {group}",
"{actor} shared calendar {calendar} with group {group}" : "{actor} ha compartit el calendari {calendar} amb el grup {group}",
- "You unshared calendar {calendar} from group {group}" : "Heu desactivat el calendari {calendar} del grup {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} calendari no publicat {calendar} del grup {grup}",
+ "You unshared calendar {calendar} from group {group}" : "Heu deixat de compartir el calendari {calendar} amb el grup {group}",
+ "{actor} unshared calendar {calendar} from group {group}" : "{actor} ha deixat de compartir el calendari {calendar} amb el grup {group}",
+ "Untitled event" : "Esdeveniment sense títol",
"{actor} created event {event} in calendar {calendar}" : "{actor} ha creat l'esdeveniment {event} al calendari {calendar}",
"You created event {event} in calendar {calendar}" : "Heu creat l'esdeveniment {event} al calendari {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} ha suprimit l'esdeveniment {esdeveniment} del calendari {calendar}",
+ "{actor} deleted event {event} from calendar {calendar}" : "{actor} ha suprimit l'esdeveniment {event} del calendari {calendar}",
"You deleted event {event} from calendar {calendar}" : "Heu suprimit l'esdeveniment {event} del calendari {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} esdeveniment actualitzat {esdeveniment} al calendari {calendar}",
- "You updated event {event} in calendar {calendar}" : "Heu actualitzat l'esdeveniment {event} al calendari {calendar}",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} ha actualitzat l'esdeveniment {event} del calendari {calendar}",
+ "You updated event {event} in calendar {calendar}" : "Heu actualitzat l'esdeveniment {event} del calendari {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} ha desplaçat l'esdeveniment {event} del calendari {sourceCalendar} al calendari {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Heu desplaçat l'esdeveniment {event} del calendari {sourceCalendar} al calendari {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} ha restaurat l'esdeveniment {event} del calendari {calendar}",
+ "You restored event {event} of calendar {calendar}" : "Heu restaurat l'esdeveniment {event} del calendari {calendar}",
"Busy" : "Ocupat",
- "{actor} created todo {todo} in list {calendar}" : "{actor} ha creat la tasca {todo} a {calendar}",
- "You created todo {todo} in list {calendar}" : "Heu creat la tasca {todo} a {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} ha suprimit la tasca {todo} de la llista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Heu suprimit la tasca {todo} de la llista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} ha actualitzat la tasca {todo} a {calendar}",
- "You updated todo {todo} in list {calendar}" : "Heu actualitzat la tasca {todo} a {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} ha resolt la tasca {todo} a {calendar}",
- "You solved todo {todo} in list {calendar}" : "Heu resolt la tasca {todo} a {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} ha tornat a obrir la tasca {todo} a {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Heu tornat a obrir la tasca {todo} a {calendar}",
- "A <strong>calendar</strong> was modified" : "El <strong>calendari</strong> has estat modificat",
- "A calendar <strong>event</strong> was modified" : "S'ha modificat un <strong> esdeveniment </strong> del calendari",
- "A calendar <strong>todo</strong> was modified" : "S'ha modificat una <strong>tasca</strong> d'un calendari",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} ha creat la tasca pendent {todo} en la llista {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Heu creat la tasca pendent {todo} en la llista {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} ha suprimit la tasca pendent {todo} de la llista {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Heu suprimit la tasca pendent {todo} de la llista {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} ha actualitzat la tasca pendent {todo} de la llista {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Heu actualitzat la tasca pendent {todo} de la llista {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} ha resolt la tasca pendent {todo} de la llista {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Heu resolt la tasca pendent {todo} de la llista {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} ha tornat a obrir la tasca pendent {todo} de la llista {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Heu tornat a obrir la tasca pendent {todo} de la llista {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} ha desplaçat la tasca pendent {todo} de la llista {sourceCalendar} a la llista {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Heu desplaçat la tasca pendent {todo} de la llista {sourceCalendar} a la llista {targetCalendar}",
+ "Calendar, contacts and tasks" : "Calendari, contactes i tasques",
+ "A <strong>calendar</strong> was modified" : "S'ha modificat un <strong>calendari</strong>",
+ "A calendar <strong>event</strong> was modified" : "S'ha modificat un <strong>esdeveniment</strong> del calendari",
+ "A calendar <strong>to-do</strong> was modified" : "S'ha modificat una <strong>tasca pendent</strong> del calendari",
"Contact birthdays" : "Aniversaris dels contactes",
"Death of %s" : "Mort de %s",
+ "Untitled calendar" : "Calendari sense títol",
"Calendar:" : "Calendari:",
"Date:" : "Data:",
- "Where:" : "On:",
+ "Where:" : "Ubicació:",
"Description:" : "Descripció:",
- "Untitled event" : "Esdeveniment sense títol",
"_%n year_::_%n years_" : ["%n any","%n anys"],
"_%n month_::_%n months_" : ["%n mes","%n mesos"],
"_%n day_::_%n days_" : ["%n dia","%n dies"],
"_%n hour_::_%n hours_" : ["%n hora","%n hores"],
"_%n minute_::_%n minutes_" : ["%n minut","%n minuts"],
- "%s (in %s)" : "%s (d'aquí %s)",
+ "%s (in %s)" : "%s (d'aquí a %s)",
"%s (%s ago)" : "%s (fa %s)",
"Calendar: %s" : "Calendari: %s",
"Date: %s" : "Data: %s",
"Description: %s" : "Descripció: %s",
- "Where: %s" : "On: %s",
+ "Where: %s" : "Ubicació: %s",
"%1$s via %2$s" : "%1$s mitjançant %2$s",
- "Invitation canceled" : "Invitació cancel·lada",
- "Invitation updated" : "Invitació actualitzada",
- "Invitation" : "Invitació",
+ "In the past on %1$s for the entire day" : "En el passat a %1$s durant tot el dia",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["En un minut a %1$s durant tot el dia","En %n minuts a %1$s durant tot el dia"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["En una hora a %1$s durant tot el dia","En %n hores a %1$s durant tot el dia"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["En un dia a %1$s durant tot el dia","En %n dies a %1$s durant tot el dia"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["En una setmana a %1$s durant tot el dia","En %n setmanes a %1$s durant tot el dia"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["En un mes el %1$s durant tot el dia","En %n mesos el %1$s durant tot el dia"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["En un any a %1$s durant tot el dia","En %n anys a %1$s durant tot el dia"],
+ "In the past on %1$s between %2$s - %3$s" : "En el passat el dia %1$s entre les %2$s i les %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["D'aquí a un minut el %1$s entre les %2$s i les %3$s","D’aquí a %n minuts el %1$s entre les %2$s i les %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["D'aquí a una hora el %1$s entre les %2$s i les %3$s","D’aquí a %n hores el %1$s entre les %2$s i les %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["En un dia el %1$s entre les %2$s i les %3$s","En %n dies el %1$s entre les %2$s i les %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["En una setmana el %1$s entre les %2$s i les %3$s","En %n setmanes el %1$s entre les %2$s i les %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["D'aquí a un mes el %1$s entre les %2$s i les %3$s","D’aquí a %n mesos el %1$s entre les %2$s i les %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["D'aquí a un any el %1$s entre les %2$s i les %3$s","D’aquí %n anys el %1$s entre les %2$s i les %3$s"],
+ "Could not generate when statement" : "No s'ha pogut generar la declaració de quan",
+ "Every Day for the entire day" : "Cada dia durant tot el dia",
+ "Every Day for the entire day until %1$s" : "Cada dia durant tot el dia fins a les %1$s",
+ "Every Day between %1$s - %2$s" : "Cada dia entre %1$s i %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Cada dia entre %1$s - %2$s fins a les %3$s",
+ "Every %1$d Days for the entire day" : "Cada %1$d dies durant tot el dia",
+ "Every %1$d Days for the entire day until %2$s" : "Cada %1$d dies durant tot el dia fins a les %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Cada %1$d dies entre %2$s i %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Cada %1$d dies entre les %2$s i les %3$s fins a les %4$s",
+ "Could not generate event recurrence statement" : "No s'ha pogut generar la instrucció de recurrència de l'esdeveniment",
+ "Every Week on %1$s for the entire day" : "Cada setmana el %1$s durant tot el dia",
+ "Every Week on %1$s for the entire day until %2$s" : "Cada setmana el %1$s durant tot el dia fins a les %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Cada setmana el %1$s entre les %2$s i les %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Cada setmana el %1$s entre les %2$s i les %3$s fins a les %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Cada %1$d setmanes el dia %2$s durant tot el dia",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Cada %1$d setmanes el %2$s durant tot el dia fins a les %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Cada %1$d setmanes el dia %2$s entre les %3$s i les %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Cada %1$d setmanes el %2$s entre les %3$s i les %4$s fins a les %5$s",
+ "Every Month on the %1$s for the entire day" : "Cada mes a les %1$s durant tot el dia",
+ "Every Month on the %1$s for the entire day until %2$s" : "Cada mes a les %1$s durant tot el dia fins a les %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Cada mes el %1$s entre les %2$s i les %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Cada mes el dia %1$s entre les %2$s i les %3$s fins a les %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Cada %1$d mesos el %2$s durant tot el dia",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Cada %1$d mesos el %2$s durant tot el dia fins a les %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Cada %1$d mesos el %2$s entre les %3$s i les %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Cada %1$d mesos el %2$s entre les %3$s i les %4$s fins a les %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Cada any a %1$s el %2$s durant tot el dia",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Cada any a %1$s el %2$s durant tot el dia fins a les %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Cada any a %1$s el %2$s entre les %3$s i les %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Cada any a %1$s el %2$s entre les %3$s i les %4$s fins a %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Cada %1$d anys a %2$s el %3$s durant tot el dia",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Cada %1$d anys a %2$s el %3$s durant tot el dia fins a les %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Cada %1$d anys a %2$s el %3$s entre les %4$s i les %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Cada %1$d anys a %2$s el %3$s entre les %4$s i les %5$s fins a %6$s",
+ "On specific dates for the entire day until %1$s" : "En dates específiques durant tot el dia fins a les %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "En dates específiques entre les %1$s i les %2$s fins a %3$s",
+ "In the past on %1$s" : "En el passat a %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["En un minut a %1$s durant tot el dia","En %n minuts a %1$s durant tot el dia"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["D'aquí a una hora a %1$s","D’aquí %n hores a %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["En un dia el %1$s","En %n dies el %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["D'aquí a una setmana el %1$s","D’aquí %n setmanes el %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["D'aquí a un mes el %1$s","D’aquí %n mesos el %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["D'aquí a un any el %1$s","D’aquí %n anys el %1$s"],
+ "In the past on %1$s then on %2$s" : "En el passat el %1$s i després el %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["D'aquí a un minut el %1$s i després el %2$s","D’aquí a %n minuts el %1$s i després el %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["D'aquí a una hora el %1$s i després el %2$s","D’aquí %n hores el %1$s i després el %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["En un dia el %1$s i després el %2$s","En %n dies el %1$s i després el %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["D'aquí a una setmana el %1$s i després el %2$s","D’aquí %n setmanes el %1$s i després el %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["D'aquí a un mes el %1$s i després el %2$s","D’aquí %n mesos el %1$s i després el %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["D'aquí a un any el %1$s i després el %2$s","D’aquí %n anys el %1$s i després el %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "En el passat el %1$s, després el %2$s i el %3$s",
+ "_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_" : ["D'aquí a un minut el %1$s i després el %2$s i el %3$s","D’aquí a %n minuts el %1$s i després el %2$s i el %3$s"],
+ "_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_" : ["D'aquí a una hora el %1$s i després el %2$s i el %3$s","D’aquí a %n hores el %1$s i després el %2$s i el %3$s"],
+ "_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_" : ["D'aquí a un dia el %1$s i després el %2$s i el %3$s","D’aquí a %n dies el %1$s i després el %2$s i el %3$s"],
+ "_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_" : ["D'aquí a una setmana el %1$s i després el %2$s i el %3$s","D’aquí a %n setmanes el %1$s i després el %2$s i el %3$s"],
+ "_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_" : ["D'aquí a un mes el %1$s i després el %2$s i el %3$s","D’aquí a %n mesos el %1$s i després el %2$s i el %3$s"],
+ "_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_" : ["D'aquí a un any el %1$s i després el %2$s i el %3$s","D’aquí a %n anys el %1$s i després el %2$s i el %3$s"],
+ "Could not generate next recurrence statement" : "No s'ha pogut generar la següent instrucció de recurrència",
+ "Cancelled: %1$s" : "S'ha cancel·lat: %1$s",
+ "\"%1$s\" has been canceled" : "S'ha cancel·lat «%1$s»",
+ "Re: %1$s" : "Re: %1$s",
+ "%1$s has accepted your invitation" : "%1$s ha acceptat la vostra invitació",
+ "%1$s has tentatively accepted your invitation" : "%1$s ha acceptat provisionalment la vostra invitació",
+ "%1$s has declined your invitation" : "%1$s ha rebutjat la vostra invitació",
+ "%1$s has responded to your invitation" : "%1$s ha respost a la vostra invitació",
+ "Invitation updated: %1$s" : "S'ha actualitzat la invitació: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s ha actualitzat l'esdeveniment «%2$s»",
+ "Invitation: %1$s" : "Invitació: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s vol convidar-vos a «%2$s»",
+ "Organizer:" : "Organització:",
+ "Attendees:" : "Assistents:",
"Title:" : "Títol:",
- "Time:" : "Hora:",
+ "When:" : "Quan:",
"Location:" : "Ubicació:",
"Link:" : "Enllaç:",
- "Organizer:" : "Organitzador:",
- "Attendees:" : "Assistents:",
+ "Occurring:" : "Ocorrent:",
"Accept" : "Accepta",
"Decline" : "Rebutja",
- "More options …" : "Més opcions …",
+ "More options …" : "Més opcions…",
"More options at %s" : "Més opcions a %s",
- "Contacts" : "Contactes",
- "Upgrade needed" : "Fa falta l'actualització",
- "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "El vostre %s cal configurar-se per utilitzar HTTPS per poder fer servir CalDAV i CardDAV amb iOS/macOS.",
- "Configures a CalDAV account" : "Configura un compte CalDAV",
- "Configures a CardDAV account" : "Configura un compte CardDAV",
- "Events" : "Esdeveniments",
- "Tasks" : "Tasques",
- "Untitled task" : "Tasca sense títol",
- "Completed on %s" : "Completat a %s",
- "Due on %s by %s" : "Venciment a %s per %s",
- "Due on %s" : "Venç en %s",
- "Contacts and groups" : "Contactes i grups",
- "WebDAV" : "WebDAV",
- "WebDAV endpoint" : "Punt final de WebDAV",
- "to" : "a",
"Monday" : "Dilluns",
"Tuesday" : "Dimarts",
"Wednesday" : "Dimecres",
@@ -97,23 +173,147 @@ OC.L10N.register(
"Friday" : "Divendres",
"Saturday" : "Dissabte",
"Sunday" : "Diumenge",
+ "January" : "Gener",
+ "February" : "Febrer",
+ "March" : "Març",
+ "April" : "Abril",
+ "May" : "Maig",
+ "June" : "Juny",
+ "July" : "Juliol",
+ "August" : "Agost",
+ "September" : "Setembre",
+ "October" : "Octubre",
+ "November" : "Novembre",
+ "December" : "Desembre",
+ "First" : "Primer",
+ "Second" : "Segon",
+ "Third" : "Tercer",
+ "Fourth" : "Quart",
+ "Fifth" : "Cinquè",
+ "Last" : "Últim",
+ "Second Last" : "Segon últim",
+ "Third Last" : "Tercer últim",
+ "Fourth Last" : "Quart últim",
+ "Fifth Last" : "Cinquè últim",
+ "Contacts" : "Contactes",
+ "{actor} created address book {addressbook}" : "{actor} ha creat la llibreta d'adreces {addressbook}",
+ "You created address book {addressbook}" : "Heu creat la llibreta d'adreces {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} ha suprimit la llibreta d'adreces {addressbook}",
+ "You deleted address book {addressbook}" : "Heu suprimit la llibreta d'adreces {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} ha actualitzat la llibreta d'adreces {addressbook}",
+ "You updated address book {addressbook}" : "Heu actualitzat la llibreta d'adreces {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} ha compartit la llibreta d'adreces {addressbook} amb vós",
+ "You shared address book {addressbook} with {user}" : "Heu compartit la llibreta d'adreces {addressbook} amb {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} ha compartit la llibreta d'adreces {addressbook} amb {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} ha deixat de compartir la llibreta d'adreces {addressbook} amb vós",
+ "You unshared address book {addressbook} from {user}" : "Heu deixat de compartir la llibreta d'adreces {addressbook} de {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} ha deixat de compartir la llibreta d'adreces {addressbook} amb {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} ha deixat de compartir la llibreta d'adreces {addressbook} amb si mateix",
+ "You shared address book {addressbook} with group {group}" : "Heu compartit la llibreta d'adreces {addressbook} amb el grup {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} ha compartit la llibreta d'adreces {addressbook} amb el grup {group}",
+ "You unshared address book {addressbook} from group {group}" : "Heu deixat de compartir la llibreta d'adreces {addressbook} amb el grup {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} ha deixat de compartir la llibreta d'adreces {addressbook} amb el grup {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} ha creat el contacte {card} en la llibreta d'adreces {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Heu creat el contacte {card} en la llibreta d'adreces {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} ha suprimit el contacte {card} de la llibreta d'adreces {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Heu suprimit el contacte {card} de la llibreta d'adreces {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} ha actualitzat el contacte {card} de la llibreta d'adreces {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Heu actualitzat el contacte {card} de la llibreta d'adreces {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "S'ha modificat un <strong>contacte</strong> o una <strong>llibreta d'adreces</strong>",
+ "Accounts" : "Comptes",
+ "System address book which holds all accounts" : "Llibreta d'adreces del sistema que conté tots els comptes",
+ "File is not updatable: %1$s" : "El fitxer no es pot actualitzar: %1$s",
+ "Failed to get storage for file" : "No s'ha pogut obtenir emmagatzematge per al fitxer",
+ "Could not write to final file, canceled by hook" : "No s'ha pogut escriure el fitxer final perquè el ganxo ho ha cancel·lat",
+ "Could not write file contents" : "No s'ha pogut escriure el contingut del fitxer",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "S'ha produït un error en copiar el fitxer a la ubicació de destinació (dades copiades: %1$s, mida esperada del fitxer: %2$s)",
+ "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." : "S'esperava una mida de fitxer de %1$s, però s'han llegit (des del client del Nextcloud) i s'han escrit (a l'emmagatzematge del Nextcloud) %2$s. Pot ser un problema de xarxa del costat d'enviament o un problema d'escriptura a l'emmagatzematge del costat del servidor.",
+ "Could not rename part file to final file, canceled by hook" : "No s'ha pogut canviar el nom del fitxer de part al fitxer final perquè el ganxo ho ha cancel·lat",
+ "Could not rename part file to final file" : "No s'ha pogut canviar el nom del fitxer de part al fitxer final",
+ "Failed to check file size: %1$s" : "No s'ha pogut comprovar la mida del fitxer: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "No s'ha pogut obrir el fitxer: %1$s, el fitxer sembla que existeix",
+ "Could not open file: %1$s, file doesn't seem to exist" : "No s'ha pogut obrir el fitxer: %1$s, sembla que el fitxer no existeix",
+ "Encryption not ready: %1$s" : "El xifratge no està preparat: %1$s",
+ "Failed to open file: %1$s" : "No s'ha pogut obrir el fitxer: %1$s",
+ "Failed to unlink: %1$s" : "No s'ha pogut desenllaçar: %1$s",
+ "Failed to write file contents: %1$s" : "No s'ha pogut escriure el contingut del fitxer: %1$s",
+ "File not found: %1$s" : "No s'ha trobat el fitxer: %1$s",
+ "Invalid target path" : "Camí de destinació no vàlid",
+ "System is in maintenance mode." : "El sistema és en mode de manteniment.",
+ "Upgrade needed" : "Cal actualitzar",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Cal que configureu el %s perquè utilitzi HTTPS per a poder utilitzar CalDAV i CardDAV amb l'iOS/macOS.",
+ "Configures a CalDAV account" : "Configura un compte CalDAV",
+ "Configures a CardDAV account" : "Configura un compte CardDAV",
+ "Events" : "Esdeveniments",
+ "Untitled task" : "Tasca sense títol",
+ "Completed on %s" : "Completada el %s",
+ "Due on %s by %s" : "Venç el %s a les %s",
+ "Due on %s" : "Venç el %s",
+ "System Address Book" : "Llibreta d'adreces del sistema",
+ "The system address book contains contact information for all users in your instance." : "La llibreta d'adreces del sistema conté informació de contacte de tots els usuaris de la vostra instància.",
+ "Enable System Address Book" : "Habilita la llibreta d'adreces del sistema",
+ "DAV system address book" : "Llibreta d'adreces del sistema DAV",
+ "No outstanding DAV system address book sync." : "No hi ha cap sincronització pendent de la llibreta d'adreces del sistema DAV.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "La sincronització de la llibreta d'adreces del sistema DAV encara no s'ha executat perquè la instància té més de 1000 usuaris o a causa d'un error. Executeu-la manualment amb «occ dav:sync-system-addressbook».",
+ "WebDAV endpoint" : "Extrem WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "No s'ha pogut comprovar que el vostre servidor web estigui configurat correctament per a permetre la sincronització de fitxers mitjançant WebDAV. Comproveu-ho manualment.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "El vostre servidor web no està configurat correctament per a permetre la sincronització de fitxers perquè sembla que la interfície WebDAV no funciona correctament.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "El vostre servidor web està configurat correctament per a permetre la sincronització de fitxers mitjançant WebDAV.",
+ "Migrated calendar (%1$s)" : "Calendari migrat (%1$s)",
+ "Calendars including events, details and attendees" : "Calendaris amb esdeveniments, detalls i assistents",
+ "Contacts and groups" : "Contactes i grups",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "S'ha desat l'absència",
+ "Failed to save your absence settings" : "No s'han pogut desar els paràmetres de l'absència",
+ "Absence cleared" : "S'ha esborrat l'absència",
+ "Failed to clear your absence settings" : "No s'han pogut esborrar els paràmetres de l'absència",
+ "First day" : "Primer dia",
+ "Last day (inclusive)" : "Darrer dia (inclòs)",
+ "Out of office replacement (optional)" : "Substitució fora de l'oficina (opcional)",
+ "Name of the replacement" : "Nom del substitut",
+ "No results." : "Cap resultat.",
+ "Start typing." : "Comença a escriure.",
+ "Short absence status" : "Estat d'absència breu",
+ "Long absence Message" : "Missatge d'absència llarga",
"Save" : "Desa",
+ "Disable absence" : "Inhabilita l'absència",
+ "Failed to load availability" : "No s'ha pogut carregar la disponibilitat",
+ "Saved availability" : "S'ha desat la disponibilitat",
+ "Failed to save availability" : "No s'ha pogut desar la disponibilitat",
+ "Time zone:" : "Fus horari:",
+ "to" : "a",
+ "Delete slot" : "Suprimeix la franja",
+ "No working hours set" : "No s'ha definit cap horari laboral",
+ "Add slot" : "Afegeix una franja",
+ "Weekdays" : "Dies de la setmana",
+ "Pick a start time for {dayName}" : "Trieu una hora d'inici per a {dayName}",
+ "Pick a end time for {dayName}" : "Trieu una hora de finalització per a {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Estableix automàticament l'estat de l'usuari com a \"No molesteu” fora de la disponibilitat per a silenciar totes les notificacions.",
+ "Cancel" : "Cancel·la",
+ "Import" : "Importa",
+ "Error while saving settings" : "S'ha produït un error en desar els paràmetres",
+ "Reset to default" : "Reinicialitza els valors per defecte",
+ "Availability" : "Disponibilitat",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Si configureu el vostre horari laboral, la resta de persones veuran quan sou fora de l'oficina quan planifiquin una reunió.",
+ "Absence" : "Absència",
+ "Configure your next absence period." : "Configureu el pròxim període d'absència.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instal·leu també {calendarappstoreopen}l'aplicació Calendari{linkclose} o {calendardocopen}connecteu el vostre dispositiu d'escriptori i el mòbil per a sincronitzar-los ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Assegureu-vos de configurar correctament el{emailopen}servidor de correu electrònic{linkclose}.",
"Calendar server" : "Servidor de calendari",
"Send invitations to attendees" : "Envia invitacions als assistents",
- "Automatically generate a birthday calendar" : "Genera automàticament un calendari d’aniversari",
- "Birthday calendars will be generated by a background job." : "Els calendaris d'aniversari es generaran per un procés en segon pla..",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Per tant, no estaran disponibles immediatament després d'habilitar-los, però apareixeran d'aquí una estona.",
+ "Automatically generate a birthday calendar" : "Genera automàticament un calendari d’aniversaris",
+ "Birthday calendars will be generated by a background job." : "Un procés en segon pla generarà els calendaris d'aniversaris.",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "Per tant, no estaran disponibles immediatament després d'habilitar-los, però apareixeran després d'una estona.",
"Send notifications for events" : "Envia notificacions per als esdeveniments",
- "Notifications are sent via background jobs, so these must occur often enough." : "Les notificacions s'envien per tasques funcionant en segon pla, així que això ha de succeir bastant sovint.",
- "Enable notifications for events via push" : "Habiliteu les notificacions per a esdeveniments mitjançant l’empenyiment",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instal·leu també {calendarappstoreopen}l’aplicació de Calendari{linkclose} o {calendardocopen}connecteu el vostre escriptori i el mòbil per sincronitzar ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Si us plau, assegureu-vos de configurar correctament {emailopen}el servidor de correu electrònic{linkclose}.",
- "There was an error updating your attendance status." : "S'ha produït un error en actualitzar l'estat de la vostra assistència.",
- "Please contact the organizer directly." : "Si us plau contacteu amb l'organitzador directament.",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Les notificacions les envien feines en segon pla, de manera que han d'ocórrer prou sovint.",
+ "Send reminder notifications to calendar sharees as well" : "Envia també notificacions de recordatori als usuaris amb qui s'ha compartit el calendari",
+ "Reminders are always sent to organizers and attendees." : "Sempre s'envien recordatoris als organitzadors i als assistents.",
+ "Enable notifications for events via push" : "Habilita les notificacions automàtiques per als esdeveniments",
+ "There was an error updating your attendance status." : "S'ha produït un error en actualitzar l'estat d'assistència.",
+ "Please contact the organizer directly." : "Contacteu amb l'organització directament.",
"Are you accepting the invitation?" : "Accepteu la invitació?",
"Tentative" : "Provisional",
- "Comment" : "Comentari",
- "Your attendance was updated successfully." : "La vostra assistència ha estat actualitzada correctament.",
- "Calendar and tasks" : "Calendari i tasques"
+ "Your attendance was updated successfully." : "S'ha actualitzat correctament l'assistència."
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/ca.json b/apps/dav/l10n/ca.json
index 298839b7186..88cb071325a 100644
--- a/apps/dav/l10n/ca.json
+++ b/apps/dav/l10n/ca.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Calendari",
- "Todos" : "Tasques",
+ "Tasks" : "Tasques",
"Personal" : "Personal",
"{actor} created calendar {calendar}" : "{actor} ha creat el calendari {calendar}",
"You created calendar {calendar}" : "Heu creat el calendari {calendar}",
@@ -8,86 +8,162 @@
"You deleted calendar {calendar}" : "Heu suprimit el calendari {calendar}",
"{actor} updated calendar {calendar}" : "{actor} ha actualitzat el calendari {calendar}",
"You updated calendar {calendar}" : "Heu actualitzat el calendari {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} ha restaurat el calendari {calendar}",
+ "You restored calendar {calendar}" : "Heu restaurat el calendari {calendar}",
"You shared calendar {calendar} as public link" : "Heu compartit el calendari {calendar} amb un enllaç públic",
- "You removed public link for calendar {calendar}" : "Heu eliminat l'enllaç públic del calendari {calendar}",
+ "You removed public link for calendar {calendar}" : "Heu suprimit l'enllaç públic del calendari {calendar}",
"{actor} shared calendar {calendar} with you" : "{actor} ha compartit el calendari {calendar} amb vós",
"You shared calendar {calendar} with {user}" : "Heu compartit el calendari {calendar} amb {user}",
"{actor} shared calendar {calendar} with {user}" : "{actor} ha compartit el calendari {calendar} amb {user}",
"{actor} unshared calendar {calendar} from you" : "{actor} ha deixat de compartir el calendari {calendar} amb vós",
"You unshared calendar {calendar} from {user}" : "Heu deixat de compartir el calendari {calendar} amb {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} no ha compartit el calendari {calendar} amb {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} no comparteixen el calendari {calendar} de si mateixos",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor} ha deixat de compartir el calendari {calendar} amb {user}",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} ha deixat de compartir el calendari {calendar} amb si mateix",
"You shared calendar {calendar} with group {group}" : "Heu compartit el calendari {calendar} amb el grup {group}",
"{actor} shared calendar {calendar} with group {group}" : "{actor} ha compartit el calendari {calendar} amb el grup {group}",
- "You unshared calendar {calendar} from group {group}" : "Heu desactivat el calendari {calendar} del grup {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} calendari no publicat {calendar} del grup {grup}",
+ "You unshared calendar {calendar} from group {group}" : "Heu deixat de compartir el calendari {calendar} amb el grup {group}",
+ "{actor} unshared calendar {calendar} from group {group}" : "{actor} ha deixat de compartir el calendari {calendar} amb el grup {group}",
+ "Untitled event" : "Esdeveniment sense títol",
"{actor} created event {event} in calendar {calendar}" : "{actor} ha creat l'esdeveniment {event} al calendari {calendar}",
"You created event {event} in calendar {calendar}" : "Heu creat l'esdeveniment {event} al calendari {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} ha suprimit l'esdeveniment {esdeveniment} del calendari {calendar}",
+ "{actor} deleted event {event} from calendar {calendar}" : "{actor} ha suprimit l'esdeveniment {event} del calendari {calendar}",
"You deleted event {event} from calendar {calendar}" : "Heu suprimit l'esdeveniment {event} del calendari {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} esdeveniment actualitzat {esdeveniment} al calendari {calendar}",
- "You updated event {event} in calendar {calendar}" : "Heu actualitzat l'esdeveniment {event} al calendari {calendar}",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} ha actualitzat l'esdeveniment {event} del calendari {calendar}",
+ "You updated event {event} in calendar {calendar}" : "Heu actualitzat l'esdeveniment {event} del calendari {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} ha desplaçat l'esdeveniment {event} del calendari {sourceCalendar} al calendari {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Heu desplaçat l'esdeveniment {event} del calendari {sourceCalendar} al calendari {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} ha restaurat l'esdeveniment {event} del calendari {calendar}",
+ "You restored event {event} of calendar {calendar}" : "Heu restaurat l'esdeveniment {event} del calendari {calendar}",
"Busy" : "Ocupat",
- "{actor} created todo {todo} in list {calendar}" : "{actor} ha creat la tasca {todo} a {calendar}",
- "You created todo {todo} in list {calendar}" : "Heu creat la tasca {todo} a {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} ha suprimit la tasca {todo} de la llista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Heu suprimit la tasca {todo} de la llista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} ha actualitzat la tasca {todo} a {calendar}",
- "You updated todo {todo} in list {calendar}" : "Heu actualitzat la tasca {todo} a {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} ha resolt la tasca {todo} a {calendar}",
- "You solved todo {todo} in list {calendar}" : "Heu resolt la tasca {todo} a {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} ha tornat a obrir la tasca {todo} a {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Heu tornat a obrir la tasca {todo} a {calendar}",
- "A <strong>calendar</strong> was modified" : "El <strong>calendari</strong> has estat modificat",
- "A calendar <strong>event</strong> was modified" : "S'ha modificat un <strong> esdeveniment </strong> del calendari",
- "A calendar <strong>todo</strong> was modified" : "S'ha modificat una <strong>tasca</strong> d'un calendari",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} ha creat la tasca pendent {todo} en la llista {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Heu creat la tasca pendent {todo} en la llista {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} ha suprimit la tasca pendent {todo} de la llista {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Heu suprimit la tasca pendent {todo} de la llista {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} ha actualitzat la tasca pendent {todo} de la llista {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Heu actualitzat la tasca pendent {todo} de la llista {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} ha resolt la tasca pendent {todo} de la llista {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Heu resolt la tasca pendent {todo} de la llista {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} ha tornat a obrir la tasca pendent {todo} de la llista {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Heu tornat a obrir la tasca pendent {todo} de la llista {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} ha desplaçat la tasca pendent {todo} de la llista {sourceCalendar} a la llista {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Heu desplaçat la tasca pendent {todo} de la llista {sourceCalendar} a la llista {targetCalendar}",
+ "Calendar, contacts and tasks" : "Calendari, contactes i tasques",
+ "A <strong>calendar</strong> was modified" : "S'ha modificat un <strong>calendari</strong>",
+ "A calendar <strong>event</strong> was modified" : "S'ha modificat un <strong>esdeveniment</strong> del calendari",
+ "A calendar <strong>to-do</strong> was modified" : "S'ha modificat una <strong>tasca pendent</strong> del calendari",
"Contact birthdays" : "Aniversaris dels contactes",
"Death of %s" : "Mort de %s",
+ "Untitled calendar" : "Calendari sense títol",
"Calendar:" : "Calendari:",
"Date:" : "Data:",
- "Where:" : "On:",
+ "Where:" : "Ubicació:",
"Description:" : "Descripció:",
- "Untitled event" : "Esdeveniment sense títol",
"_%n year_::_%n years_" : ["%n any","%n anys"],
"_%n month_::_%n months_" : ["%n mes","%n mesos"],
"_%n day_::_%n days_" : ["%n dia","%n dies"],
"_%n hour_::_%n hours_" : ["%n hora","%n hores"],
"_%n minute_::_%n minutes_" : ["%n minut","%n minuts"],
- "%s (in %s)" : "%s (d'aquí %s)",
+ "%s (in %s)" : "%s (d'aquí a %s)",
"%s (%s ago)" : "%s (fa %s)",
"Calendar: %s" : "Calendari: %s",
"Date: %s" : "Data: %s",
"Description: %s" : "Descripció: %s",
- "Where: %s" : "On: %s",
+ "Where: %s" : "Ubicació: %s",
"%1$s via %2$s" : "%1$s mitjançant %2$s",
- "Invitation canceled" : "Invitació cancel·lada",
- "Invitation updated" : "Invitació actualitzada",
- "Invitation" : "Invitació",
+ "In the past on %1$s for the entire day" : "En el passat a %1$s durant tot el dia",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["En un minut a %1$s durant tot el dia","En %n minuts a %1$s durant tot el dia"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["En una hora a %1$s durant tot el dia","En %n hores a %1$s durant tot el dia"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["En un dia a %1$s durant tot el dia","En %n dies a %1$s durant tot el dia"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["En una setmana a %1$s durant tot el dia","En %n setmanes a %1$s durant tot el dia"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["En un mes el %1$s durant tot el dia","En %n mesos el %1$s durant tot el dia"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["En un any a %1$s durant tot el dia","En %n anys a %1$s durant tot el dia"],
+ "In the past on %1$s between %2$s - %3$s" : "En el passat el dia %1$s entre les %2$s i les %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["D'aquí a un minut el %1$s entre les %2$s i les %3$s","D’aquí a %n minuts el %1$s entre les %2$s i les %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["D'aquí a una hora el %1$s entre les %2$s i les %3$s","D’aquí a %n hores el %1$s entre les %2$s i les %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["En un dia el %1$s entre les %2$s i les %3$s","En %n dies el %1$s entre les %2$s i les %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["En una setmana el %1$s entre les %2$s i les %3$s","En %n setmanes el %1$s entre les %2$s i les %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["D'aquí a un mes el %1$s entre les %2$s i les %3$s","D’aquí a %n mesos el %1$s entre les %2$s i les %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["D'aquí a un any el %1$s entre les %2$s i les %3$s","D’aquí %n anys el %1$s entre les %2$s i les %3$s"],
+ "Could not generate when statement" : "No s'ha pogut generar la declaració de quan",
+ "Every Day for the entire day" : "Cada dia durant tot el dia",
+ "Every Day for the entire day until %1$s" : "Cada dia durant tot el dia fins a les %1$s",
+ "Every Day between %1$s - %2$s" : "Cada dia entre %1$s i %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Cada dia entre %1$s - %2$s fins a les %3$s",
+ "Every %1$d Days for the entire day" : "Cada %1$d dies durant tot el dia",
+ "Every %1$d Days for the entire day until %2$s" : "Cada %1$d dies durant tot el dia fins a les %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Cada %1$d dies entre %2$s i %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Cada %1$d dies entre les %2$s i les %3$s fins a les %4$s",
+ "Could not generate event recurrence statement" : "No s'ha pogut generar la instrucció de recurrència de l'esdeveniment",
+ "Every Week on %1$s for the entire day" : "Cada setmana el %1$s durant tot el dia",
+ "Every Week on %1$s for the entire day until %2$s" : "Cada setmana el %1$s durant tot el dia fins a les %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Cada setmana el %1$s entre les %2$s i les %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Cada setmana el %1$s entre les %2$s i les %3$s fins a les %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Cada %1$d setmanes el dia %2$s durant tot el dia",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Cada %1$d setmanes el %2$s durant tot el dia fins a les %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Cada %1$d setmanes el dia %2$s entre les %3$s i les %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Cada %1$d setmanes el %2$s entre les %3$s i les %4$s fins a les %5$s",
+ "Every Month on the %1$s for the entire day" : "Cada mes a les %1$s durant tot el dia",
+ "Every Month on the %1$s for the entire day until %2$s" : "Cada mes a les %1$s durant tot el dia fins a les %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Cada mes el %1$s entre les %2$s i les %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Cada mes el dia %1$s entre les %2$s i les %3$s fins a les %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Cada %1$d mesos el %2$s durant tot el dia",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Cada %1$d mesos el %2$s durant tot el dia fins a les %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Cada %1$d mesos el %2$s entre les %3$s i les %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Cada %1$d mesos el %2$s entre les %3$s i les %4$s fins a les %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Cada any a %1$s el %2$s durant tot el dia",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Cada any a %1$s el %2$s durant tot el dia fins a les %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Cada any a %1$s el %2$s entre les %3$s i les %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Cada any a %1$s el %2$s entre les %3$s i les %4$s fins a %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Cada %1$d anys a %2$s el %3$s durant tot el dia",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Cada %1$d anys a %2$s el %3$s durant tot el dia fins a les %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Cada %1$d anys a %2$s el %3$s entre les %4$s i les %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Cada %1$d anys a %2$s el %3$s entre les %4$s i les %5$s fins a %6$s",
+ "On specific dates for the entire day until %1$s" : "En dates específiques durant tot el dia fins a les %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "En dates específiques entre les %1$s i les %2$s fins a %3$s",
+ "In the past on %1$s" : "En el passat a %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["En un minut a %1$s durant tot el dia","En %n minuts a %1$s durant tot el dia"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["D'aquí a una hora a %1$s","D’aquí %n hores a %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["En un dia el %1$s","En %n dies el %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["D'aquí a una setmana el %1$s","D’aquí %n setmanes el %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["D'aquí a un mes el %1$s","D’aquí %n mesos el %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["D'aquí a un any el %1$s","D’aquí %n anys el %1$s"],
+ "In the past on %1$s then on %2$s" : "En el passat el %1$s i després el %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["D'aquí a un minut el %1$s i després el %2$s","D’aquí a %n minuts el %1$s i després el %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["D'aquí a una hora el %1$s i després el %2$s","D’aquí %n hores el %1$s i després el %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["En un dia el %1$s i després el %2$s","En %n dies el %1$s i després el %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["D'aquí a una setmana el %1$s i després el %2$s","D’aquí %n setmanes el %1$s i després el %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["D'aquí a un mes el %1$s i després el %2$s","D’aquí %n mesos el %1$s i després el %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["D'aquí a un any el %1$s i després el %2$s","D’aquí %n anys el %1$s i després el %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "En el passat el %1$s, després el %2$s i el %3$s",
+ "_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_" : ["D'aquí a un minut el %1$s i després el %2$s i el %3$s","D’aquí a %n minuts el %1$s i després el %2$s i el %3$s"],
+ "_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_" : ["D'aquí a una hora el %1$s i després el %2$s i el %3$s","D’aquí a %n hores el %1$s i després el %2$s i el %3$s"],
+ "_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_" : ["D'aquí a un dia el %1$s i després el %2$s i el %3$s","D’aquí a %n dies el %1$s i després el %2$s i el %3$s"],
+ "_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_" : ["D'aquí a una setmana el %1$s i després el %2$s i el %3$s","D’aquí a %n setmanes el %1$s i després el %2$s i el %3$s"],
+ "_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_" : ["D'aquí a un mes el %1$s i després el %2$s i el %3$s","D’aquí a %n mesos el %1$s i després el %2$s i el %3$s"],
+ "_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_" : ["D'aquí a un any el %1$s i després el %2$s i el %3$s","D’aquí a %n anys el %1$s i després el %2$s i el %3$s"],
+ "Could not generate next recurrence statement" : "No s'ha pogut generar la següent instrucció de recurrència",
+ "Cancelled: %1$s" : "S'ha cancel·lat: %1$s",
+ "\"%1$s\" has been canceled" : "S'ha cancel·lat «%1$s»",
+ "Re: %1$s" : "Re: %1$s",
+ "%1$s has accepted your invitation" : "%1$s ha acceptat la vostra invitació",
+ "%1$s has tentatively accepted your invitation" : "%1$s ha acceptat provisionalment la vostra invitació",
+ "%1$s has declined your invitation" : "%1$s ha rebutjat la vostra invitació",
+ "%1$s has responded to your invitation" : "%1$s ha respost a la vostra invitació",
+ "Invitation updated: %1$s" : "S'ha actualitzat la invitació: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s ha actualitzat l'esdeveniment «%2$s»",
+ "Invitation: %1$s" : "Invitació: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s vol convidar-vos a «%2$s»",
+ "Organizer:" : "Organització:",
+ "Attendees:" : "Assistents:",
"Title:" : "Títol:",
- "Time:" : "Hora:",
+ "When:" : "Quan:",
"Location:" : "Ubicació:",
"Link:" : "Enllaç:",
- "Organizer:" : "Organitzador:",
- "Attendees:" : "Assistents:",
+ "Occurring:" : "Ocorrent:",
"Accept" : "Accepta",
"Decline" : "Rebutja",
- "More options …" : "Més opcions …",
+ "More options …" : "Més opcions…",
"More options at %s" : "Més opcions a %s",
- "Contacts" : "Contactes",
- "Upgrade needed" : "Fa falta l'actualització",
- "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "El vostre %s cal configurar-se per utilitzar HTTPS per poder fer servir CalDAV i CardDAV amb iOS/macOS.",
- "Configures a CalDAV account" : "Configura un compte CalDAV",
- "Configures a CardDAV account" : "Configura un compte CardDAV",
- "Events" : "Esdeveniments",
- "Tasks" : "Tasques",
- "Untitled task" : "Tasca sense títol",
- "Completed on %s" : "Completat a %s",
- "Due on %s by %s" : "Venciment a %s per %s",
- "Due on %s" : "Venç en %s",
- "Contacts and groups" : "Contactes i grups",
- "WebDAV" : "WebDAV",
- "WebDAV endpoint" : "Punt final de WebDAV",
- "to" : "a",
"Monday" : "Dilluns",
"Tuesday" : "Dimarts",
"Wednesday" : "Dimecres",
@@ -95,23 +171,147 @@
"Friday" : "Divendres",
"Saturday" : "Dissabte",
"Sunday" : "Diumenge",
+ "January" : "Gener",
+ "February" : "Febrer",
+ "March" : "Març",
+ "April" : "Abril",
+ "May" : "Maig",
+ "June" : "Juny",
+ "July" : "Juliol",
+ "August" : "Agost",
+ "September" : "Setembre",
+ "October" : "Octubre",
+ "November" : "Novembre",
+ "December" : "Desembre",
+ "First" : "Primer",
+ "Second" : "Segon",
+ "Third" : "Tercer",
+ "Fourth" : "Quart",
+ "Fifth" : "Cinquè",
+ "Last" : "Últim",
+ "Second Last" : "Segon últim",
+ "Third Last" : "Tercer últim",
+ "Fourth Last" : "Quart últim",
+ "Fifth Last" : "Cinquè últim",
+ "Contacts" : "Contactes",
+ "{actor} created address book {addressbook}" : "{actor} ha creat la llibreta d'adreces {addressbook}",
+ "You created address book {addressbook}" : "Heu creat la llibreta d'adreces {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} ha suprimit la llibreta d'adreces {addressbook}",
+ "You deleted address book {addressbook}" : "Heu suprimit la llibreta d'adreces {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} ha actualitzat la llibreta d'adreces {addressbook}",
+ "You updated address book {addressbook}" : "Heu actualitzat la llibreta d'adreces {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} ha compartit la llibreta d'adreces {addressbook} amb vós",
+ "You shared address book {addressbook} with {user}" : "Heu compartit la llibreta d'adreces {addressbook} amb {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} ha compartit la llibreta d'adreces {addressbook} amb {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} ha deixat de compartir la llibreta d'adreces {addressbook} amb vós",
+ "You unshared address book {addressbook} from {user}" : "Heu deixat de compartir la llibreta d'adreces {addressbook} de {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} ha deixat de compartir la llibreta d'adreces {addressbook} amb {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} ha deixat de compartir la llibreta d'adreces {addressbook} amb si mateix",
+ "You shared address book {addressbook} with group {group}" : "Heu compartit la llibreta d'adreces {addressbook} amb el grup {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} ha compartit la llibreta d'adreces {addressbook} amb el grup {group}",
+ "You unshared address book {addressbook} from group {group}" : "Heu deixat de compartir la llibreta d'adreces {addressbook} amb el grup {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} ha deixat de compartir la llibreta d'adreces {addressbook} amb el grup {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} ha creat el contacte {card} en la llibreta d'adreces {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Heu creat el contacte {card} en la llibreta d'adreces {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} ha suprimit el contacte {card} de la llibreta d'adreces {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Heu suprimit el contacte {card} de la llibreta d'adreces {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} ha actualitzat el contacte {card} de la llibreta d'adreces {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Heu actualitzat el contacte {card} de la llibreta d'adreces {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "S'ha modificat un <strong>contacte</strong> o una <strong>llibreta d'adreces</strong>",
+ "Accounts" : "Comptes",
+ "System address book which holds all accounts" : "Llibreta d'adreces del sistema que conté tots els comptes",
+ "File is not updatable: %1$s" : "El fitxer no es pot actualitzar: %1$s",
+ "Failed to get storage for file" : "No s'ha pogut obtenir emmagatzematge per al fitxer",
+ "Could not write to final file, canceled by hook" : "No s'ha pogut escriure el fitxer final perquè el ganxo ho ha cancel·lat",
+ "Could not write file contents" : "No s'ha pogut escriure el contingut del fitxer",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "S'ha produït un error en copiar el fitxer a la ubicació de destinació (dades copiades: %1$s, mida esperada del fitxer: %2$s)",
+ "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." : "S'esperava una mida de fitxer de %1$s, però s'han llegit (des del client del Nextcloud) i s'han escrit (a l'emmagatzematge del Nextcloud) %2$s. Pot ser un problema de xarxa del costat d'enviament o un problema d'escriptura a l'emmagatzematge del costat del servidor.",
+ "Could not rename part file to final file, canceled by hook" : "No s'ha pogut canviar el nom del fitxer de part al fitxer final perquè el ganxo ho ha cancel·lat",
+ "Could not rename part file to final file" : "No s'ha pogut canviar el nom del fitxer de part al fitxer final",
+ "Failed to check file size: %1$s" : "No s'ha pogut comprovar la mida del fitxer: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "No s'ha pogut obrir el fitxer: %1$s, el fitxer sembla que existeix",
+ "Could not open file: %1$s, file doesn't seem to exist" : "No s'ha pogut obrir el fitxer: %1$s, sembla que el fitxer no existeix",
+ "Encryption not ready: %1$s" : "El xifratge no està preparat: %1$s",
+ "Failed to open file: %1$s" : "No s'ha pogut obrir el fitxer: %1$s",
+ "Failed to unlink: %1$s" : "No s'ha pogut desenllaçar: %1$s",
+ "Failed to write file contents: %1$s" : "No s'ha pogut escriure el contingut del fitxer: %1$s",
+ "File not found: %1$s" : "No s'ha trobat el fitxer: %1$s",
+ "Invalid target path" : "Camí de destinació no vàlid",
+ "System is in maintenance mode." : "El sistema és en mode de manteniment.",
+ "Upgrade needed" : "Cal actualitzar",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Cal que configureu el %s perquè utilitzi HTTPS per a poder utilitzar CalDAV i CardDAV amb l'iOS/macOS.",
+ "Configures a CalDAV account" : "Configura un compte CalDAV",
+ "Configures a CardDAV account" : "Configura un compte CardDAV",
+ "Events" : "Esdeveniments",
+ "Untitled task" : "Tasca sense títol",
+ "Completed on %s" : "Completada el %s",
+ "Due on %s by %s" : "Venç el %s a les %s",
+ "Due on %s" : "Venç el %s",
+ "System Address Book" : "Llibreta d'adreces del sistema",
+ "The system address book contains contact information for all users in your instance." : "La llibreta d'adreces del sistema conté informació de contacte de tots els usuaris de la vostra instància.",
+ "Enable System Address Book" : "Habilita la llibreta d'adreces del sistema",
+ "DAV system address book" : "Llibreta d'adreces del sistema DAV",
+ "No outstanding DAV system address book sync." : "No hi ha cap sincronització pendent de la llibreta d'adreces del sistema DAV.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "La sincronització de la llibreta d'adreces del sistema DAV encara no s'ha executat perquè la instància té més de 1000 usuaris o a causa d'un error. Executeu-la manualment amb «occ dav:sync-system-addressbook».",
+ "WebDAV endpoint" : "Extrem WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "No s'ha pogut comprovar que el vostre servidor web estigui configurat correctament per a permetre la sincronització de fitxers mitjançant WebDAV. Comproveu-ho manualment.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "El vostre servidor web no està configurat correctament per a permetre la sincronització de fitxers perquè sembla que la interfície WebDAV no funciona correctament.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "El vostre servidor web està configurat correctament per a permetre la sincronització de fitxers mitjançant WebDAV.",
+ "Migrated calendar (%1$s)" : "Calendari migrat (%1$s)",
+ "Calendars including events, details and attendees" : "Calendaris amb esdeveniments, detalls i assistents",
+ "Contacts and groups" : "Contactes i grups",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "S'ha desat l'absència",
+ "Failed to save your absence settings" : "No s'han pogut desar els paràmetres de l'absència",
+ "Absence cleared" : "S'ha esborrat l'absència",
+ "Failed to clear your absence settings" : "No s'han pogut esborrar els paràmetres de l'absència",
+ "First day" : "Primer dia",
+ "Last day (inclusive)" : "Darrer dia (inclòs)",
+ "Out of office replacement (optional)" : "Substitució fora de l'oficina (opcional)",
+ "Name of the replacement" : "Nom del substitut",
+ "No results." : "Cap resultat.",
+ "Start typing." : "Comença a escriure.",
+ "Short absence status" : "Estat d'absència breu",
+ "Long absence Message" : "Missatge d'absència llarga",
"Save" : "Desa",
+ "Disable absence" : "Inhabilita l'absència",
+ "Failed to load availability" : "No s'ha pogut carregar la disponibilitat",
+ "Saved availability" : "S'ha desat la disponibilitat",
+ "Failed to save availability" : "No s'ha pogut desar la disponibilitat",
+ "Time zone:" : "Fus horari:",
+ "to" : "a",
+ "Delete slot" : "Suprimeix la franja",
+ "No working hours set" : "No s'ha definit cap horari laboral",
+ "Add slot" : "Afegeix una franja",
+ "Weekdays" : "Dies de la setmana",
+ "Pick a start time for {dayName}" : "Trieu una hora d'inici per a {dayName}",
+ "Pick a end time for {dayName}" : "Trieu una hora de finalització per a {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Estableix automàticament l'estat de l'usuari com a \"No molesteu” fora de la disponibilitat per a silenciar totes les notificacions.",
+ "Cancel" : "Cancel·la",
+ "Import" : "Importa",
+ "Error while saving settings" : "S'ha produït un error en desar els paràmetres",
+ "Reset to default" : "Reinicialitza els valors per defecte",
+ "Availability" : "Disponibilitat",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Si configureu el vostre horari laboral, la resta de persones veuran quan sou fora de l'oficina quan planifiquin una reunió.",
+ "Absence" : "Absència",
+ "Configure your next absence period." : "Configureu el pròxim període d'absència.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instal·leu també {calendarappstoreopen}l'aplicació Calendari{linkclose} o {calendardocopen}connecteu el vostre dispositiu d'escriptori i el mòbil per a sincronitzar-los ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Assegureu-vos de configurar correctament el{emailopen}servidor de correu electrònic{linkclose}.",
"Calendar server" : "Servidor de calendari",
"Send invitations to attendees" : "Envia invitacions als assistents",
- "Automatically generate a birthday calendar" : "Genera automàticament un calendari d’aniversari",
- "Birthday calendars will be generated by a background job." : "Els calendaris d'aniversari es generaran per un procés en segon pla..",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Per tant, no estaran disponibles immediatament després d'habilitar-los, però apareixeran d'aquí una estona.",
+ "Automatically generate a birthday calendar" : "Genera automàticament un calendari d’aniversaris",
+ "Birthday calendars will be generated by a background job." : "Un procés en segon pla generarà els calendaris d'aniversaris.",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "Per tant, no estaran disponibles immediatament després d'habilitar-los, però apareixeran després d'una estona.",
"Send notifications for events" : "Envia notificacions per als esdeveniments",
- "Notifications are sent via background jobs, so these must occur often enough." : "Les notificacions s'envien per tasques funcionant en segon pla, així que això ha de succeir bastant sovint.",
- "Enable notifications for events via push" : "Habiliteu les notificacions per a esdeveniments mitjançant l’empenyiment",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instal·leu també {calendarappstoreopen}l’aplicació de Calendari{linkclose} o {calendardocopen}connecteu el vostre escriptori i el mòbil per sincronitzar ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Si us plau, assegureu-vos de configurar correctament {emailopen}el servidor de correu electrònic{linkclose}.",
- "There was an error updating your attendance status." : "S'ha produït un error en actualitzar l'estat de la vostra assistència.",
- "Please contact the organizer directly." : "Si us plau contacteu amb l'organitzador directament.",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Les notificacions les envien feines en segon pla, de manera que han d'ocórrer prou sovint.",
+ "Send reminder notifications to calendar sharees as well" : "Envia també notificacions de recordatori als usuaris amb qui s'ha compartit el calendari",
+ "Reminders are always sent to organizers and attendees." : "Sempre s'envien recordatoris als organitzadors i als assistents.",
+ "Enable notifications for events via push" : "Habilita les notificacions automàtiques per als esdeveniments",
+ "There was an error updating your attendance status." : "S'ha produït un error en actualitzar l'estat d'assistència.",
+ "Please contact the organizer directly." : "Contacteu amb l'organització directament.",
"Are you accepting the invitation?" : "Accepteu la invitació?",
"Tentative" : "Provisional",
- "Comment" : "Comentari",
- "Your attendance was updated successfully." : "La vostra assistència ha estat actualitzada correctament.",
- "Calendar and tasks" : "Calendari i tasques"
+ "Your attendance was updated successfully." : "S'ha actualitzat correctament l'assistència."
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/dav/l10n/cs.js b/apps/dav/l10n/cs.js
index 69d0de5f453..ec86f1ce320 100644
--- a/apps/dav/l10n/cs.js
+++ b/apps/dav/l10n/cs.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Kalendář",
- "Todos" : "Úkoly",
+ "Tasks" : "Úkoly",
"Personal" : "Osobní",
"{actor} created calendar {calendar}" : "{actor} vytvořil(a) kalendář {calendar}",
"You created calendar {calendar}" : "Vytvořili jste kalendář {calendar}",
@@ -25,36 +25,41 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} nasdílel(a) kalendář {calendar} skupině {group}",
"You unshared calendar {calendar} from group {group}" : "Zrušili jste sdílení kalendáře {calendar} skupině {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} přestal(a) sdílet kalendář {calendar} se skupinou {group}",
+ "Untitled event" : "Událost bez názvu",
"{actor} created event {event} in calendar {calendar}" : "{actor} vytvořil(a) událost {event} v kalendáři {calendar}",
"You created event {event} in calendar {calendar}" : "V kalendáři {calendar} jste vytvořili událost {event}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} z kalendáře {calendar} smazal(a) událost {event}",
"You deleted event {event} from calendar {calendar}" : "Smazali jste událost {event} z kalendáře {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} aktualizoval(a) událost {event} v kalendáři {calendar}",
"You updated event {event} in calendar {calendar}" : "Aktualizovali jste událost {event} v kalendáři {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} přesunul(a) událost {event} z kalendáře {sourceCalendar} do kalendáře {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Přesunuli jste událost {event} z kalendáře {sourceCalendar} do kalendáře {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} obnovil(a) událost {event} kalendáře {calendar}",
"You restored event {event} of calendar {calendar}" : "Obnovili jste událost {event} kalendáře {calendar}",
"Busy" : "Zaneprázdněn(a)",
- "{actor} created todo {todo} in list {calendar}" : "{actor} vytvořil(a) v seznamu {calendar} vytvořila úkol {todo}",
- "You created todo {todo} in list {calendar}" : "V seznamu {calendar} jste vytvořili úkol {todo}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} smazal(a) úkol {todo} ze seznamu {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Ze seznamu {calendar} jste smazali úkol {todo}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} aktualizoval(a) úkol {todo} v seznamu {calendar}",
- "You updated todo {todo} in list {calendar}" : "Aktualizovali jste úkol {todo} v seznamu {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} vyřešil(a) úkol {todo} v seznamu {calendar}",
- "You solved todo {todo} in list {calendar}" : "Vyřešili jste úkol {todo} v seznamu {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} znovu otevřel(a) úkol {todo} v seznamu {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Znovu jste otevřeli úkol {todo} v seznamu {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} vytvořil(a) úkol {todo} v seznamu {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Vytvořili jste úkol {todo} v seznamu {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} vymazal(a) úkol {todo} ze seznamu {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Vymazali jste úkol {todo} ze seznamu {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} aktualizoval(a) úkol {todo} v seznamu {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Aktualizovali jste úkol {todo} v seznamu {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} vyřešil(a) úkol {todo} v seznamu {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Vyřešili jste úkol {todo} v seznamu {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} znovu otevřel(a) úkol {todo} v seznamu {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Znovuotevřeli jste úkol {todo} v seznamu {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} přesunul(a) úkol {todo} ze seznamu {sourceCalendar} do seznamu {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Přesunuli jste úkol {todo} ze seznamu {sourceCalendar} do seznamu {targetCalendar}",
"Calendar, contacts and tasks" : "Kalendář, kontakty a úkoly",
"A <strong>calendar</strong> was modified" : "<strong>Kalendář</strong> byl změněn",
"A calendar <strong>event</strong> was modified" : "<strong>Událost</strong> v kalendáři byla změněna",
- "A calendar <strong>todo</strong> was modified" : "<strong>Úkol</strong> v kalendáři byl změněn",
+ "A calendar <strong>to-do</strong> was modified" : "Kalendář <strong>úkoly</strong> byl upraven",
"Contact birthdays" : "Narozeniny kontaktů",
"Death of %s" : "Datum úmrtí %s",
+ "Untitled calendar" : "Nepojmenovaný kalendář",
"Calendar:" : "Kalendář:",
"Date:" : "Datum:",
"Where:" : "Kde:",
"Description:" : "Popis:",
- "Untitled event" : "Událost bez názvu",
"_%n year_::_%n years_" : ["%n rok","%n roky","%n let","%n roky"],
"_%n month_::_%n months_" : ["%n měsíc","%n měsíce","%n měsíců","%n měsíce"],
"_%n day_::_%n days_" : ["%n den","%n dny","%n dnů","%n dny"],
@@ -67,22 +72,129 @@ OC.L10N.register(
"Description: %s" : "Popis: %s",
"Where: %s" : "Kde: %s",
"%1$s via %2$s" : "%1$s prostřednictvím %2$s",
+ "In the past on %1$s for the entire day" : "V minulosti %1$s po celý den",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Za minutu %2$s po celý den","Za %n minuty %1$s po celý den","Za %n minut %1$s po celý den","Za %n minuty %1$s po celý den"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Za hodinu %1$s po celý den","Za %n hodiny %1$s po celý den","Za %n hodin %1$s po celý den","Za %n hodiny %1$s po celý den"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Za den %1$s po celý den","Za %n dny %1$s po celý den","Za %n dnů %1$s po celý den","Za %n dny %1$s po celý den"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Za týden %1$s po celý den","Za %n týdny %1$s po celý den","Za %n týdnů %1$s po celý den","Za %n týdny %1$s po celý den"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Za měsíc %2$s po celý den","Za %n měsíce %1$s po celý den","Za %n měsíců %1$s po celý den","Za %n měsíce %1$s po celý den"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Za rok %1$s po celý den","Za %n roky %1$s po celý den","Za %n let %1$s po celý den","Za %n roky %1$s po celý den"],
+ "In the past on %1$s between %2$s - %3$s" : "V minulosti %1$s mezi %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Za minutu %1$s mezi %2$s - %3$s","Za %n minuty %1$s mezi %2$s - %3$s","Za %n minut %1$s mezi %2$s - %3$s","Za %n minuty %1$s mezi %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Za hodinu %1$s mezi %2$s - %3$s ","Za %n hodiny %1$s mezi %2$s - %3$s","Za %n hodin %1$s mezi %2$s - %3$s","Za %n hodiny %1$s mezi %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Za den %1$s mezi %2$s - %3$s","Za %n dny %1$s mezi %2$s - %3$s","Za %n dnů %1$s mezi %2$s - %3$s","Za %n dny %1$s mezi %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Za týden %1$s mezi %2$s - %3$s","Za %n týdny %1$s mezi %2$s - %3$s","Za %n týdnů %1$s mezi %2$s - %3$s","Za %n týdny %1$s mezi %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Za měsíc %1$s mezi %2$s - %3$s","Za %n měsíce %1$s mezi %2$s - %3$s","Za %n měsíců %1$s mezi %2$s - %3$s","Za %n měsíce %1$s mezi %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Za rok %1$s mezi %2$s - %3$s","Za %n roky %1$s mezi %2$s - %3$s","Za %n let %1$s mezi %2$s - %3$s","Za %n roky %1$s mezi %2$s - %3$s"],
+ "Could not generate when statement" : "Nepodařilo se vytvořit výrok kdy",
+ "Every Day for the entire day" : "Každý den pro celý den",
+ "Every Day for the entire day until %1$s" : "Každý den pro celý den až do %1$s",
+ "Every Day between %1$s - %2$s" : "Každý den mezi %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Každý den mezi %1$s - %2$s až do %3$s",
+ "Every %1$d Days for the entire day" : "Co %1$d dnů po celý den",
+ "Every %1$d Days for the entire day until %2$s" : "Co %1$d dnů po celý den až do %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Co %1$d dnů mezi %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Co %1$d dnů mezi %2$s - %3$s až do %4$s",
+ "Could not generate event recurrence statement" : "Nepodařilo se vytvořit výrok o opakování události",
+ "Every Week on %1$s for the entire day" : "Každý týden v(e) %1$s po celý den",
+ "Every Week on %1$s for the entire day until %2$s" : "Každý týden v(e) %1$s po celý den až do %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Každý týden v %1$s mezi %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Každý týden v(e) %1$s mezi %2$s - %3$s až do %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Každých %1$d týdnů v(e) %2$s po celý den",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Co %1$d týdnů v(e) %2$s po celý den až do %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Co %1$d týdnů v(e) %2$s mezi %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Co %1$d týdnů v(e) %2$s mezi %3$s - %4$s až do %5$s",
+ "Every Month on the %1$s for the entire day" : "Každý měsíc v %1$s po celý den",
+ "Every Month on the %1$s for the entire day until %2$s" : "Každý měsíc v %1$s po celý den až do %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Každý měsíc v %1$s mezi %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Každý měsíc v %1$s mezi %2$s - %3$s až do %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Co %1$d měsíců v %2$s po celý den",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Co %1$d měsíců v %2$s po celý den až do %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Co %1$d měsíců v %2$s mezi %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Co %1$d měsíců v %2$s mezi %3$s - %4$s až do %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Každoročně %1$s v %2$s po celý den",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Každoročně v %1$s v %2$s po celý den až do %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Každoročně v %1$s v %2$s mezi %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Každoročně v %1$s v %2$s mezi %3$s - %4$s až do %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Co %1$d let v %2$s v %3$s po celý den",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Co %1$d let v %2$s v %3$s po celý den až do %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Co %1$d let v %2$s v %3$s mezi %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Co %1$d let %2$s v %3$s mezi %4$s - %5$s až do %6$s",
+ "On specific dates for the entire day until %1$s" : "V konkrétních datech pro celý den až do %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "V konkrétních datech mezi %1$s - %2$s až do %3$s",
+ "In the past on %1$s" : "V minulosti %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Za minutu %1$s","Za %n minuty %1$s","Za %n minut %1$s","Za %n minuty %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Za hodinu %1$s","Za %n hodiny %1$s","Za %n hodin %1$s","Za %n hodiny %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Za den %1$s","Za %n dny %1$s","Za %n dnů %1$s","Za %n dny %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Za týden %1$s","Za %n týdny %1$s","Za %n týdnů %1$s","Za %n týdny %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Za měsíce %1$s","Za %n měsíce %1$s","Za %n měsíců %1$s","Za %n měsíce %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Za rok %1$s","Za %n roky %1$s","Za %n let %1$s","Za %n roky %1$s"],
+ "In the past on %1$s then on %2$s" : "V minulosti %1$s, pak %2$s ",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Za minutu %1$s, poté %2$s","Za %n minuty %1$s, poté %2$s","Za %n minut %1$s, poté %2$s ","Za %n minuty %1$s, poté %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Za hodinu %1$s, poté %2$s","Za %n hodiny %1$s, poté %2$s","Za %n hodin %1$s, poté %2$s","Za %n hodiny %1$s, poté %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Za den %1$s, poté %2$s","Za %n dny %1$s, poté %2$s","Za %n dnů %1$s, poté %2$s","Za %n dny %1$s, poté %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Za týden %1$s, poté %2$s","Za %n týdny %1$s, poté %2$s","Za %n týdnů %1$s, poté %2$s","Za %n týdny %1$s, poté %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Za měsíc %1$s, poté %2$s","Za %n měsíce %1$s, poté %2$s","Za %n měsíců %1$s, poté %2$s","Za %n měsíce %1$s, poté %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Za rok %1$s, poté %2$s","Za %n roky %1$s, poté %2$s","Za %n let %1$s, poté %2$s","Za %n roky %1$s, poté %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "V minulosti %1$s, poté %2$s a %3$s ",
+ "_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_" : ["Za minutu %1$s, poté %2$s a %3$s ","Za %n minuty %1$s, poté %2$s a %3$s ","Za %n minut %1$s, poté %2$s a %3$s ","Za %n minuty %1$s, poté %2$s a %3$s "],
+ "_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_" : ["Za hodinu %1$s, poté %2$s a %3$s","Za %n hodiny %1$s, poté %2$s a %3$s","Za %n hodin %1$s, poté %2$s a %3$s","Za %n hodiny %1$s, poté %2$s a %3$s"],
+ "_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_" : ["Za den %1$s, poté %2$s a %3$s","Za %n dny %1$s, poté %2$s a %3$s","Za %n dnů %1$s, poté %2$s a %3$s","Za %n dny %1$s, poté %2$s a %3$s"],
+ "_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_" : ["Za týden %1$s, poté %2$s a %3$s","Za %n týdny %1$s, poté %2$s a %3$s","Za %n týdnů %1$s, poté %2$s a %3$s","Za %n týdny %1$s, poté %2$s a %3$s"],
+ "_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_" : ["Za měsíc %1$s, poté %2$s a %3$s","Za %n měsíce %1$s, poté %2$s a %3$s","Za %n měsíců %1$s, poté %2$s a %3$s","Za %n měsíce %1$s, poté %2$s a %3$s"],
+ "_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_" : ["Za rok %1$s, poté %2$s a %3$s","Za %n roky %1$s, poté %2$s a %3$s","Za %n let %1$s, poté %2$s a %3$s","Za %n roky %1$s, poté %2$s a %3$s"],
+ "Could not generate next recurrence statement" : "Nepodařilo se vytvořit výrok o příštím opakování",
"Cancelled: %1$s" : "Zrušeno: %1$s",
- "Invitation canceled" : "Pozvánka zrušena",
+ "\"%1$s\" has been canceled" : "„%1$s“ bylo zrušeno",
"Re: %1$s" : "Odp.: %1$s",
- "Invitation updated" : "Pozvánka aktualizována",
+ "%1$s has accepted your invitation" : "%1$s přijal(a) vaše pozvání",
+ "%1$s has tentatively accepted your invitation" : "%1$s nezávazně přijal(a) vaši pozvánku",
+ "%1$s has declined your invitation" : "%1$s odmítla(a) vaši pozvánku",
+ "%1$s has responded to your invitation" : "%1$s odpověděl(a) na vaši pozvánku",
+ "Invitation updated: %1$s" : "Pozvánka aktualizována: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s zaktualizoval(a) událost „%2$s",
"Invitation: %1$s" : "Pozvánka: %1$s",
- "Invitation" : "Pozvání",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s by vás ráda pozval(a) na „%2$s“",
+ "Organizer:" : "Organizátor:",
+ "Attendees:" : "Účastníci:",
"Title:" : "Název:",
- "Time:" : "Čas:",
+ "When:" : "Kdy:",
"Location:" : "Umístění:",
"Link:" : "Odkaz:",
- "Organizer:" : "Organizátor:",
- "Attendees:" : "Účastníci:",
+ "Occurring:" : "Opakuje se::",
"Accept" : "Přijmout",
- "Decline" : "Zamítnout",
+ "Decline" : "Odmítnout",
"More options …" : "Další volby…",
"More options at %s" : "Další volby viz %s",
+ "Monday" : "pondělí",
+ "Tuesday" : "úterý",
+ "Wednesday" : "středa",
+ "Thursday" : "čtvrtek",
+ "Friday" : "pátek",
+ "Saturday" : "sobota",
+ "Sunday" : "neděle",
+ "January" : "leden",
+ "February" : "únor",
+ "March" : "březen",
+ "April" : "duben",
+ "May" : "květen",
+ "June" : "červen",
+ "July" : "červenec",
+ "August" : "srpen",
+ "September" : "září",
+ "October" : "říjen",
+ "November" : "listopad",
+ "December" : "prosinec",
+ "First" : "První",
+ "Second" : "Sekund",
+ "Third" : "Třetí",
+ "Fourth" : "Čtvrtý",
+ "Fifth" : "Páté",
+ "Last" : "Poslední",
+ "Second Last" : "Druhý poslední",
+ "Third Last" : "Třetí poslední",
+ "Fourth Last" : "Čtvrtý poslední",
+ "Fifth Last" : "Pátý poslední",
"Contacts" : "Kontakty",
"{actor} created address book {addressbook}" : "{actor} vytvořil(a) adresář kontaktů {addressbook}",
"You created address book {addressbook}" : "Vytvořili jste adresář kontaktů {addressbook}",
@@ -108,7 +220,10 @@ OC.L10N.register(
"{actor} updated contact {card} in address book {addressbook}" : "{actor} upravil(a) kontakt {card} v adresáři kontaktů {addressbook}",
"You updated contact {card} in address book {addressbook}" : "Upravili jste kontakt {card} v adresáři kontaktů {addressbook}",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>Kontakt</strong> nebo <strong>adresář kontaktů</strong> byl upraven",
+ "Accounts" : "Účty",
+ "System address book which holds all accounts" : "Systémový adresář kontaktů, který obsahuje veškeré účty",
"File is not updatable: %1$s" : "Soubor není možné zaktualizovat: %1$s",
+ "Failed to get storage for file" : "Nepodařilo se získat úložiště pro soubor",
"Could not write to final file, canceled by hook" : "Nedaří se zapsat do konečného souboru – zrušeno háčkem (hook)",
"Could not write file contents" : "Nedaří se zapsat obsahy souborů",
"_%n byte_::_%n bytes_" : ["%n bajt","%n bajty","%n bajtů","%n bajty"],
@@ -117,47 +232,92 @@ OC.L10N.register(
"Could not rename part file to final file, canceled by hook" : "Nedaří se zapsat přejmenovat částečný soubor na ten končený – zrušeno háčkem (hook)",
"Could not rename part file to final file" : "Nedaří se přejmenovat částečný soubor na ten konečný",
"Failed to check file size: %1$s" : "Nepodařilo se zkontrolovat velikost souboru: %1$s",
- "Could not open file" : "Nedaří se otevřít soubor",
+ "Could not open file: %1$s, file does seem to exist" : "Nebylo možné otevřít soubor: %1$s – soubor zdá se existuje",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Nebylo možné otevřít soubor: %1$s – soubor zdá se neexistuje",
"Encryption not ready: %1$s" : "Šifrování není připraveno: %1$s",
"Failed to open file: %1$s" : "Nepodařilo se otevřít soubor: %1$s",
"Failed to unlink: %1$s" : "Nepodařilo se zrušit propojení: %1$s",
- "Invalid chunk name" : "Neplatný název pro shluk (chunk)",
- "Could not rename part file assembled from chunks" : "Nedaří se přejmenovat částečný soubor složený ze shluků",
"Failed to write file contents: %1$s" : "Nepodařilo se zapsat obsahy souborů: %1$s",
"File not found: %1$s" : "Soubor nenalezen: %1$s",
- "System is in maintenance mode." : "Na systému právě probíhá údržba.",
+ "Invalid target path" : "Neplatný popis umístění cíle",
+ "System is in maintenance mode." : "Systém se právě nachází v režimu údržby.",
"Upgrade needed" : "Je třeba přejít na novější verzi",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Váš %s potřebuje být nastavený aby používal HTTPS, aby bylo možné používat CalDAV a CardDAV s iOS/macOS.",
"Configures a CalDAV account" : "Nastaví CalDAV účet",
"Configures a CardDAV account" : "Nastaví CardDAV účet",
"Events" : "Události",
- "Tasks" : "Úkoly",
"Untitled task" : "Nepojmenovaný úkol",
"Completed on %s" : "Dokončeno %s",
"Due on %s by %s" : "Termín do %s od %s",
"Due on %s" : "Termín do %s",
+ "Example event - open me!" : "Událost pro ukázku – otevřete ji!",
+ "System Address Book" : "Systémový adresář kontaktů",
+ "The system address book contains contact information for all users in your instance." : "Systémový adresář kontaktů obsahuje informace pro všechny uživatele ve vámi využívané instanci.",
+ "Enable System Address Book" : "Zapnout systémový adresář kontaktů",
+ "DAV system address book" : "Systémový DAV adresář kontaktů",
+ "No outstanding DAV system address book sync." : "Žádná zbývající synchronizace systémového DAV adresáře kontaktů.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV synchronizace systémového adresáře kontaktů ještě nebyla spuštěná protože vámi využívaná instance má více než 1 000 uživatelů nebo protože došlo k chybě. Spusťte ji ručně příkazem „occ dav:sync-system-addressbook“.",
+ "WebDAV endpoint" : "WebDAV endpoint",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Nepodařilo se zkontrolovat že vámi využívaný webový server je správně nastaven tak, aby umožňoval synchronizaci přes WebDAV. Zkontrolujte to ručně.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Váš webový server ještě není správně nastaven, pro umožnění synchronizace souborů, rozhraní WebDAV pravděpodobně není funkční.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Vámi využívaný webový server je správně nastaven pro umotnění synchronizace souborů přes WebDAV",
"Migrated calendar (%1$s)" : "Přesunut kalendář (%1$s)",
"Calendars including events, details and attendees" : "Kalendáře včetně událostí, podrobností a účastníků",
"Contacts and groups" : "Kontakty a skupiny",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV endpoint",
- "Availability" : "Dostupnost",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Když sem zadáte svou pracovní dobu, ostatní uživatelé při rezervování schůzky uvidí, kdy jste mimo kancelář.",
+ "Absence saved" : "Nepřítomnost uložena",
+ "Failed to save your absence settings" : "Nepodařilo se uložit nastavení vaší nepřítomnosti",
+ "Absence cleared" : "Nepřítomnost vyčištěna",
+ "Failed to clear your absence settings" : "Nepodařilo se vymazat nastavení vaší nepřítomnosti",
+ "First day" : "První den",
+ "Last day (inclusive)" : "Poslední den (včetně)",
+ "Out of office replacement (optional)" : "Zástup když mimo kancelář (volitelné)",
+ "Name of the replacement" : "Jméno zástupu",
+ "No results." : "Nic nenalezeno.",
+ "Start typing." : "Začněte psát.",
+ "Short absence status" : "Stav krátké nepřítomnosti",
+ "Long absence Message" : "Zpráva pro dlouhou nepřítomnost",
+ "Save" : "Uložit",
+ "Disable absence" : "Vypnout nepřítomnost",
+ "Failed to load availability" : "Nepodařilo se načíst dostupnost",
+ "Saved availability" : "Uložena dostupnost",
+ "Failed to save availability" : "Nepodařilo se uložit dostupnost",
"Time zone:" : "Časové pásmo:",
"to" : "do",
"Delete slot" : "Smazat slot",
"No working hours set" : "Nenastaveny pracovní hodiny",
"Add slot" : "Přidat slot",
- "Monday" : "pondělí",
- "Tuesday" : "úterý",
- "Wednesday" : "středa",
- "Thursday" : "čtvrtek",
- "Friday" : "pátek",
- "Saturday" : "sobota",
- "Sunday" : "neděle",
- "Save" : "Uložit",
+ "Weekdays" : "Dny v týdnu",
+ "Pick a start time for {dayName}" : "Vyberte začátek pro {dayName}",
+ "Pick a end time for {dayName}" : "Vyberte konec pro {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "V době, kdy není k dispozici, automaticky nastavit stav uživatele na „Nerušit“ a ztlumit tak veškerá upozornění.",
+ "Cancel" : "Storno",
+ "Import" : "Naimportovat",
+ "Error while saving settings" : "Chyba při ukládání nastavení",
+ "Contact reset successfully" : "Kontakt úspěšně resetován",
+ "Error while resetting contact" : "Chyba při resetování kontaktu",
+ "Contact imported successfully" : "Kontakt úspěšně naimportován",
+ "Error while importing contact" : "Chyba při importování kontaktu",
+ "Import contact" : "Importovat kontakt",
+ "Reset to default" : "Vrátit zpět na výchozí hodnoty",
+ "Import contacts" : "Importovat kontakty",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Naimportování nového .vcf souboru smaže stávající výchozí kontakt a nahradí ho novým. Chcete pokračovat?",
+ "Failed to save example event creation setting" : "Nepodařilo se uložit nastavení vytváření události pro ukázku",
+ "Failed to upload the example event" : "Nepodařilo se nahrát událost pro ukázku",
+ "Custom example event was saved successfully" : "Uživatelsky určená událost pro ukázku byla úspěšně uložena",
+ "Failed to delete the custom example event" : "Nepodařilo se smazat uživatelsky určenou událost pro ukázku",
+ "Custom example event was deleted successfully" : "Uživatelsky určená událost pro ukázku byla úspěšně smazána",
+ "Import calendar event" : "Naimportovat událost kalendáře",
+ "Uploading a new event will overwrite the existing one." : "Nahrání nové události přepíše tu existující.",
+ "Upload event" : "Nahrát událost",
+ "Availability" : "Dostupnost",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Když sem zadáte svou pracovní dobu, ostatní lidé při rezervování schůzky uvidí, kdy jste mimo kancelář.",
+ "Absence" : "Nepřítomnost",
+ "Configure your next absence period." : "Nastavte období své nepřítomnosti.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Také nainstalujte {calendarappstoreopen}aplikaci Kalendář{linkclose}, nebo {calendardocopen}připojte svůj počítač a telefon pro synchronizaci ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Ověřte, že jste správně nastavili {emailopen}e-mailový server{linkclose}.",
"Calendar server" : "Kalendářový server",
- "Send invitations to attendees" : "Poslat pozvánky na adresy účastníků",
+ "Send invitations to attendees" : "Poslat účastníkům pozvánky",
"Automatically generate a birthday calendar" : "Automaticky vytvořit kalendář s narozeninami",
"Birthday calendars will be generated by a background job." : "Kalendáře s narozeninami budou vytvářeny úlohou na pozadí.",
"Hence they will not be available immediately after enabling but will show up after some time." : "A tedy nebudou zpřístupněny ihned po povolení, ale objeví se až se zpožděním.",
@@ -166,15 +326,12 @@ OC.L10N.register(
"Send reminder notifications to calendar sharees as well" : "Poslat připomínky také těm, kteří mají tento sdílený kalendář připojený",
"Reminders are always sent to organizers and attendees." : "Připomínky jsou vždy poslány organizátorům a účastníkům.",
"Enable notifications for events via push" : "Upozorňovat na události prostřednictvím služby push",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Také nainstalujte {calendarappstoreopen}aplikaci Kalendář{linkclose}, nebo {calendardocopen}připojte svůj počítač a telefon pro synchronizaci ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Ověřte, že jste správně nastavili {emailopen}e-mailový server{linkclose}.",
+ "Example content" : "Obsah pro ukázku",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Obsah pro ukázku slouží pro předvedení funkcí Nextcloud. Výchozí obsah je dodáván s Nextcloud a je možné ho nahradit uživatelsky určeným.",
"There was an error updating your attendance status." : "Vyskytla se chyba při aktualizaci vašeho stavu účasti.",
"Please contact the organizer directly." : "Kontaktujte organizátora přímo.",
"Are you accepting the invitation?" : "Přijímáte pozvání?",
"Tentative" : "Nezávazně",
- "Number of guests" : "Počet hostů",
- "Comment" : "Komentář",
- "Your attendance was updated successfully." : "Vaše účast byla úspěšně aktualizována.",
- "Calendar and tasks" : "Kalendář a úkoly"
+ "Your attendance was updated successfully." : "Vaše účast byla úspěšně aktualizována."
},
"nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;");
diff --git a/apps/dav/l10n/cs.json b/apps/dav/l10n/cs.json
index c3e555519be..37407cceb6d 100644
--- a/apps/dav/l10n/cs.json
+++ b/apps/dav/l10n/cs.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Kalendář",
- "Todos" : "Úkoly",
+ "Tasks" : "Úkoly",
"Personal" : "Osobní",
"{actor} created calendar {calendar}" : "{actor} vytvořil(a) kalendář {calendar}",
"You created calendar {calendar}" : "Vytvořili jste kalendář {calendar}",
@@ -23,36 +23,41 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} nasdílel(a) kalendář {calendar} skupině {group}",
"You unshared calendar {calendar} from group {group}" : "Zrušili jste sdílení kalendáře {calendar} skupině {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} přestal(a) sdílet kalendář {calendar} se skupinou {group}",
+ "Untitled event" : "Událost bez názvu",
"{actor} created event {event} in calendar {calendar}" : "{actor} vytvořil(a) událost {event} v kalendáři {calendar}",
"You created event {event} in calendar {calendar}" : "V kalendáři {calendar} jste vytvořili událost {event}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} z kalendáře {calendar} smazal(a) událost {event}",
"You deleted event {event} from calendar {calendar}" : "Smazali jste událost {event} z kalendáře {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} aktualizoval(a) událost {event} v kalendáři {calendar}",
"You updated event {event} in calendar {calendar}" : "Aktualizovali jste událost {event} v kalendáři {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} přesunul(a) událost {event} z kalendáře {sourceCalendar} do kalendáře {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Přesunuli jste událost {event} z kalendáře {sourceCalendar} do kalendáře {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} obnovil(a) událost {event} kalendáře {calendar}",
"You restored event {event} of calendar {calendar}" : "Obnovili jste událost {event} kalendáře {calendar}",
"Busy" : "Zaneprázdněn(a)",
- "{actor} created todo {todo} in list {calendar}" : "{actor} vytvořil(a) v seznamu {calendar} vytvořila úkol {todo}",
- "You created todo {todo} in list {calendar}" : "V seznamu {calendar} jste vytvořili úkol {todo}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} smazal(a) úkol {todo} ze seznamu {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Ze seznamu {calendar} jste smazali úkol {todo}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} aktualizoval(a) úkol {todo} v seznamu {calendar}",
- "You updated todo {todo} in list {calendar}" : "Aktualizovali jste úkol {todo} v seznamu {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} vyřešil(a) úkol {todo} v seznamu {calendar}",
- "You solved todo {todo} in list {calendar}" : "Vyřešili jste úkol {todo} v seznamu {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} znovu otevřel(a) úkol {todo} v seznamu {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Znovu jste otevřeli úkol {todo} v seznamu {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} vytvořil(a) úkol {todo} v seznamu {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Vytvořili jste úkol {todo} v seznamu {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} vymazal(a) úkol {todo} ze seznamu {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Vymazali jste úkol {todo} ze seznamu {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} aktualizoval(a) úkol {todo} v seznamu {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Aktualizovali jste úkol {todo} v seznamu {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} vyřešil(a) úkol {todo} v seznamu {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Vyřešili jste úkol {todo} v seznamu {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} znovu otevřel(a) úkol {todo} v seznamu {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Znovuotevřeli jste úkol {todo} v seznamu {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} přesunul(a) úkol {todo} ze seznamu {sourceCalendar} do seznamu {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Přesunuli jste úkol {todo} ze seznamu {sourceCalendar} do seznamu {targetCalendar}",
"Calendar, contacts and tasks" : "Kalendář, kontakty a úkoly",
"A <strong>calendar</strong> was modified" : "<strong>Kalendář</strong> byl změněn",
"A calendar <strong>event</strong> was modified" : "<strong>Událost</strong> v kalendáři byla změněna",
- "A calendar <strong>todo</strong> was modified" : "<strong>Úkol</strong> v kalendáři byl změněn",
+ "A calendar <strong>to-do</strong> was modified" : "Kalendář <strong>úkoly</strong> byl upraven",
"Contact birthdays" : "Narozeniny kontaktů",
"Death of %s" : "Datum úmrtí %s",
+ "Untitled calendar" : "Nepojmenovaný kalendář",
"Calendar:" : "Kalendář:",
"Date:" : "Datum:",
"Where:" : "Kde:",
"Description:" : "Popis:",
- "Untitled event" : "Událost bez názvu",
"_%n year_::_%n years_" : ["%n rok","%n roky","%n let","%n roky"],
"_%n month_::_%n months_" : ["%n měsíc","%n měsíce","%n měsíců","%n měsíce"],
"_%n day_::_%n days_" : ["%n den","%n dny","%n dnů","%n dny"],
@@ -65,22 +70,129 @@
"Description: %s" : "Popis: %s",
"Where: %s" : "Kde: %s",
"%1$s via %2$s" : "%1$s prostřednictvím %2$s",
+ "In the past on %1$s for the entire day" : "V minulosti %1$s po celý den",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Za minutu %2$s po celý den","Za %n minuty %1$s po celý den","Za %n minut %1$s po celý den","Za %n minuty %1$s po celý den"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Za hodinu %1$s po celý den","Za %n hodiny %1$s po celý den","Za %n hodin %1$s po celý den","Za %n hodiny %1$s po celý den"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Za den %1$s po celý den","Za %n dny %1$s po celý den","Za %n dnů %1$s po celý den","Za %n dny %1$s po celý den"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Za týden %1$s po celý den","Za %n týdny %1$s po celý den","Za %n týdnů %1$s po celý den","Za %n týdny %1$s po celý den"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Za měsíc %2$s po celý den","Za %n měsíce %1$s po celý den","Za %n měsíců %1$s po celý den","Za %n měsíce %1$s po celý den"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Za rok %1$s po celý den","Za %n roky %1$s po celý den","Za %n let %1$s po celý den","Za %n roky %1$s po celý den"],
+ "In the past on %1$s between %2$s - %3$s" : "V minulosti %1$s mezi %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Za minutu %1$s mezi %2$s - %3$s","Za %n minuty %1$s mezi %2$s - %3$s","Za %n minut %1$s mezi %2$s - %3$s","Za %n minuty %1$s mezi %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Za hodinu %1$s mezi %2$s - %3$s ","Za %n hodiny %1$s mezi %2$s - %3$s","Za %n hodin %1$s mezi %2$s - %3$s","Za %n hodiny %1$s mezi %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Za den %1$s mezi %2$s - %3$s","Za %n dny %1$s mezi %2$s - %3$s","Za %n dnů %1$s mezi %2$s - %3$s","Za %n dny %1$s mezi %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Za týden %1$s mezi %2$s - %3$s","Za %n týdny %1$s mezi %2$s - %3$s","Za %n týdnů %1$s mezi %2$s - %3$s","Za %n týdny %1$s mezi %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Za měsíc %1$s mezi %2$s - %3$s","Za %n měsíce %1$s mezi %2$s - %3$s","Za %n měsíců %1$s mezi %2$s - %3$s","Za %n měsíce %1$s mezi %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Za rok %1$s mezi %2$s - %3$s","Za %n roky %1$s mezi %2$s - %3$s","Za %n let %1$s mezi %2$s - %3$s","Za %n roky %1$s mezi %2$s - %3$s"],
+ "Could not generate when statement" : "Nepodařilo se vytvořit výrok kdy",
+ "Every Day for the entire day" : "Každý den pro celý den",
+ "Every Day for the entire day until %1$s" : "Každý den pro celý den až do %1$s",
+ "Every Day between %1$s - %2$s" : "Každý den mezi %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Každý den mezi %1$s - %2$s až do %3$s",
+ "Every %1$d Days for the entire day" : "Co %1$d dnů po celý den",
+ "Every %1$d Days for the entire day until %2$s" : "Co %1$d dnů po celý den až do %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Co %1$d dnů mezi %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Co %1$d dnů mezi %2$s - %3$s až do %4$s",
+ "Could not generate event recurrence statement" : "Nepodařilo se vytvořit výrok o opakování události",
+ "Every Week on %1$s for the entire day" : "Každý týden v(e) %1$s po celý den",
+ "Every Week on %1$s for the entire day until %2$s" : "Každý týden v(e) %1$s po celý den až do %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Každý týden v %1$s mezi %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Každý týden v(e) %1$s mezi %2$s - %3$s až do %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Každých %1$d týdnů v(e) %2$s po celý den",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Co %1$d týdnů v(e) %2$s po celý den až do %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Co %1$d týdnů v(e) %2$s mezi %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Co %1$d týdnů v(e) %2$s mezi %3$s - %4$s až do %5$s",
+ "Every Month on the %1$s for the entire day" : "Každý měsíc v %1$s po celý den",
+ "Every Month on the %1$s for the entire day until %2$s" : "Každý měsíc v %1$s po celý den až do %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Každý měsíc v %1$s mezi %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Každý měsíc v %1$s mezi %2$s - %3$s až do %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Co %1$d měsíců v %2$s po celý den",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Co %1$d měsíců v %2$s po celý den až do %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Co %1$d měsíců v %2$s mezi %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Co %1$d měsíců v %2$s mezi %3$s - %4$s až do %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Každoročně %1$s v %2$s po celý den",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Každoročně v %1$s v %2$s po celý den až do %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Každoročně v %1$s v %2$s mezi %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Každoročně v %1$s v %2$s mezi %3$s - %4$s až do %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Co %1$d let v %2$s v %3$s po celý den",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Co %1$d let v %2$s v %3$s po celý den až do %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Co %1$d let v %2$s v %3$s mezi %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Co %1$d let %2$s v %3$s mezi %4$s - %5$s až do %6$s",
+ "On specific dates for the entire day until %1$s" : "V konkrétních datech pro celý den až do %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "V konkrétních datech mezi %1$s - %2$s až do %3$s",
+ "In the past on %1$s" : "V minulosti %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Za minutu %1$s","Za %n minuty %1$s","Za %n minut %1$s","Za %n minuty %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Za hodinu %1$s","Za %n hodiny %1$s","Za %n hodin %1$s","Za %n hodiny %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Za den %1$s","Za %n dny %1$s","Za %n dnů %1$s","Za %n dny %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Za týden %1$s","Za %n týdny %1$s","Za %n týdnů %1$s","Za %n týdny %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Za měsíce %1$s","Za %n měsíce %1$s","Za %n měsíců %1$s","Za %n měsíce %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Za rok %1$s","Za %n roky %1$s","Za %n let %1$s","Za %n roky %1$s"],
+ "In the past on %1$s then on %2$s" : "V minulosti %1$s, pak %2$s ",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Za minutu %1$s, poté %2$s","Za %n minuty %1$s, poté %2$s","Za %n minut %1$s, poté %2$s ","Za %n minuty %1$s, poté %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Za hodinu %1$s, poté %2$s","Za %n hodiny %1$s, poté %2$s","Za %n hodin %1$s, poté %2$s","Za %n hodiny %1$s, poté %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Za den %1$s, poté %2$s","Za %n dny %1$s, poté %2$s","Za %n dnů %1$s, poté %2$s","Za %n dny %1$s, poté %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Za týden %1$s, poté %2$s","Za %n týdny %1$s, poté %2$s","Za %n týdnů %1$s, poté %2$s","Za %n týdny %1$s, poté %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Za měsíc %1$s, poté %2$s","Za %n měsíce %1$s, poté %2$s","Za %n měsíců %1$s, poté %2$s","Za %n měsíce %1$s, poté %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Za rok %1$s, poté %2$s","Za %n roky %1$s, poté %2$s","Za %n let %1$s, poté %2$s","Za %n roky %1$s, poté %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "V minulosti %1$s, poté %2$s a %3$s ",
+ "_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_" : ["Za minutu %1$s, poté %2$s a %3$s ","Za %n minuty %1$s, poté %2$s a %3$s ","Za %n minut %1$s, poté %2$s a %3$s ","Za %n minuty %1$s, poté %2$s a %3$s "],
+ "_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_" : ["Za hodinu %1$s, poté %2$s a %3$s","Za %n hodiny %1$s, poté %2$s a %3$s","Za %n hodin %1$s, poté %2$s a %3$s","Za %n hodiny %1$s, poté %2$s a %3$s"],
+ "_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_" : ["Za den %1$s, poté %2$s a %3$s","Za %n dny %1$s, poté %2$s a %3$s","Za %n dnů %1$s, poté %2$s a %3$s","Za %n dny %1$s, poté %2$s a %3$s"],
+ "_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_" : ["Za týden %1$s, poté %2$s a %3$s","Za %n týdny %1$s, poté %2$s a %3$s","Za %n týdnů %1$s, poté %2$s a %3$s","Za %n týdny %1$s, poté %2$s a %3$s"],
+ "_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_" : ["Za měsíc %1$s, poté %2$s a %3$s","Za %n měsíce %1$s, poté %2$s a %3$s","Za %n měsíců %1$s, poté %2$s a %3$s","Za %n měsíce %1$s, poté %2$s a %3$s"],
+ "_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_" : ["Za rok %1$s, poté %2$s a %3$s","Za %n roky %1$s, poté %2$s a %3$s","Za %n let %1$s, poté %2$s a %3$s","Za %n roky %1$s, poté %2$s a %3$s"],
+ "Could not generate next recurrence statement" : "Nepodařilo se vytvořit výrok o příštím opakování",
"Cancelled: %1$s" : "Zrušeno: %1$s",
- "Invitation canceled" : "Pozvánka zrušena",
+ "\"%1$s\" has been canceled" : "„%1$s“ bylo zrušeno",
"Re: %1$s" : "Odp.: %1$s",
- "Invitation updated" : "Pozvánka aktualizována",
+ "%1$s has accepted your invitation" : "%1$s přijal(a) vaše pozvání",
+ "%1$s has tentatively accepted your invitation" : "%1$s nezávazně přijal(a) vaši pozvánku",
+ "%1$s has declined your invitation" : "%1$s odmítla(a) vaši pozvánku",
+ "%1$s has responded to your invitation" : "%1$s odpověděl(a) na vaši pozvánku",
+ "Invitation updated: %1$s" : "Pozvánka aktualizována: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s zaktualizoval(a) událost „%2$s",
"Invitation: %1$s" : "Pozvánka: %1$s",
- "Invitation" : "Pozvání",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s by vás ráda pozval(a) na „%2$s“",
+ "Organizer:" : "Organizátor:",
+ "Attendees:" : "Účastníci:",
"Title:" : "Název:",
- "Time:" : "Čas:",
+ "When:" : "Kdy:",
"Location:" : "Umístění:",
"Link:" : "Odkaz:",
- "Organizer:" : "Organizátor:",
- "Attendees:" : "Účastníci:",
+ "Occurring:" : "Opakuje se::",
"Accept" : "Přijmout",
- "Decline" : "Zamítnout",
+ "Decline" : "Odmítnout",
"More options …" : "Další volby…",
"More options at %s" : "Další volby viz %s",
+ "Monday" : "pondělí",
+ "Tuesday" : "úterý",
+ "Wednesday" : "středa",
+ "Thursday" : "čtvrtek",
+ "Friday" : "pátek",
+ "Saturday" : "sobota",
+ "Sunday" : "neděle",
+ "January" : "leden",
+ "February" : "únor",
+ "March" : "březen",
+ "April" : "duben",
+ "May" : "květen",
+ "June" : "červen",
+ "July" : "červenec",
+ "August" : "srpen",
+ "September" : "září",
+ "October" : "říjen",
+ "November" : "listopad",
+ "December" : "prosinec",
+ "First" : "První",
+ "Second" : "Sekund",
+ "Third" : "Třetí",
+ "Fourth" : "Čtvrtý",
+ "Fifth" : "Páté",
+ "Last" : "Poslední",
+ "Second Last" : "Druhý poslední",
+ "Third Last" : "Třetí poslední",
+ "Fourth Last" : "Čtvrtý poslední",
+ "Fifth Last" : "Pátý poslední",
"Contacts" : "Kontakty",
"{actor} created address book {addressbook}" : "{actor} vytvořil(a) adresář kontaktů {addressbook}",
"You created address book {addressbook}" : "Vytvořili jste adresář kontaktů {addressbook}",
@@ -106,7 +218,10 @@
"{actor} updated contact {card} in address book {addressbook}" : "{actor} upravil(a) kontakt {card} v adresáři kontaktů {addressbook}",
"You updated contact {card} in address book {addressbook}" : "Upravili jste kontakt {card} v adresáři kontaktů {addressbook}",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>Kontakt</strong> nebo <strong>adresář kontaktů</strong> byl upraven",
+ "Accounts" : "Účty",
+ "System address book which holds all accounts" : "Systémový adresář kontaktů, který obsahuje veškeré účty",
"File is not updatable: %1$s" : "Soubor není možné zaktualizovat: %1$s",
+ "Failed to get storage for file" : "Nepodařilo se získat úložiště pro soubor",
"Could not write to final file, canceled by hook" : "Nedaří se zapsat do konečného souboru – zrušeno háčkem (hook)",
"Could not write file contents" : "Nedaří se zapsat obsahy souborů",
"_%n byte_::_%n bytes_" : ["%n bajt","%n bajty","%n bajtů","%n bajty"],
@@ -115,47 +230,92 @@
"Could not rename part file to final file, canceled by hook" : "Nedaří se zapsat přejmenovat částečný soubor na ten končený – zrušeno háčkem (hook)",
"Could not rename part file to final file" : "Nedaří se přejmenovat částečný soubor na ten konečný",
"Failed to check file size: %1$s" : "Nepodařilo se zkontrolovat velikost souboru: %1$s",
- "Could not open file" : "Nedaří se otevřít soubor",
+ "Could not open file: %1$s, file does seem to exist" : "Nebylo možné otevřít soubor: %1$s – soubor zdá se existuje",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Nebylo možné otevřít soubor: %1$s – soubor zdá se neexistuje",
"Encryption not ready: %1$s" : "Šifrování není připraveno: %1$s",
"Failed to open file: %1$s" : "Nepodařilo se otevřít soubor: %1$s",
"Failed to unlink: %1$s" : "Nepodařilo se zrušit propojení: %1$s",
- "Invalid chunk name" : "Neplatný název pro shluk (chunk)",
- "Could not rename part file assembled from chunks" : "Nedaří se přejmenovat částečný soubor složený ze shluků",
"Failed to write file contents: %1$s" : "Nepodařilo se zapsat obsahy souborů: %1$s",
"File not found: %1$s" : "Soubor nenalezen: %1$s",
- "System is in maintenance mode." : "Na systému právě probíhá údržba.",
+ "Invalid target path" : "Neplatný popis umístění cíle",
+ "System is in maintenance mode." : "Systém se právě nachází v režimu údržby.",
"Upgrade needed" : "Je třeba přejít na novější verzi",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Váš %s potřebuje být nastavený aby používal HTTPS, aby bylo možné používat CalDAV a CardDAV s iOS/macOS.",
"Configures a CalDAV account" : "Nastaví CalDAV účet",
"Configures a CardDAV account" : "Nastaví CardDAV účet",
"Events" : "Události",
- "Tasks" : "Úkoly",
"Untitled task" : "Nepojmenovaný úkol",
"Completed on %s" : "Dokončeno %s",
"Due on %s by %s" : "Termín do %s od %s",
"Due on %s" : "Termín do %s",
+ "Example event - open me!" : "Událost pro ukázku – otevřete ji!",
+ "System Address Book" : "Systémový adresář kontaktů",
+ "The system address book contains contact information for all users in your instance." : "Systémový adresář kontaktů obsahuje informace pro všechny uživatele ve vámi využívané instanci.",
+ "Enable System Address Book" : "Zapnout systémový adresář kontaktů",
+ "DAV system address book" : "Systémový DAV adresář kontaktů",
+ "No outstanding DAV system address book sync." : "Žádná zbývající synchronizace systémového DAV adresáře kontaktů.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV synchronizace systémového adresáře kontaktů ještě nebyla spuštěná protože vámi využívaná instance má více než 1 000 uživatelů nebo protože došlo k chybě. Spusťte ji ručně příkazem „occ dav:sync-system-addressbook“.",
+ "WebDAV endpoint" : "WebDAV endpoint",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Nepodařilo se zkontrolovat že vámi využívaný webový server je správně nastaven tak, aby umožňoval synchronizaci přes WebDAV. Zkontrolujte to ručně.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Váš webový server ještě není správně nastaven, pro umožnění synchronizace souborů, rozhraní WebDAV pravděpodobně není funkční.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Vámi využívaný webový server je správně nastaven pro umotnění synchronizace souborů přes WebDAV",
"Migrated calendar (%1$s)" : "Přesunut kalendář (%1$s)",
"Calendars including events, details and attendees" : "Kalendáře včetně událostí, podrobností a účastníků",
"Contacts and groups" : "Kontakty a skupiny",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV endpoint",
- "Availability" : "Dostupnost",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Když sem zadáte svou pracovní dobu, ostatní uživatelé při rezervování schůzky uvidí, kdy jste mimo kancelář.",
+ "Absence saved" : "Nepřítomnost uložena",
+ "Failed to save your absence settings" : "Nepodařilo se uložit nastavení vaší nepřítomnosti",
+ "Absence cleared" : "Nepřítomnost vyčištěna",
+ "Failed to clear your absence settings" : "Nepodařilo se vymazat nastavení vaší nepřítomnosti",
+ "First day" : "První den",
+ "Last day (inclusive)" : "Poslední den (včetně)",
+ "Out of office replacement (optional)" : "Zástup když mimo kancelář (volitelné)",
+ "Name of the replacement" : "Jméno zástupu",
+ "No results." : "Nic nenalezeno.",
+ "Start typing." : "Začněte psát.",
+ "Short absence status" : "Stav krátké nepřítomnosti",
+ "Long absence Message" : "Zpráva pro dlouhou nepřítomnost",
+ "Save" : "Uložit",
+ "Disable absence" : "Vypnout nepřítomnost",
+ "Failed to load availability" : "Nepodařilo se načíst dostupnost",
+ "Saved availability" : "Uložena dostupnost",
+ "Failed to save availability" : "Nepodařilo se uložit dostupnost",
"Time zone:" : "Časové pásmo:",
"to" : "do",
"Delete slot" : "Smazat slot",
"No working hours set" : "Nenastaveny pracovní hodiny",
"Add slot" : "Přidat slot",
- "Monday" : "pondělí",
- "Tuesday" : "úterý",
- "Wednesday" : "středa",
- "Thursday" : "čtvrtek",
- "Friday" : "pátek",
- "Saturday" : "sobota",
- "Sunday" : "neděle",
- "Save" : "Uložit",
+ "Weekdays" : "Dny v týdnu",
+ "Pick a start time for {dayName}" : "Vyberte začátek pro {dayName}",
+ "Pick a end time for {dayName}" : "Vyberte konec pro {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "V době, kdy není k dispozici, automaticky nastavit stav uživatele na „Nerušit“ a ztlumit tak veškerá upozornění.",
+ "Cancel" : "Storno",
+ "Import" : "Naimportovat",
+ "Error while saving settings" : "Chyba při ukládání nastavení",
+ "Contact reset successfully" : "Kontakt úspěšně resetován",
+ "Error while resetting contact" : "Chyba při resetování kontaktu",
+ "Contact imported successfully" : "Kontakt úspěšně naimportován",
+ "Error while importing contact" : "Chyba při importování kontaktu",
+ "Import contact" : "Importovat kontakt",
+ "Reset to default" : "Vrátit zpět na výchozí hodnoty",
+ "Import contacts" : "Importovat kontakty",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Naimportování nového .vcf souboru smaže stávající výchozí kontakt a nahradí ho novým. Chcete pokračovat?",
+ "Failed to save example event creation setting" : "Nepodařilo se uložit nastavení vytváření události pro ukázku",
+ "Failed to upload the example event" : "Nepodařilo se nahrát událost pro ukázku",
+ "Custom example event was saved successfully" : "Uživatelsky určená událost pro ukázku byla úspěšně uložena",
+ "Failed to delete the custom example event" : "Nepodařilo se smazat uživatelsky určenou událost pro ukázku",
+ "Custom example event was deleted successfully" : "Uživatelsky určená událost pro ukázku byla úspěšně smazána",
+ "Import calendar event" : "Naimportovat událost kalendáře",
+ "Uploading a new event will overwrite the existing one." : "Nahrání nové události přepíše tu existující.",
+ "Upload event" : "Nahrát událost",
+ "Availability" : "Dostupnost",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Když sem zadáte svou pracovní dobu, ostatní lidé při rezervování schůzky uvidí, kdy jste mimo kancelář.",
+ "Absence" : "Nepřítomnost",
+ "Configure your next absence period." : "Nastavte období své nepřítomnosti.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Také nainstalujte {calendarappstoreopen}aplikaci Kalendář{linkclose}, nebo {calendardocopen}připojte svůj počítač a telefon pro synchronizaci ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Ověřte, že jste správně nastavili {emailopen}e-mailový server{linkclose}.",
"Calendar server" : "Kalendářový server",
- "Send invitations to attendees" : "Poslat pozvánky na adresy účastníků",
+ "Send invitations to attendees" : "Poslat účastníkům pozvánky",
"Automatically generate a birthday calendar" : "Automaticky vytvořit kalendář s narozeninami",
"Birthday calendars will be generated by a background job." : "Kalendáře s narozeninami budou vytvářeny úlohou na pozadí.",
"Hence they will not be available immediately after enabling but will show up after some time." : "A tedy nebudou zpřístupněny ihned po povolení, ale objeví se až se zpožděním.",
@@ -164,15 +324,12 @@
"Send reminder notifications to calendar sharees as well" : "Poslat připomínky také těm, kteří mají tento sdílený kalendář připojený",
"Reminders are always sent to organizers and attendees." : "Připomínky jsou vždy poslány organizátorům a účastníkům.",
"Enable notifications for events via push" : "Upozorňovat na události prostřednictvím služby push",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Také nainstalujte {calendarappstoreopen}aplikaci Kalendář{linkclose}, nebo {calendardocopen}připojte svůj počítač a telefon pro synchronizaci ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Ověřte, že jste správně nastavili {emailopen}e-mailový server{linkclose}.",
+ "Example content" : "Obsah pro ukázku",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Obsah pro ukázku slouží pro předvedení funkcí Nextcloud. Výchozí obsah je dodáván s Nextcloud a je možné ho nahradit uživatelsky určeným.",
"There was an error updating your attendance status." : "Vyskytla se chyba při aktualizaci vašeho stavu účasti.",
"Please contact the organizer directly." : "Kontaktujte organizátora přímo.",
"Are you accepting the invitation?" : "Přijímáte pozvání?",
"Tentative" : "Nezávazně",
- "Number of guests" : "Počet hostů",
- "Comment" : "Komentář",
- "Your attendance was updated successfully." : "Vaše účast byla úspěšně aktualizována.",
- "Calendar and tasks" : "Kalendář a úkoly"
+ "Your attendance was updated successfully." : "Vaše účast byla úspěšně aktualizována."
},"pluralForm" :"nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;"
} \ No newline at end of file
diff --git a/apps/dav/l10n/da.js b/apps/dav/l10n/da.js
index 2fecf5fe6d8..1368b899919 100644
--- a/apps/dav/l10n/da.js
+++ b/apps/dav/l10n/da.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Kalender",
- "Todos" : "Opgaver",
+ "Tasks" : "Opgaver",
"Personal" : "Personligt",
"{actor} created calendar {calendar}" : "{actor} oprettede kalenderen {calendar}",
"You created calendar {calendar}" : "Du oprettede kalenderen {calendar}",
@@ -25,36 +25,41 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} delte kalenderen {calendar} med gruppen {group}",
"You unshared calendar {calendar} from group {group}" : "Du fjernede delingen af {calendar} fra gruppen {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} fjernede deling af kalenderen {calendar} fra gruppen {group}",
+ "Untitled event" : "Unavngiven begivenhed",
"{actor} created event {event} in calendar {calendar}" : "{actor} oprettede begivenheden {event} i kalenderen {calendar}",
"You created event {event} in calendar {calendar}" : "Du oprettede begivenheden {event} i kalenderen {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} slettede begivenheden {event} fra kalenderen {calendar}",
"You deleted event {event} from calendar {calendar}" : "Du slettede begivenheden {event} fra kalenderen {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} opdaterede begivenheden {event} i kalenderen {calendar}",
"You updated event {event} in calendar {calendar}" : "Du opdaterede begivenheden {event} i kalenderen {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} flyttede begivenhed {event} fra kalender {sourceCalendar} til kalender {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Du flyttede begivenhed {event} fra kalender {sourceCalendar} til kalender {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} gendannede begivenhed {event} i kalender {calendar}",
- "You restored event {event} of calendar {calendar}" : "Du gendannede begivenhed {begivenhed} i kalender {kalender}",
+ "You restored event {event} of calendar {calendar}" : "Du gendannede begivenhed {event} i kalender {calendar}",
"Busy" : "Optaget",
- "{actor} created todo {todo} in list {calendar}" : "{actor} oprettede en opgave {todo} i listen {calendar}",
- "You created todo {todo} in list {calendar}" : "Du oprettede opgaven {todo} i listen {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} slettede opgaven {todo} fra listen {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Du slettede opgaven {todo} fra listen {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} opdaterede opgaven {todo} i listen {calendar}",
- "You updated todo {todo} in list {calendar}" : "Du opdaterede opgaven {todo} i listen {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} løste opgaven {todo} i listen {calendar}",
- "You solved todo {todo} in list {calendar}" : "Du løste opgaven {todo} i listen {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} genåbnede opgaven {todo} i listen {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Du genåbnede opgaven {todo} i listen {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} oprettede en opgave {todo} i listen {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Du oprettede opgaven {todo} i listen {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} slettede opgaven {todo} fra listen {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Du slettede opgaven {todo} fra listen {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} opdaterede opgaven {todo} i listen {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Du opdaterede opgaven {todo} i listen {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} løste opgaven {todo} i listen {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Du løste opgaven {todo} i listen {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} genåbnede opgaven {todo} i listen {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Du genåbnede opgaven {todo} i listen {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} flyttede opgave {todo} fra liste {sourceCalendar} til liste {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Du flyttede opgave {todo} fra liste {sourceCalendar} til liste {targetCalendar}",
"Calendar, contacts and tasks" : "Kalender, kontakter og opgaver",
"A <strong>calendar</strong> was modified" : "En <strong>kalender</strong> er blevet ændret",
"A calendar <strong>event</strong> was modified" : "En kalender <strong>begivenhed</strong> er blevet ændret",
- "A calendar <strong>todo</strong> was modified" : "En kalender <strong>opgave</strong> blev ændret",
+ "A calendar <strong>to-do</strong> was modified" : "En kalender <strong>opgave</strong> blev ændret",
"Contact birthdays" : "Kontakt fødselsdag",
"Death of %s" : "Død af%s",
+ "Untitled calendar" : "Unanvngiven kalender",
"Calendar:" : "Kalender:",
"Date:" : "Dato:",
"Where:" : "Hvor:",
"Description:" : "Beskrivelse:",
- "Untitled event" : "Unavngiven begivenhed",
"_%n year_::_%n years_" : ["%n år","%n år"],
"_%n month_::_%n months_" : ["%n måned","%n måneder"],
"_%n day_::_%n days_" : ["%n dag","%n dage"],
@@ -67,26 +72,100 @@ OC.L10N.register(
"Description: %s" : "Beskrivelse: %s",
"Where: %s" : "Hvor: %s",
"%1$s via %2$s" : "%1$s via %2$s",
- "Invitation canceled" : "Invitation annulleret",
- "Invitation updated" : "Invitation opdateret ",
- "Invitation" : "Invitation",
+ "In the past on %1$s for the entire day" : "Tidligere den %1$s for hele dagen",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Om et minut på %1$s for hele dagen","Om %n minutter den %1$s for hele dagen"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Om en time på %1$s for hele dagen","Om %n timer den %1$s for hele dagen"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["I en dag på %1$s for hele dagen","Om %n dage den %1$s for hele dagen"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["I en uge på %1$s for hele dagen","Om %n uger den %1$s for hele dagen"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["I en måned på %1$s for hele dagen","Om %n måneder den %1$s for hele dagen"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["I et år på %1$s for hele dagen","Om %n år den %1$s for hele dagen"],
+ "In the past on %1$s between %2$s - %3$s" : "Tidligere den %1$s mellem %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["I et minut på %1$s mellem %2$s - %3$s","Om %n minutter den %1$s mellem %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["I en time på %1$s mellem %2$s - %3$s","Om %n timer den %1$s mellem %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["I en dag på %1$s mellem %2$s - %3$s","Om %n dage den %1$s mellem %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["I en uge på %1$s mellem %2$s - %3$s","Om %n uger den %1$s mellem %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["I en måned på %1$s mellem %2$s - %3$s","Om %n måneder den %1$s mellem %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["I et år på %1$s mellem %2$s - %3$s","Om %n år den %1$s mellem %2$s - %3$s"],
+ "Could not generate when statement" : "Kunne ikke generere when sætning",
+ "Every Day for the entire day" : "Hver dag hele dagen",
+ "Every Day for the entire day until %1$s" : "Hver dag hele dagen indtil %1$s",
+ "Every Day between %1$s - %2$s" : "Hver dag mellem %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Hver dag mellem %1$s - %2$s indtil %3$s",
+ "Every %1$d Days for the entire day" : "Hver %1$d dage for hele dagen",
+ "Every %1$d Days for the entire day until %2$s" : "Hver %1$d dage for hele dagen indtil %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Hver %1$d dage mellem %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Hver %1$d dage mellem %2$s - %3$s indtil %4$s",
+ "Could not generate event recurrence statement" : "Kunne ikke generere erklæring for begivenhedsgentagelse",
+ "Every Week on %1$s for the entire day" : "Hver uge den %1$s for hele dagen",
+ "Every Week on %1$s for the entire day until %2$s" : "Hver uge den %1$s for hele dagen indtil %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Hver uge den %1$s mellem %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Hver uge den %1$s mellem %2$s - %3$s indtil %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Hver %1$d uger den %2$s for hele dagen",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Hver %1$d uger den %2$s for hele dagen indtil %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Hver %1$d uger den %2$s mellem %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Hver %1$d uger den %2$s mellem %3$s - %4$s indtil %5$s",
+ "Every Month on the %1$s for the entire day" : "Hver måned den %1$s for hele dagen",
+ "Every Month on the %1$s for the entire day until %2$s" : "Hver måned den %1$s for hele dagen indtil %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Hver måned den %1$s mellem %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Hver måned den %1$s mellem %2$s - %3$s indtil %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Hver %1$d måneder den %2$s for hele dagen",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Hver %1$d måneder den %2$s for hele dagen indtil %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Hver %1$d måneder den %2$s mellem %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Hver %1$d måneder den %2$s mellem %3$s - %4$s indtil %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Hvert år i %1$s den %2$s for hele dagen",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Hvert år i %1$s den %2$s for hele dagen indtil %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Hvert år i %1$s den %2$s mellem %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Hvert år i %1$s den %2$s mellem %3$s - %4$s indtil %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Hver %1$d år i %2$s den %3$s for hele dagen",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Hvert %1$d år i %2$s den %3$s for hele dagen indtil %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Hvert %1$d år i %2$s den %3$s mellem %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Hvert %1$d år i %2$s den %3$s mellem %4$s - %5$s indtil %6$s",
+ "On specific dates for the entire day until %1$s" : "På specifikke datoer for hele dagen indtil %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "På specifikke datoer mellem %1$s - %2$s indtil %3$s",
+ "In the past on %1$s" : "Tidligere den %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["I et minut på %1$s","Om %n minutter den %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["I en time på %1$s","Om %n timer den %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["I en dag på %1$s","Om %n dage den %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["I en uge på %1$s","Om %n uger den %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["I en måned på %1$s","Om %n måneder den %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["I et år på %1$s","Om %n år den %1$s"],
+ "In the past on %1$s then on %2$s" : "Tidligere på %1$s derefter den %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["I et minut på %1$s så på %2$s","Om %n minutter den %1$s derefter den %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["I en time på %1$s så på %2$s","Om %n timer den %1$s derefter den %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["I en dag på %1$s så på %2$s","Om %n dage den %1$s derefter den %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["I en uge på %1$s så på %2$s","Om %n uger den %1$s derefter den %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["I en måned på %1$s så på %2$s","Om %n måneder den %1$s derefter den %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["I et år på %1$s så på %2$s","Om %n år den %1$s derefter den %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "Tidligere den %1$s derefter den %2$s og %3$s",
+ "_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_" : ["I et minut på %1$s så på %2$s og %3$s","Om %n minutter den %1$s derefter den %2$s og %3$s"],
+ "_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_" : ["I en time på %1$s så på %2$s og %3$s","Om %n timer den %1$s derefter den %2$s og %3$s"],
+ "_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_" : ["I en dag på %1$s så på %2$s og %3$s","Om %n dage den %1$s derefter den %2$s og %3$s"],
+ "_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_" : ["I en uge på %1$s så på %2$s og %3$s","Om %n uger den %1$s derefter den %2$s og %3$s"],
+ "_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_" : ["I en måned på %1$s så på %2$s og %3$s","Om %n måneder den %1$s derefter den %2$s og %3$s"],
+ "_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_" : ["I et år på %1$s så på %2$s og %3$s","Om %n år den %1$s derefter den %2$s og %3$s"],
+ "Could not generate next recurrence statement" : "Kunne ikke generere næste gentagelseserklæring",
+ "Cancelled: %1$s" : "Annullerede: %1$s",
+ "\"%1$s\" has been canceled" : "\"%1$s\" er blevet annulleret",
+ "Re: %1$s" : "Re: %1$s",
+ "%1$s has accepted your invitation" : "%1$s har accepteret din invitation",
+ "%1$s has tentatively accepted your invitation" : "%1$s har foreløbigt accepteret din invitation",
+ "%1$s has declined your invitation" : "%1$s har afvist din invitation",
+ "%1$s has responded to your invitation" : "%1$s har svaret på din invitation",
+ "Invitation updated: %1$s" : "Invitation opdateret: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s opdaterede begivenheden \"%2$s\"",
+ "Invitation: %1$s" : "Invitation: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s vil gerne invitere dig til \"%2$s\"",
+ "Organizer:" : "Arrangør:",
+ "Attendees:" : "Deltagere:",
"Title:" : "Titel:",
- "Time:" : "Tid:",
+ "When:" : "Hvornår:",
"Location:" : "Sted:",
"Link:" : "Link:",
- "Organizer:" : "Arrangør:",
- "Attendees:" : "Deltagere:",
+ "Occurring:" : "Forekomst:",
"Accept" : "Accepter",
"Decline" : "Afvis",
"More options …" : "Flere indstillinger…",
- "Contacts" : "Kontakter",
- "System is in maintenance mode." : "Systemet er i vedligeholdelsestilstand.",
- "Upgrade needed" : "Opgradering er nødvendig",
- "Tasks" : "Opgaver",
- "Contacts and groups" : "Kontakter og grupper",
- "WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV endpoint",
- "to" : "til",
+ "More options at %s" : "Flere muligheder på %s",
"Monday" : "Mandag",
"Tuesday" : "Tirsdag",
"Wednesday" : "Onsdag",
@@ -94,14 +173,154 @@ OC.L10N.register(
"Friday" : "Fredag",
"Saturday" : "Lørdag",
"Sunday" : "Søndag",
+ "January" : "Januar",
+ "February" : "Februar",
+ "March" : "Marts",
+ "April" : "April",
+ "May" : "Maj",
+ "June" : "Juni",
+ "July" : "Juli",
+ "August" : "August",
+ "September" : "September",
+ "October" : "Oktober",
+ "November" : "November",
+ "December" : "December",
+ "First" : "Første",
+ "Second" : "Andet",
+ "Third" : "Tredje",
+ "Fourth" : "Fjerde",
+ "Fifth" : "Femte",
+ "Last" : "Sidste",
+ "Second Last" : "Anden sidste",
+ "Third Last" : "Tredje sidste",
+ "Fourth Last" : "Fjerde sidste",
+ "Fifth Last" : "Femte sidste",
+ "Contacts" : "Kontakter",
+ "{actor} created address book {addressbook}" : "{actor} oprettede adressebog {addressbook}",
+ "You created address book {addressbook}" : "Du oprettede adressebog {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} slattede adressebog {addressbook}",
+ "You deleted address book {addressbook}" : "Du slettede adressebog {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} opdaterede adressebog {addressbook}",
+ "You updated address book {addressbook}" : "Du opdaterede adressebog {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} delte adressebog {addressbook} med dig",
+ "You shared address book {addressbook} with {user}" : "Du delte adressebog {addressbook} med {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} delte adressebog {addressbook} med {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} fjernede delingen af adressebog {addressbook} med dig",
+ "You unshared address book {addressbook} from {user}" : "Du fjernede delingen af adressebog {addressbook} med {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} fjernede delingen af adressebog {addressbook} med {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} fjernede delingen af adressebog {addressbook} med sig selv",
+ "You shared address book {addressbook} with group {group}" : "Du delte adressebog {addressbook} med gruppen {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} delte adressebog {addressbook} med gruppen {group}",
+ "You unshared address book {addressbook} from group {group}" : "Du fjernede delingen af adressebog {addressbook} fra gruppen {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} fjernede delingen af adressebog {addressbook} med gruppen {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} oprettede kontakten {card} i adressebog {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Du oprettede kontakten {card} i adressebog {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} slettede kontakten {card} i adressebog {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Du slettede kontakten {card} i adressebog {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} opdaterede kontakten {card} i adressebog {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Du opdaterede kontakten {card} i adressebog {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "En <strong>kontakt</strong> eller <strong>adressebog</strong> blev ændret",
+ "Accounts" : "Konti",
+ "System address book which holds all accounts" : "Systemets adressebog, som indeholder alle konti",
+ "File is not updatable: %1$s" : "Filen kan ikke updateres: %1$s",
+ "Failed to get storage for file" : "Kunne ikke hente lagerplads til filen",
+ "Could not write to final file, canceled by hook" : "Kunne ikke skrive til den endelige fil, annulleret af hook",
+ "Could not write file contents" : "Kunne ikke skrive filindhold",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Fejl under kopiering af fil til placering (kopieret: %1$s, forventet filstørrelse: %2$s)",
+ "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." : "Forventede filstørrelse på %1$s, men læste (fra Nextcloud-klienten) og skrev (til Nextcloud-lageret) %2$s. Det kan enten være et netværksproblem på afsendersiden eller et problem med at skrive til lageret på serversiden.",
+ "Could not rename part file to final file, canceled by hook" : "Kunne ikke omdøbe delfilen til den endelige fil, annulleret af hook",
+ "Could not rename part file to final file" : "Delfilen kunne ikke omdøbes til den endelige fil",
+ "Failed to check file size: %1$s" : "Kunne ikke kontrollere filstørrelsen: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "Kunne ikke åbne filen: %1$s, filen ser ud til at eksistere",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Kunne ikke åbne filen: %1$s, filen ser ikke ud til at eksistere",
+ "Encryption not ready: %1$s" : "Kryptering ikke klar: %1$s",
+ "Failed to open file: %1$s" : "Kunne ikke åbne fil: %1$s",
+ "Failed to unlink: %1$s" : "Tilknytningen kunne ikke fjernes: %1$s",
+ "Failed to write file contents: %1$s" : "Kunne ikke skrive filindhold: %1$s",
+ "File not found: %1$s" : "Fil ikke fundet: %1$s",
+ "Invalid target path" : "Ugyldig målsti",
+ "System is in maintenance mode." : "Systemet er i vedligeholdelsestilstand.",
+ "Upgrade needed" : "Opgradering er nødvendig",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Din %s skal konfigureres til at bruge HTTPS for at kunne bruge CalDAV og CardDAV med iOS/macOS.",
+ "Configures a CalDAV account" : "Konfigurerer en CalDAV-konto",
+ "Configures a CardDAV account" : "Konfigurerer en CardDAV-konto",
+ "Events" : "Begivenheder",
+ "Untitled task" : "Unavngivet opgave",
+ "Completed on %s" : "Fuldført den %s",
+ "Due on %s by %s" : "Forfalder på %s til %s",
+ "Due on %s" : "Forfalder på %s",
+ "System Address Book" : "System adressebog",
+ "The system address book contains contact information for all users in your instance." : "System adressebogen indeholder kontaktoplysninger for alle brugere i din instans.",
+ "Enable System Address Book" : "Aktivér System adressebog",
+ "DAV system address book" : "DAV system adressebog",
+ "No outstanding DAV system address book sync." : "Ingen udestående synkronisering af DAV-systemets adressebog.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV-systemets adressebogssynkronisering er ikke kørt endnu, da din instans har mere end 1000 brugere, eller fordi der opstod en fejl. Kør det manuelt ved at kalde \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "WebDAV endpoint",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Kunne ikke kontrollere, at din webserver er korrekt konfigureret til at tillade filsynkronisering over WebDAV. Tjek venligst manuelt.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Din webserver er endnu ikke sat korrekt op til at tillade filsynkronisering, fordi WebDAV-grænsefladen ser ud til at være i stykker.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Din webserver er korrekt konfigureret til at tillade filsynkronisering over WebDAV.",
+ "Migrated calendar (%1$s)" : "Migreret kalender (%1$s)",
+ "Calendars including events, details and attendees" : "Kalendere indeholdende begivenheder, detaljer og deltagere",
+ "Contacts and groups" : "Kontakter og grupper",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "Fraværet er gemt",
+ "Failed to save your absence settings" : "Kunne ikke gemme dine fraværsindstillinger",
+ "Absence cleared" : "Fravær fjernet",
+ "Failed to clear your absence settings" : "Kunne ikke rydde dine fraværsindstillinger",
+ "First day" : "Første dag",
+ "Last day (inclusive)" : "Sidste dag (indklusiv)",
+ "Out of office replacement (optional)" : "Ikke på kontoret udskiftning (valgfrit)",
+ "Name of the replacement" : "Navn på udskiftning",
+ "No results." : "Ingen resultater.",
+ "Start typing." : "Begynd at skrive.",
+ "Short absence status" : "Kort fraværsstatus",
+ "Long absence Message" : "Langt fravær besked",
"Save" : "Gem",
+ "Disable absence" : "Deaktiver fravær",
+ "Failed to load availability" : "Kunne ikke indlæse tilgængelighed",
+ "Saved availability" : "Gemt tilgængelighed",
+ "Failed to save availability" : "Kunne ikke gemme tilgængelighed",
+ "Time zone:" : "Tidszone:",
+ "to" : "til",
+ "Delete slot" : "Slet slot",
+ "No working hours set" : "Arbejdstider er ikke sat",
+ "Add slot" : "Tilføj slot",
+ "Weekdays" : "Hverdage",
+ "Pick a start time for {dayName}" : "Vælg et starttidspunkt for {dayName}",
+ "Pick a end time for {dayName}" : "Vælg et sluttidspunkt for {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Indstil automatisk brugerstatus til \"Forstyr ikke\" uden for tilgængelighed for at slå alle notifikationer fra.",
+ "Cancel" : "Annuller",
+ "Import" : "Importér",
+ "Error while saving settings" : "Der opstod en fejl under lagring af indstillinger",
+ "Contact reset successfully" : "Kontakten blev nulstillet",
+ "Error while resetting contact" : "Fejl under nulstilling af kontakt",
+ "Contact imported successfully" : "Kontakten blev importeret",
+ "Error while importing contact" : "Fejl under import af kontakt",
+ "Import contact" : "Importér kontakt",
+ "Reset to default" : "Nulstil",
+ "Import contacts" : "Importér kontakter",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Import af en ny .vcf-fil vil slette den eksisterende standardkontakt og erstatte den med den nye. Vil du fortsætte?",
+ "Availability" : "tilgængelighed",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Hvis du konfigurerer dine arbejdstider, vil andre se, når du er fraværende, når de booker et møde.",
+ "Absence" : "Fravær",
+ "Configure your next absence period." : "Konfigurer din næste fraværsperiode.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installer også {calendarappstoreopen}Kalender-appen{linkclose}, eller {calendardocopen}tilslut dit skrivebord og din mobil til synkronisering ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Sørg for at konfigurere {emailopen}e-mail-serveren{linkclose} korrekt.",
"Calendar server" : "Kalenderserver",
"Send invitations to attendees" : "Send invitation til deltagere",
"Automatically generate a birthday calendar" : "Generer en fødselsdagskalender automatisk",
"Birthday calendars will be generated by a background job." : "Fødselsdagskalendere vil blive oprettet af et job, der kører i baggrunden.",
"Hence they will not be available immediately after enabling but will show up after some time." : "Derfor vil de ikke blive synlige med det samme efter aktivering, men vil vise sig efter noget tid.",
+ "Send notifications for events" : "Send meddelelser om begivenheder",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Underretninger sendes via baggrundsjob, så disse skal ske ofte nok.",
+ "Send reminder notifications to calendar sharees as well" : "Send også påmindelsesmeddelelser til kalenderdelinger",
+ "Reminders are always sent to organizers and attendees." : "Påmindelser sendes altid til arrangører og deltagere.",
+ "Enable notifications for events via push" : "Aktiver notifikationer for begivenheder via push",
+ "There was an error updating your attendance status." : "Der opstod en fejl under opdatering af din fremmødestatus.",
+ "Please contact the organizer directly." : "Kontakt venligst arrangøren direkte.",
"Are you accepting the invitation?" : "Accepter du invitationen?",
"Tentative" : "Foreløbig",
- "Comment" : "Kommentér"
+ "Your attendance was updated successfully." : "Dit tilstedeværelse blev opdateret."
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/da.json b/apps/dav/l10n/da.json
index 1a70d847ccc..723ebc26b7d 100644
--- a/apps/dav/l10n/da.json
+++ b/apps/dav/l10n/da.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Kalender",
- "Todos" : "Opgaver",
+ "Tasks" : "Opgaver",
"Personal" : "Personligt",
"{actor} created calendar {calendar}" : "{actor} oprettede kalenderen {calendar}",
"You created calendar {calendar}" : "Du oprettede kalenderen {calendar}",
@@ -23,36 +23,41 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} delte kalenderen {calendar} med gruppen {group}",
"You unshared calendar {calendar} from group {group}" : "Du fjernede delingen af {calendar} fra gruppen {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} fjernede deling af kalenderen {calendar} fra gruppen {group}",
+ "Untitled event" : "Unavngiven begivenhed",
"{actor} created event {event} in calendar {calendar}" : "{actor} oprettede begivenheden {event} i kalenderen {calendar}",
"You created event {event} in calendar {calendar}" : "Du oprettede begivenheden {event} i kalenderen {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} slettede begivenheden {event} fra kalenderen {calendar}",
"You deleted event {event} from calendar {calendar}" : "Du slettede begivenheden {event} fra kalenderen {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} opdaterede begivenheden {event} i kalenderen {calendar}",
"You updated event {event} in calendar {calendar}" : "Du opdaterede begivenheden {event} i kalenderen {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} flyttede begivenhed {event} fra kalender {sourceCalendar} til kalender {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Du flyttede begivenhed {event} fra kalender {sourceCalendar} til kalender {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} gendannede begivenhed {event} i kalender {calendar}",
- "You restored event {event} of calendar {calendar}" : "Du gendannede begivenhed {begivenhed} i kalender {kalender}",
+ "You restored event {event} of calendar {calendar}" : "Du gendannede begivenhed {event} i kalender {calendar}",
"Busy" : "Optaget",
- "{actor} created todo {todo} in list {calendar}" : "{actor} oprettede en opgave {todo} i listen {calendar}",
- "You created todo {todo} in list {calendar}" : "Du oprettede opgaven {todo} i listen {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} slettede opgaven {todo} fra listen {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Du slettede opgaven {todo} fra listen {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} opdaterede opgaven {todo} i listen {calendar}",
- "You updated todo {todo} in list {calendar}" : "Du opdaterede opgaven {todo} i listen {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} løste opgaven {todo} i listen {calendar}",
- "You solved todo {todo} in list {calendar}" : "Du løste opgaven {todo} i listen {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} genåbnede opgaven {todo} i listen {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Du genåbnede opgaven {todo} i listen {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} oprettede en opgave {todo} i listen {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Du oprettede opgaven {todo} i listen {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} slettede opgaven {todo} fra listen {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Du slettede opgaven {todo} fra listen {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} opdaterede opgaven {todo} i listen {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Du opdaterede opgaven {todo} i listen {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} løste opgaven {todo} i listen {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Du løste opgaven {todo} i listen {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} genåbnede opgaven {todo} i listen {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Du genåbnede opgaven {todo} i listen {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} flyttede opgave {todo} fra liste {sourceCalendar} til liste {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Du flyttede opgave {todo} fra liste {sourceCalendar} til liste {targetCalendar}",
"Calendar, contacts and tasks" : "Kalender, kontakter og opgaver",
"A <strong>calendar</strong> was modified" : "En <strong>kalender</strong> er blevet ændret",
"A calendar <strong>event</strong> was modified" : "En kalender <strong>begivenhed</strong> er blevet ændret",
- "A calendar <strong>todo</strong> was modified" : "En kalender <strong>opgave</strong> blev ændret",
+ "A calendar <strong>to-do</strong> was modified" : "En kalender <strong>opgave</strong> blev ændret",
"Contact birthdays" : "Kontakt fødselsdag",
"Death of %s" : "Død af%s",
+ "Untitled calendar" : "Unanvngiven kalender",
"Calendar:" : "Kalender:",
"Date:" : "Dato:",
"Where:" : "Hvor:",
"Description:" : "Beskrivelse:",
- "Untitled event" : "Unavngiven begivenhed",
"_%n year_::_%n years_" : ["%n år","%n år"],
"_%n month_::_%n months_" : ["%n måned","%n måneder"],
"_%n day_::_%n days_" : ["%n dag","%n dage"],
@@ -65,26 +70,100 @@
"Description: %s" : "Beskrivelse: %s",
"Where: %s" : "Hvor: %s",
"%1$s via %2$s" : "%1$s via %2$s",
- "Invitation canceled" : "Invitation annulleret",
- "Invitation updated" : "Invitation opdateret ",
- "Invitation" : "Invitation",
+ "In the past on %1$s for the entire day" : "Tidligere den %1$s for hele dagen",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Om et minut på %1$s for hele dagen","Om %n minutter den %1$s for hele dagen"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Om en time på %1$s for hele dagen","Om %n timer den %1$s for hele dagen"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["I en dag på %1$s for hele dagen","Om %n dage den %1$s for hele dagen"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["I en uge på %1$s for hele dagen","Om %n uger den %1$s for hele dagen"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["I en måned på %1$s for hele dagen","Om %n måneder den %1$s for hele dagen"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["I et år på %1$s for hele dagen","Om %n år den %1$s for hele dagen"],
+ "In the past on %1$s between %2$s - %3$s" : "Tidligere den %1$s mellem %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["I et minut på %1$s mellem %2$s - %3$s","Om %n minutter den %1$s mellem %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["I en time på %1$s mellem %2$s - %3$s","Om %n timer den %1$s mellem %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["I en dag på %1$s mellem %2$s - %3$s","Om %n dage den %1$s mellem %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["I en uge på %1$s mellem %2$s - %3$s","Om %n uger den %1$s mellem %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["I en måned på %1$s mellem %2$s - %3$s","Om %n måneder den %1$s mellem %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["I et år på %1$s mellem %2$s - %3$s","Om %n år den %1$s mellem %2$s - %3$s"],
+ "Could not generate when statement" : "Kunne ikke generere when sætning",
+ "Every Day for the entire day" : "Hver dag hele dagen",
+ "Every Day for the entire day until %1$s" : "Hver dag hele dagen indtil %1$s",
+ "Every Day between %1$s - %2$s" : "Hver dag mellem %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Hver dag mellem %1$s - %2$s indtil %3$s",
+ "Every %1$d Days for the entire day" : "Hver %1$d dage for hele dagen",
+ "Every %1$d Days for the entire day until %2$s" : "Hver %1$d dage for hele dagen indtil %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Hver %1$d dage mellem %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Hver %1$d dage mellem %2$s - %3$s indtil %4$s",
+ "Could not generate event recurrence statement" : "Kunne ikke generere erklæring for begivenhedsgentagelse",
+ "Every Week on %1$s for the entire day" : "Hver uge den %1$s for hele dagen",
+ "Every Week on %1$s for the entire day until %2$s" : "Hver uge den %1$s for hele dagen indtil %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Hver uge den %1$s mellem %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Hver uge den %1$s mellem %2$s - %3$s indtil %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Hver %1$d uger den %2$s for hele dagen",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Hver %1$d uger den %2$s for hele dagen indtil %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Hver %1$d uger den %2$s mellem %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Hver %1$d uger den %2$s mellem %3$s - %4$s indtil %5$s",
+ "Every Month on the %1$s for the entire day" : "Hver måned den %1$s for hele dagen",
+ "Every Month on the %1$s for the entire day until %2$s" : "Hver måned den %1$s for hele dagen indtil %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Hver måned den %1$s mellem %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Hver måned den %1$s mellem %2$s - %3$s indtil %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Hver %1$d måneder den %2$s for hele dagen",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Hver %1$d måneder den %2$s for hele dagen indtil %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Hver %1$d måneder den %2$s mellem %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Hver %1$d måneder den %2$s mellem %3$s - %4$s indtil %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Hvert år i %1$s den %2$s for hele dagen",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Hvert år i %1$s den %2$s for hele dagen indtil %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Hvert år i %1$s den %2$s mellem %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Hvert år i %1$s den %2$s mellem %3$s - %4$s indtil %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Hver %1$d år i %2$s den %3$s for hele dagen",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Hvert %1$d år i %2$s den %3$s for hele dagen indtil %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Hvert %1$d år i %2$s den %3$s mellem %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Hvert %1$d år i %2$s den %3$s mellem %4$s - %5$s indtil %6$s",
+ "On specific dates for the entire day until %1$s" : "På specifikke datoer for hele dagen indtil %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "På specifikke datoer mellem %1$s - %2$s indtil %3$s",
+ "In the past on %1$s" : "Tidligere den %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["I et minut på %1$s","Om %n minutter den %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["I en time på %1$s","Om %n timer den %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["I en dag på %1$s","Om %n dage den %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["I en uge på %1$s","Om %n uger den %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["I en måned på %1$s","Om %n måneder den %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["I et år på %1$s","Om %n år den %1$s"],
+ "In the past on %1$s then on %2$s" : "Tidligere på %1$s derefter den %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["I et minut på %1$s så på %2$s","Om %n minutter den %1$s derefter den %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["I en time på %1$s så på %2$s","Om %n timer den %1$s derefter den %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["I en dag på %1$s så på %2$s","Om %n dage den %1$s derefter den %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["I en uge på %1$s så på %2$s","Om %n uger den %1$s derefter den %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["I en måned på %1$s så på %2$s","Om %n måneder den %1$s derefter den %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["I et år på %1$s så på %2$s","Om %n år den %1$s derefter den %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "Tidligere den %1$s derefter den %2$s og %3$s",
+ "_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_" : ["I et minut på %1$s så på %2$s og %3$s","Om %n minutter den %1$s derefter den %2$s og %3$s"],
+ "_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_" : ["I en time på %1$s så på %2$s og %3$s","Om %n timer den %1$s derefter den %2$s og %3$s"],
+ "_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_" : ["I en dag på %1$s så på %2$s og %3$s","Om %n dage den %1$s derefter den %2$s og %3$s"],
+ "_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_" : ["I en uge på %1$s så på %2$s og %3$s","Om %n uger den %1$s derefter den %2$s og %3$s"],
+ "_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_" : ["I en måned på %1$s så på %2$s og %3$s","Om %n måneder den %1$s derefter den %2$s og %3$s"],
+ "_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_" : ["I et år på %1$s så på %2$s og %3$s","Om %n år den %1$s derefter den %2$s og %3$s"],
+ "Could not generate next recurrence statement" : "Kunne ikke generere næste gentagelseserklæring",
+ "Cancelled: %1$s" : "Annullerede: %1$s",
+ "\"%1$s\" has been canceled" : "\"%1$s\" er blevet annulleret",
+ "Re: %1$s" : "Re: %1$s",
+ "%1$s has accepted your invitation" : "%1$s har accepteret din invitation",
+ "%1$s has tentatively accepted your invitation" : "%1$s har foreløbigt accepteret din invitation",
+ "%1$s has declined your invitation" : "%1$s har afvist din invitation",
+ "%1$s has responded to your invitation" : "%1$s har svaret på din invitation",
+ "Invitation updated: %1$s" : "Invitation opdateret: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s opdaterede begivenheden \"%2$s\"",
+ "Invitation: %1$s" : "Invitation: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s vil gerne invitere dig til \"%2$s\"",
+ "Organizer:" : "Arrangør:",
+ "Attendees:" : "Deltagere:",
"Title:" : "Titel:",
- "Time:" : "Tid:",
+ "When:" : "Hvornår:",
"Location:" : "Sted:",
"Link:" : "Link:",
- "Organizer:" : "Arrangør:",
- "Attendees:" : "Deltagere:",
+ "Occurring:" : "Forekomst:",
"Accept" : "Accepter",
"Decline" : "Afvis",
"More options …" : "Flere indstillinger…",
- "Contacts" : "Kontakter",
- "System is in maintenance mode." : "Systemet er i vedligeholdelsestilstand.",
- "Upgrade needed" : "Opgradering er nødvendig",
- "Tasks" : "Opgaver",
- "Contacts and groups" : "Kontakter og grupper",
- "WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV endpoint",
- "to" : "til",
+ "More options at %s" : "Flere muligheder på %s",
"Monday" : "Mandag",
"Tuesday" : "Tirsdag",
"Wednesday" : "Onsdag",
@@ -92,14 +171,154 @@
"Friday" : "Fredag",
"Saturday" : "Lørdag",
"Sunday" : "Søndag",
+ "January" : "Januar",
+ "February" : "Februar",
+ "March" : "Marts",
+ "April" : "April",
+ "May" : "Maj",
+ "June" : "Juni",
+ "July" : "Juli",
+ "August" : "August",
+ "September" : "September",
+ "October" : "Oktober",
+ "November" : "November",
+ "December" : "December",
+ "First" : "Første",
+ "Second" : "Andet",
+ "Third" : "Tredje",
+ "Fourth" : "Fjerde",
+ "Fifth" : "Femte",
+ "Last" : "Sidste",
+ "Second Last" : "Anden sidste",
+ "Third Last" : "Tredje sidste",
+ "Fourth Last" : "Fjerde sidste",
+ "Fifth Last" : "Femte sidste",
+ "Contacts" : "Kontakter",
+ "{actor} created address book {addressbook}" : "{actor} oprettede adressebog {addressbook}",
+ "You created address book {addressbook}" : "Du oprettede adressebog {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} slattede adressebog {addressbook}",
+ "You deleted address book {addressbook}" : "Du slettede adressebog {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} opdaterede adressebog {addressbook}",
+ "You updated address book {addressbook}" : "Du opdaterede adressebog {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} delte adressebog {addressbook} med dig",
+ "You shared address book {addressbook} with {user}" : "Du delte adressebog {addressbook} med {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} delte adressebog {addressbook} med {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} fjernede delingen af adressebog {addressbook} med dig",
+ "You unshared address book {addressbook} from {user}" : "Du fjernede delingen af adressebog {addressbook} med {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} fjernede delingen af adressebog {addressbook} med {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} fjernede delingen af adressebog {addressbook} med sig selv",
+ "You shared address book {addressbook} with group {group}" : "Du delte adressebog {addressbook} med gruppen {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} delte adressebog {addressbook} med gruppen {group}",
+ "You unshared address book {addressbook} from group {group}" : "Du fjernede delingen af adressebog {addressbook} fra gruppen {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} fjernede delingen af adressebog {addressbook} med gruppen {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} oprettede kontakten {card} i adressebog {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Du oprettede kontakten {card} i adressebog {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} slettede kontakten {card} i adressebog {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Du slettede kontakten {card} i adressebog {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} opdaterede kontakten {card} i adressebog {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Du opdaterede kontakten {card} i adressebog {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "En <strong>kontakt</strong> eller <strong>adressebog</strong> blev ændret",
+ "Accounts" : "Konti",
+ "System address book which holds all accounts" : "Systemets adressebog, som indeholder alle konti",
+ "File is not updatable: %1$s" : "Filen kan ikke updateres: %1$s",
+ "Failed to get storage for file" : "Kunne ikke hente lagerplads til filen",
+ "Could not write to final file, canceled by hook" : "Kunne ikke skrive til den endelige fil, annulleret af hook",
+ "Could not write file contents" : "Kunne ikke skrive filindhold",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Fejl under kopiering af fil til placering (kopieret: %1$s, forventet filstørrelse: %2$s)",
+ "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." : "Forventede filstørrelse på %1$s, men læste (fra Nextcloud-klienten) og skrev (til Nextcloud-lageret) %2$s. Det kan enten være et netværksproblem på afsendersiden eller et problem med at skrive til lageret på serversiden.",
+ "Could not rename part file to final file, canceled by hook" : "Kunne ikke omdøbe delfilen til den endelige fil, annulleret af hook",
+ "Could not rename part file to final file" : "Delfilen kunne ikke omdøbes til den endelige fil",
+ "Failed to check file size: %1$s" : "Kunne ikke kontrollere filstørrelsen: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "Kunne ikke åbne filen: %1$s, filen ser ud til at eksistere",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Kunne ikke åbne filen: %1$s, filen ser ikke ud til at eksistere",
+ "Encryption not ready: %1$s" : "Kryptering ikke klar: %1$s",
+ "Failed to open file: %1$s" : "Kunne ikke åbne fil: %1$s",
+ "Failed to unlink: %1$s" : "Tilknytningen kunne ikke fjernes: %1$s",
+ "Failed to write file contents: %1$s" : "Kunne ikke skrive filindhold: %1$s",
+ "File not found: %1$s" : "Fil ikke fundet: %1$s",
+ "Invalid target path" : "Ugyldig målsti",
+ "System is in maintenance mode." : "Systemet er i vedligeholdelsestilstand.",
+ "Upgrade needed" : "Opgradering er nødvendig",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Din %s skal konfigureres til at bruge HTTPS for at kunne bruge CalDAV og CardDAV med iOS/macOS.",
+ "Configures a CalDAV account" : "Konfigurerer en CalDAV-konto",
+ "Configures a CardDAV account" : "Konfigurerer en CardDAV-konto",
+ "Events" : "Begivenheder",
+ "Untitled task" : "Unavngivet opgave",
+ "Completed on %s" : "Fuldført den %s",
+ "Due on %s by %s" : "Forfalder på %s til %s",
+ "Due on %s" : "Forfalder på %s",
+ "System Address Book" : "System adressebog",
+ "The system address book contains contact information for all users in your instance." : "System adressebogen indeholder kontaktoplysninger for alle brugere i din instans.",
+ "Enable System Address Book" : "Aktivér System adressebog",
+ "DAV system address book" : "DAV system adressebog",
+ "No outstanding DAV system address book sync." : "Ingen udestående synkronisering af DAV-systemets adressebog.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV-systemets adressebogssynkronisering er ikke kørt endnu, da din instans har mere end 1000 brugere, eller fordi der opstod en fejl. Kør det manuelt ved at kalde \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "WebDAV endpoint",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Kunne ikke kontrollere, at din webserver er korrekt konfigureret til at tillade filsynkronisering over WebDAV. Tjek venligst manuelt.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Din webserver er endnu ikke sat korrekt op til at tillade filsynkronisering, fordi WebDAV-grænsefladen ser ud til at være i stykker.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Din webserver er korrekt konfigureret til at tillade filsynkronisering over WebDAV.",
+ "Migrated calendar (%1$s)" : "Migreret kalender (%1$s)",
+ "Calendars including events, details and attendees" : "Kalendere indeholdende begivenheder, detaljer og deltagere",
+ "Contacts and groups" : "Kontakter og grupper",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "Fraværet er gemt",
+ "Failed to save your absence settings" : "Kunne ikke gemme dine fraværsindstillinger",
+ "Absence cleared" : "Fravær fjernet",
+ "Failed to clear your absence settings" : "Kunne ikke rydde dine fraværsindstillinger",
+ "First day" : "Første dag",
+ "Last day (inclusive)" : "Sidste dag (indklusiv)",
+ "Out of office replacement (optional)" : "Ikke på kontoret udskiftning (valgfrit)",
+ "Name of the replacement" : "Navn på udskiftning",
+ "No results." : "Ingen resultater.",
+ "Start typing." : "Begynd at skrive.",
+ "Short absence status" : "Kort fraværsstatus",
+ "Long absence Message" : "Langt fravær besked",
"Save" : "Gem",
+ "Disable absence" : "Deaktiver fravær",
+ "Failed to load availability" : "Kunne ikke indlæse tilgængelighed",
+ "Saved availability" : "Gemt tilgængelighed",
+ "Failed to save availability" : "Kunne ikke gemme tilgængelighed",
+ "Time zone:" : "Tidszone:",
+ "to" : "til",
+ "Delete slot" : "Slet slot",
+ "No working hours set" : "Arbejdstider er ikke sat",
+ "Add slot" : "Tilføj slot",
+ "Weekdays" : "Hverdage",
+ "Pick a start time for {dayName}" : "Vælg et starttidspunkt for {dayName}",
+ "Pick a end time for {dayName}" : "Vælg et sluttidspunkt for {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Indstil automatisk brugerstatus til \"Forstyr ikke\" uden for tilgængelighed for at slå alle notifikationer fra.",
+ "Cancel" : "Annuller",
+ "Import" : "Importér",
+ "Error while saving settings" : "Der opstod en fejl under lagring af indstillinger",
+ "Contact reset successfully" : "Kontakten blev nulstillet",
+ "Error while resetting contact" : "Fejl under nulstilling af kontakt",
+ "Contact imported successfully" : "Kontakten blev importeret",
+ "Error while importing contact" : "Fejl under import af kontakt",
+ "Import contact" : "Importér kontakt",
+ "Reset to default" : "Nulstil",
+ "Import contacts" : "Importér kontakter",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Import af en ny .vcf-fil vil slette den eksisterende standardkontakt og erstatte den med den nye. Vil du fortsætte?",
+ "Availability" : "tilgængelighed",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Hvis du konfigurerer dine arbejdstider, vil andre se, når du er fraværende, når de booker et møde.",
+ "Absence" : "Fravær",
+ "Configure your next absence period." : "Konfigurer din næste fraværsperiode.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installer også {calendarappstoreopen}Kalender-appen{linkclose}, eller {calendardocopen}tilslut dit skrivebord og din mobil til synkronisering ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Sørg for at konfigurere {emailopen}e-mail-serveren{linkclose} korrekt.",
"Calendar server" : "Kalenderserver",
"Send invitations to attendees" : "Send invitation til deltagere",
"Automatically generate a birthday calendar" : "Generer en fødselsdagskalender automatisk",
"Birthday calendars will be generated by a background job." : "Fødselsdagskalendere vil blive oprettet af et job, der kører i baggrunden.",
"Hence they will not be available immediately after enabling but will show up after some time." : "Derfor vil de ikke blive synlige med det samme efter aktivering, men vil vise sig efter noget tid.",
+ "Send notifications for events" : "Send meddelelser om begivenheder",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Underretninger sendes via baggrundsjob, så disse skal ske ofte nok.",
+ "Send reminder notifications to calendar sharees as well" : "Send også påmindelsesmeddelelser til kalenderdelinger",
+ "Reminders are always sent to organizers and attendees." : "Påmindelser sendes altid til arrangører og deltagere.",
+ "Enable notifications for events via push" : "Aktiver notifikationer for begivenheder via push",
+ "There was an error updating your attendance status." : "Der opstod en fejl under opdatering af din fremmødestatus.",
+ "Please contact the organizer directly." : "Kontakt venligst arrangøren direkte.",
"Are you accepting the invitation?" : "Accepter du invitationen?",
"Tentative" : "Foreløbig",
- "Comment" : "Kommentér"
+ "Your attendance was updated successfully." : "Dit tilstedeværelse blev opdateret."
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/dav/l10n/de.js b/apps/dav/l10n/de.js
index 10380d482d4..496a658c833 100644
--- a/apps/dav/l10n/de.js
+++ b/apps/dav/l10n/de.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Kalender",
- "Todos" : "Aufgaben",
+ "Tasks" : "Aufgaben",
"Personal" : "Persönlich",
"{actor} created calendar {calendar}" : "{actor} hat den Kalender {calendar} erstellt",
"You created calendar {calendar}" : "Du hast den Kalender {calendar} erstellt",
@@ -14,10 +14,10 @@ OC.L10N.register(
"You restored calendar {calendar}" : "Du hast den Kalender {calendar} wiederhergestellt",
"You shared calendar {calendar} as public link" : "Du hast den Kalender {calendar} als öffentlichen Link geteilt",
"You removed public link for calendar {calendar}" : "Du hast den öffentlichen Link für Kalender {calendar} entfernt",
- "{actor} shared calendar {calendar} with you" : "{actor} hat den Kalender {calendar} mit Dir geteilt",
+ "{actor} shared calendar {calendar} with you" : "{actor} hat den Kalender {calendar} mit dir geteilt",
"You shared calendar {calendar} with {user}" : "Du hast den Kalender {calendar} mit {user} geteilt",
"{actor} shared calendar {calendar} with {user}" : "{actor} hat den Kalender {calendar} mit {user} geteilt",
- "{actor} unshared calendar {calendar} from you" : "{actor} teilt den Kalender {calendar} nicht mehr mit Dir",
+ "{actor} unshared calendar {calendar} from you" : "{actor} teilt den Kalender {calendar} nicht mehr mit dir",
"You unshared calendar {calendar} from {user}" : "Du teilst den Kalender {calendar} nicht mehr mit {user}",
"{actor} unshared calendar {calendar} from {user}" : "{actor} teilt den Kalender {calendar} nicht mehr mit {user}",
"{actor} unshared calendar {calendar} from themselves" : "{actor} teilt den Kalender {calendar} nicht mehr mit sich selbst",
@@ -25,36 +25,41 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} teilt den Kalender {calendar} mit der Gruppe {group}",
"You unshared calendar {calendar} from group {group}" : "Du teilst den Kalender {calendar} nicht mehr mit der Gruppe {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} teilt den Kalender {calendar} nicht mehr mit der Gruppe {group}",
+ "Untitled event" : "Unbenannter Termin",
"{actor} created event {event} in calendar {calendar}" : "{actor} hat den Termin {event} im Kalender {calendar} erstellt",
"You created event {event} in calendar {calendar}" : "Du hast den Termin {event} im Kalender {calendar} erstellt",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} hat den Termin {event} im Kalender {calendar} gelöscht",
"You deleted event {event} from calendar {calendar}" : "Du hast den Termin {event} im Kalender {calendar} gelöscht",
"{actor} updated event {event} in calendar {calendar}" : "{actor} hat den Termin {event} im Kalender {calendar} aktualisiert",
"You updated event {event} in calendar {calendar}" : "Du hast den Termin {event} im Kalender {calendar} aktualisiert",
- "{actor} restored event {event} of calendar {calendar}" : "{actor} hat das Adressbuch {addressbook} mit Dir geteilt",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} hat das Ereignis {event} vom Kalender {sourceCalendar} in den Kalender {targetCalendar} verschoben",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Du hast das Ereignis {event} vom Kalender {sourceCalendar} in den Kalender {targetCalendar} verschoben",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} hat den Termin {event} in Kalender {calendar} wiederhergestellt",
"You restored event {event} of calendar {calendar}" : "Du hast den Termin {event} im Kalender {calendar} wiederhergestellt",
"Busy" : "Beschäftigt",
- "{actor} created todo {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} erstellt",
- "You created todo {todo} in list {calendar}" : "Du hast die Aufgabe {todo} in der Liste {calendar} erstellt",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} gelöscht",
- "You deleted todo {todo} from list {calendar}" : "Du hast die Aufgabe {todo} in der Liste {calendar} gelöscht",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} aktualisiert",
- "You updated todo {todo} in list {calendar}" : "Du hast die Aufgabe {todo} in der Liste {calendar} aktualisiert",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} erledigt",
- "You solved todo {todo} in list {calendar}" : "Du hast die Aufgabe {todo} in der Liste {calendar} erledigt",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} wiedereröffnet",
- "You reopened todo {todo} in list {calendar}" : "Du hast die Aufgabe {todo} in der Liste {calendar} wiedereröffnet",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} erstellt",
+ "You created to-do {todo} in list {calendar}" : "Du hast eine Aufgabe {todo} in der Liste {calendar} erstellt",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} hat die Aufgabe {todo} aus der Liste {calendar} gelöscht",
+ "You deleted to-do {todo} from list {calendar}" : "Du hast die Aufgabe {todo} aus der Liste {calendar} gelöscht",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} aktualisiert",
+ "You updated to-do {todo} in list {calendar}" : "Du hast die Aufgabe {todo} in der Liste {calendar} aktualisiert",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} erledigt",
+ "You solved to-do {todo} in list {calendar}" : "Du hast die Aufgabe {todo} in der Liste {calendar} erledigt",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} wiedereröffnet",
+ "You reopened to-do {todo} in list {calendar}" : "Du hast die Aufgabe {todo} in der Liste {calendar} wiedereröffnet",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} hat die Aufgabe {todo} von der Liste {sourceCalendar} in die Liste {targetCalendar} verschoben",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Du hast die Aufgabe {todo} von der Liste {sourceCalendar} in die Liste {targetCalendar} verschoben",
"Calendar, contacts and tasks" : "Kalender, Kontakte und Aufgaben",
"A <strong>calendar</strong> was modified" : "Ein <strong>Kalender</strong> wurde bearbeitet",
"A calendar <strong>event</strong> was modified" : "Ein Kalender-<strong>Termin</strong> wurde bearbeitet",
- "A calendar <strong>todo</strong> was modified" : "Eine Kalender-<strong>Aufgabe</strong> wurde bearbeitet",
+ "A calendar <strong>to-do</strong> was modified" : "Eine Kalender-<strong>Aufgabe</strong> wurde geändert",
"Contact birthdays" : "Geburtstage von Kontakten",
"Death of %s" : "Todestag von %s",
+ "Untitled calendar" : "Unbenannter Kalender",
"Calendar:" : "Kalender:",
"Date:" : "Datum:",
"Where:" : "Wo:",
"Description:" : "Beschreibung:",
- "Untitled event" : "Termin ohne Titel",
"_%n year_::_%n years_" : ["%n Jahr","%n Jahre"],
"_%n month_::_%n months_" : ["%n Monat","%n Monate"],
"_%n day_::_%n days_" : ["%n Tag","%n Tage"],
@@ -67,22 +72,129 @@ OC.L10N.register(
"Description: %s" : "Beschreibung: %s",
"Where: %s" : "Ort: %s",
"%1$s via %2$s" : "%1$s über %2$s",
+ "In the past on %1$s for the entire day" : "In der Vergangenheit am %1$s für den ganzen Tag",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["In einer Minute um %1$s für den ganzen Tag","In %n Minuten um %1$s für den ganzen Tag"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["In einer Stunde um %1$s für den ganzen Tag","In %n Stunden um %1$s für den ganzen Tag"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["In einem Tag am %1$s für den ganzen Tag","In %n Tagen am %1$s für den ganzen Tag"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["In einer Woche am %1$s den ganzen Tag","In %n Wochen am %1$s den ganzen Tag"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["In einem Monat am %1$s den ganzen Tag","In %n Monaten am %1$s den ganzen Tag"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["In einem Jahr am %1$s für den ganzen Tag","In %n Jahren am %1$s für den ganzen Tag"],
+ "In the past on %1$s between %2$s - %3$s" : "In der Vergangenheit am %1$s zwischen %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["In einer Minute am%1$s zwischen %2$s - %3$s","In %n Minuten am %1$s zwischen %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["In einer Stunde am %1$s zwischen %2$s - %3$s","In %n Stunden am%1$s zwischen %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["In einem Tag am %1$s zwischen %2$s - %3$s","In %n Tagen am %1$szwischen %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["In einer Woche am %1$s zwischen%2$s - %3$s","In %n Wochen am%1$s zwischen %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["In einem Monat am %1$s zwischen %2$s - %3$s","In %n Monaten am %1$s zwischen %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["In einem Jahr am %1$s zwischen %2$s - %3$s","In %n Jahren am%1$s zwischen %2$s - %3$s"],
+ "Could not generate when statement" : "Wann-Angabe konnte nicht erzeugt werden.",
+ "Every Day for the entire day" : "Jeden Tag für den ganzen Tag",
+ "Every Day for the entire day until %1$s" : "Jeden Tag für den ganzen Tag bis %1$s",
+ "Every Day between %1$s - %2$s" : "Jeden Tag zwischen %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Jeden Tag zwischen %1$s - %2$s bis %3$s",
+ "Every %1$d Days for the entire day" : "Alle %1$d Tage für den ganzen Tag",
+ "Every %1$d Days for the entire day until %2$s" : "Alle %1$d Tage für den ganzen Tag bis %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Alle %1$d Tage zwischen %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Alle %1$d Tage zwischen %2$s - %3$s bis %4$s",
+ "Could not generate event recurrence statement" : "Angabe für Terminwiederholung konnte nicht erzeugt werden.",
+ "Every Week on %1$s for the entire day" : "Jede Woche am %1$s für den ganzen Tag",
+ "Every Week on %1$s for the entire day until %2$s" : "Jede Woche am %1$s für den ganzen Tag bis %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Jede Woche am %1$s zwischen %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Jede Woche am %1$s zwischen %2$s - %3$s bis %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Alle %1$d Wochen am %2$s für den ganzen Tag",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Alle %1$d Wochen am %2$s für den ganzen Tag bis %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Alle %1$d Wochen am %2$s zwischen %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Alle %1$d Wochen am %2$s zwischen %3$s - %4$s bis %5$s",
+ "Every Month on the %1$s for the entire day" : "Jeden Monat am %1$s für den ganzen Tag",
+ "Every Month on the %1$s for the entire day until %2$s" : "Jeden Monat am %1$s für den ganzen Tag bis %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Jeden Monat am %1$s zwischen %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Jeden Monat am %1$s zwischen %2$s - %3$s bis %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Alle %1$d Monate am %2$s für den ganzen Tag",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Alle %1$d Monate am %2$s für den ganzen Tag bis %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Alle %1$d Monate am %2$s zwischen %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Alle %1$d Monate am %2$s zwischen %3$s - %4$s bis %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Jedes Jahr im %1$s am %2$s für den ganzen Tag",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Jedes Jahr im %1$s am %2$s für den ganzen Tag bis %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Jedes Jahr im %1$s am %2$s zwischen %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Jedes Jahr im %1$s am %2$s zwischen %3$s - %4$s bis %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Alle %1$d Jahre im %2$s am %3$s für den ganzen Tag",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Alle %1$d Jahre im %2$s am %3$s für den ganzen Tag bis %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Alle %1$d Jahre im %2$s am %3$s zwischen %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Alle %1$d Jahre im %2$s am %3$s zwischen %4$s - %5$s bis %6$s",
+ "On specific dates for the entire day until %1$s" : "An bestimmten Tagen für den ganzen Tag bis %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "An bestimmten Tagen zwischen %1$s - %2$s bis %3$s",
+ "In the past on %1$s" : "In der Vergangenheit am %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["In einer Minute am %1$s","In %n Minuten am %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["In einer Stunde am %1$s","In %n Stunden am %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["In einem Tag am %1$s","In %n Tagen am %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["In einer Woche am %1$s","In %n Wochen am %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["In einem Monat am %1$s","In %n Monaten am %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["In einem Jahr am %1$s","In %n Jahren am %1$s"],
+ "In the past on %1$s then on %2$s" : "In der Vergangenheit am %1$s danach am %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["In einer Minute am %1$s danach am %2$s","In %n Minuten am %1$s danach am %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["In einer Stunde am %1$s danach am %2$s","In %n Stunden am %1$s danach am %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["In einem Tag am %1$s danach am %2$s","In %n Tagen am %1$s danach am %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["In einer Woche am %1$s danach am %2$s","In %n Wochen am %1$s danach am %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["In einem Monat am %1$s danach am %2$s","In %n Monaten am %1$s danach am %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["In einem Jahr am %1$s danach am %2$s","In %n Jahren am %1$s danach am %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "In der Vergangenheit am %1$s danach am %2$s und %3$s",
+ "_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_" : ["In einer Minute am %1$s danach am %2$s und %3$s","In %n Minuten am %1$s danach am %2$s und %3$s"],
+ "_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_" : ["In einer Stunde am %1$s danach am %2$s und %3$s","In %n Stunden am %1$s danach am %2$s und %3$s"],
+ "_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_" : ["In einem Tag am %1$s danach am %2$s und %3$s","In %n Tagen am %1$s danach am %2$s und %3$s"],
+ "_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_" : ["In einer Woche am %1$s danach am %2$s und %3$s","In %n Wochen am %1$s danach am %2$s und %3$s"],
+ "_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_" : ["In einem Monat am %1$s danach am %2$s und %3$s","In %n Monaten am %1$s danach am %2$s und %3$s"],
+ "_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_" : ["In einem Jahr am %1$s danach am %2$s und %3$s","In %n Jahren am %1$s danach am %2$s und %3$s"],
+ "Could not generate next recurrence statement" : "Angabe für nächste Wiederholung konnte nicht erzeugt werden.",
"Cancelled: %1$s" : "Abgesagt: %1$s",
- "Invitation canceled" : "Einladung abgebrochen",
+ "\"%1$s\" has been canceled" : "\"%1$s\" wurde abgesagt.",
"Re: %1$s" : "Re: %1$s",
- "Invitation updated" : "Einladung aktualisiert",
+ "%1$s has accepted your invitation" : "%1$s hat deine Einladung angenommen.",
+ "%1$s has tentatively accepted your invitation" : "%1$s hat deine Einladung vorläufig angenommen.",
+ "%1$s has declined your invitation" : "%1$s hat deine Einladung abgelehnt.",
+ "%1$s has responded to your invitation" : "%1$s hat auf deine Einladung geantwortet.",
+ "Invitation updated: %1$s" : "Einladung aktualisiert: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s hat die Veranstaltung \"%2$s\" aktualisiert",
"Invitation: %1$s" : "Einladung: %1$s",
- "Invitation" : "Einladung",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s möchte dich zu \"%2$s\" einladen.",
+ "Organizer:" : "Organisator:",
+ "Attendees:" : "Teilnehmer:",
"Title:" : "Titel:",
- "Time:" : "Zeit:",
+ "When:" : "Wann:",
"Location:" : "Ort:",
"Link:" : "Link:",
- "Organizer:" : "Organisator:",
- "Attendees:" : "Teilnehmer:",
+ "Occurring:" : "Findet statt:",
"Accept" : "Akzeptieren",
"Decline" : "Ablehnen",
"More options …" : "Weitere Optionen …",
"More options at %s" : "Weitere Optionen unter %s",
+ "Monday" : "Montag",
+ "Tuesday" : "Dienstag",
+ "Wednesday" : "Mittwoch",
+ "Thursday" : "Donnerstag",
+ "Friday" : "Freitag",
+ "Saturday" : "Samstag",
+ "Sunday" : "Sonntag",
+ "January" : "Januar",
+ "February" : "Februar",
+ "March" : "März",
+ "April" : "April",
+ "May" : "Mai",
+ "June" : "Juni",
+ "July" : "Juli",
+ "August" : "August",
+ "September" : "September",
+ "October" : "Oktober",
+ "November" : "November",
+ "December" : "Dezember",
+ "First" : "Erste",
+ "Second" : "Zweite",
+ "Third" : "Dritte",
+ "Fourth" : "Vierte",
+ "Fifth" : "Fünften",
+ "Last" : "Letzte",
+ "Second Last" : "Vorletztes",
+ "Third Last" : "Drittletztes",
+ "Fourth Last" : "Viertletztes",
+ "Fifth Last" : "Fünftletztes",
"Contacts" : "Kontakte",
"{actor} created address book {addressbook}" : "{actor} hat das Adressbuch {addressbook} erstellt",
"You created address book {addressbook}" : "Du hast das Adressbuch {addressbook} erstellt",
@@ -90,13 +202,13 @@ OC.L10N.register(
"You deleted address book {addressbook}" : "Du hast das Adressbuch {addressbook} gelöscht",
"{actor} updated address book {addressbook}" : "{actor} hat das Adressbuch {addressbook} aktualisiert",
"You updated address book {addressbook}" : "Du hast das Adressbuch {addressbook} aktualisiert",
- "{actor} shared address book {addressbook} with you" : "{actor} hat das Adressbuch {addressbook} mit Dir geteilt",
+ "{actor} shared address book {addressbook} with you" : "{actor} hat das Adressbuch {addressbook} mit dir geteilt",
"You shared address book {addressbook} with {user}" : "Du hast das Adressbuch {addressbook} geteilt",
"{actor} shared address book {addressbook} with {user}" : "{actor} hat das Adressbuch {addressbook} mit {user} geteilt",
- "{actor} unshared address book {addressbook} from you" : "{actor} teilt das Adressbuch {addressbook} nicht mehr mit Dir",
+ "{actor} unshared address book {addressbook} from you" : "{actor} teilt das Adressbuch {addressbook} nicht mehr mit dir.",
"You unshared address book {addressbook} from {user}" : "Du teilst das Adressbuch {addressbook} nicht mehr mit {user}",
"{actor} unshared address book {addressbook} from {user}" : "{actor} teilt das Adressbuch {addressbook} nicht mehr mit {user}",
- "{actor} unshared address book {addressbook} from themselves" : "{actor} teilt das Adressbuch {addressbook} nicht mehr mit Dir",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} teilt das Adressbuch {addressbook} nicht mehr mit dir.",
"You shared address book {addressbook} with group {group}" : "Du hast das Adressbuch {addressbook} mit der Gruppe {group} geteilt",
"{actor} shared address book {addressbook} with group {group}" : "{actor} hat das Adressbuch {addressbook} mit der Gruppe {group} geteilt",
"You unshared address book {addressbook} from group {group}" : "Du teilst das Adressbuch {addressbook} nicht mehr mit der Gruppe {group}",
@@ -108,7 +220,10 @@ OC.L10N.register(
"{actor} updated contact {card} in address book {addressbook}" : "{actor} hat den Kontakt {card} im Adressbuch {addressbook} aktualisiert",
"You updated contact {card} in address book {addressbook}" : "Du hast den Kontakt {card} im Adressbuch {addressbook} aktualisiert",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "Ein <strong>Kontakt</strong> oder ein <strong>Adressbuch</strong> wurde geändert",
+ "Accounts" : "Konten",
+ "System address book which holds all accounts" : "Systemadressbuch, das alle Konten enthält",
"File is not updatable: %1$s" : "Datei kann nicht aktualisiert werden: %1$s",
+ "Failed to get storage for file" : "Speicherplatz für Datei konnte nicht abgerufen werden",
"Could not write to final file, canceled by hook" : "In die endgültige Datei konnte nicht geschrieben werden, wurde durch einen Hook abgebrochen",
"Could not write file contents" : "Dateiinhalt konnte nicht geschrieben werden",
"_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
@@ -117,64 +232,107 @@ OC.L10N.register(
"Could not rename part file to final file, canceled by hook" : "Konnte temporäre Datei nicht in die endgültige Datei umbenennen, wurde durch einen Hook abgebrochen",
"Could not rename part file to final file" : "Konnte temporäre Datei nicht in die endgültige Datei umbenennen",
"Failed to check file size: %1$s" : "Dateigröße konnte nicht überprüft werden: %1$s",
- "Could not open file" : "Datei konnte nicht geöffnet werden",
+ "Could not open file: %1$s, file does seem to exist" : "Datei konnte nicht geöffnet werden: %1$s, Datei scheint aber zu existieren.",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Datei konnte nicht geöffnet werden: %1$s, Datei scheint nicht zu existieren.",
"Encryption not ready: %1$s" : "Verschlüsselung nicht bereit: %1$s",
"Failed to open file: %1$s" : "Datei konnte nicht geöffnet werden: %1$s",
"Failed to unlink: %1$s" : "Fehler beim Aufheben der Verknüpfung: %1$s",
- "Invalid chunk name" : "Ungültiger Chunk-Name",
- "Could not rename part file assembled from chunks" : "Aus Chunks zusammengesetzte temporäre Datei konnte nicht umbenannt werden",
"Failed to write file contents: %1$s" : "Fehler beim Schreiben des Dateiinhaltes: %1$s",
"File not found: %1$s" : "Datei nicht gefunden: %1$s",
+ "Invalid target path" : "Ungültiger Ziel-Pfad",
"System is in maintenance mode." : "Das System befindet sich im Wartungsmodus.",
"Upgrade needed" : "Aktualisierung erforderlich",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Dein %s muss konfiguriert werden, um HTTPS zusammen mit CalDAV und CardDAV unter iOS/macOS nutzen zu können.",
"Configures a CalDAV account" : "Ein CalDAV-Konto einrichten",
"Configures a CardDAV account" : "Ein CardDAV-Konto einrichten",
"Events" : "Ereignisse",
- "Tasks" : "Aufgaben",
"Untitled task" : "Unbenannte Aufgabe",
"Completed on %s" : "Erledigt am %s",
"Due on %s by %s" : "Fällig am %s von %s",
"Due on %s" : "Fällig am %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Willkommen bei Nextcloud Calendar!\n\nDies ist ein Beispielereignis – entdecke die Flexibilität der Planung mit Nextcloud Calendar und nimm beliebige Änderungen vor!\n\nMit Nextcloud Calendar kannst du:\n– Ereignisse mühelos erstellen, bearbeiten und verwalten.\n– Mehrere Kalender erstellen und mit Teamkollegen, Freunden oder der Familie teilen.\n– Verfügbarkeit prüfen und Ihre Termine anderen anzeigen.\n– Nahtlose Integration mit Apps und Geräten über CalDAV.\n– Individuelle Gestaltung: Plane wiederkehrende Ereignisse, passe Benachrichtigungen und andere Einstellungen an.",
+ "Example event - open me!" : "Beispielereignis – öffne mich!",
+ "System Address Book" : "Systemadressbuch",
+ "The system address book contains contact information for all users in your instance." : "Das Systemadressbuch enthält Kontaktinformationen für alle Benutzer in dieser Instanz.",
+ "Enable System Address Book" : "Systemadressbuch aktivieren",
+ "DAV system address book" : "DAV-Systemadressbuch",
+ "No outstanding DAV system address book sync." : "Keine ausstehende Synchronisierung des DAV-Systemadressbuchs",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Die Synchronisierung des DAV-Systemadressbuchs wurde noch nicht ausgeführt, da diese Instanz mehr als 1000 Benutzer hat oder weil ein Fehler aufgetreten ist. Bitte manuell ausführen, mittels \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "WebDAV-Endpunkt",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Es konnte nicht überprüft werden, ob der Webserver ordnungsgemäß eingerichtet ist, um Dateisynchronisation über WebDAV zu ermöglichen. Bitte dies manuell überprüfen.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Der Webserver ist noch nicht hinreichend für Datei-Synchronisierung konfiguriert, da die WebDAV-Schnittstelle vermutlich nicht funktioniert.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Der Webserver ist ordnungsgemäß eingerichtet um Dateisynchronisation über WebDAV zu ermöglichen.",
"Migrated calendar (%1$s)" : "Migrierter Kalender (%1$s)",
"Calendars including events, details and attendees" : "Kalender mit Terminen, Details und Teilnehmern",
"Contacts and groups" : "Kontakte und Gruppen",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV-Endpunkt",
- "Availability" : "Verfügbarkeit",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Wenn Du Deine Arbeitszeiten konfigurierst, können andere Benutzer sehen, wann Du nicht im Büro bist, wenn sie eine Besprechung buchen.",
+ "Absence saved" : "Abwesenheit gespeichert",
+ "Failed to save your absence settings" : "Deine Abwesenheitseinstellungen konnten nicht gespeichert werden.",
+ "Absence cleared" : "Abwesenheit gelöscht",
+ "Failed to clear your absence settings" : "Deine Abwesenheitseinstellungen konnten nicht gelöscht werden.",
+ "First day" : "Erster Tag",
+ "Last day (inclusive)" : "Letzter Tag (inklusiv)",
+ "Out of office replacement (optional)" : "Abwesenheitsvertretung (optional)",
+ "Name of the replacement" : "Name der Vertretung",
+ "No results." : "Keine Ergebnisse",
+ "Start typing." : "Mit dem Schreiben beginnen.",
+ "Short absence status" : "Kurzer Abwesenheitsstatus",
+ "Long absence Message" : "Lange Abwesenheitsnachricht",
+ "Save" : "Speichern",
+ "Disable absence" : "Abwesenheit deaktivieren",
+ "Failed to load availability" : "Verfügbarkeit konnte nicht geladen werden",
+ "Saved availability" : "Verfügbarkeit gespeichert",
+ "Failed to save availability" : "Verfügbarkeit konnte nicht gespeichert werden",
"Time zone:" : "Zeitzone:",
- "to" : "an",
+ "to" : "bis",
"Delete slot" : "Slot löschen",
"No working hours set" : "Keine Arbeitszeiten konfiguriert",
"Add slot" : "Slot hinzufügen",
- "Monday" : "Montag",
- "Tuesday" : "Dienstag",
- "Wednesday" : "Mittwoch",
- "Thursday" : "Donnerstag",
- "Friday" : "Freitag",
- "Saturday" : "Samstag",
- "Sunday" : "Sonntag",
- "Save" : "Speichern",
+ "Weekdays" : "Wochentage",
+ "Pick a start time for {dayName}" : "Eine Startzeit für {dayName} wählen",
+ "Pick a end time for {dayName}" : "Eine Endezeit für {dayName} wählen",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Setze den Benutzerstatus außerhalb deiner Verfügbarkeit automatisch auf \"Nicht stören\", um alle Benachrichtigungen stumm zu schalten.",
+ "Cancel" : "Abbrechen",
+ "Import" : "Importieren",
+ "Error while saving settings" : "Fehler beim Speichern der Einstellungen",
+ "Contact reset successfully" : "Kontakt zurückgesetzt",
+ "Error while resetting contact" : "Fehler beim Zurücksetzen des Kontakts",
+ "Contact imported successfully" : "Kontakt importiert",
+ "Error while importing contact" : "Fehler beim Import des Kontakts",
+ "Import contact" : "Kontakt importieren",
+ "Reset to default" : "Auf Standard zurücksetzen ",
+ "Import contacts" : "Kontakte importieren",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Durch das Importieren einer neuen VCF-Datei wird der vorhandene Standardkontakt gelöscht und durch den neuen ersetzt. Fortsetzen?",
+ "Failed to save example event creation setting" : "Einstellung für die Beispiels-Ereigniserstellung konnte nicht gespeichert werden",
+ "Failed to upload the example event" : "Das Beispielsereignis konnte nicht hochgeladen werden",
+ "Custom example event was saved successfully" : "Benutzerdefiniertes Beispielereignis gespeichert",
+ "Failed to delete the custom example event" : "Benutzerdefiniertes Beispielsereignis konnte nicht gelöscht werden",
+ "Custom example event was deleted successfully" : "Benutzerdefiniertes Beispielsereignis wurde gelöscht",
+ "Import calendar event" : "Kalenderereignis importieren",
+ "Uploading a new event will overwrite the existing one." : "Das Hochladen eines neuen Ereignisses wird das bestehende Ereignis überschreiben.",
+ "Upload event" : "Ereignis hochladen",
+ "Availability" : "Verfügbarkeit",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Wenn du deine Arbeitszeiten angibst, können andere beim Buchen einer Besprechung sehen, wann du nicht im Büro bist.",
+ "Absence" : "Abwesenheit",
+ "Configure your next absence period." : "Richte deinen nächsten Abwesenheitszeitraum ein.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installiere außerdem die {calendarappstoreopen}Kalender-App{linkclose} oder {calendardocopen}verbinde deinen Desktop & Mobilgerät zur Synchronisierung ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Bitte stelle sicher, dass du {emailopen}den E-Mail Server{linkclose} ordnungsgemäß einrichtest.",
"Calendar server" : "Kalender-Server",
- "Send invitations to attendees" : "Einladungen an die Teilnehmer versenden",
+ "Send invitations to attendees" : "Einladungen an die Teilnehmer senden",
"Automatically generate a birthday calendar" : "Automatisch einen Kalender für Geburtstage erstellen",
- "Birthday calendars will be generated by a background job." : "Kalender für Geburtstage werden von einem Hintergrund-Auftrag erstellt",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Die Einträge werden nicht sofort angezeigt. Nach der Aktivierung wird es ein wenig dauern bis zur Anzeige.",
+ "Birthday calendars will be generated by a background job." : "Kalender für Geburtstage werden von einer Hintergrundaufgabe erstellt",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "Die Einträge werden nicht sofort angezeigt. Nach der Aktivierung wird es bis zur Anzeige ein wenig dauern.",
"Send notifications for events" : "Sende Benachrichtigungen für Termine",
- "Notifications are sent via background jobs, so these must occur often enough." : "Benachrichtigungen werden von Hintergrundjobs versendet, so dass diese häufig genug ausgeführt werden müssen.",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Benachrichtigungen werden von Hintergrundaufgaben gesendet, so dass diese häufig genug ausgeführt werden.",
"Send reminder notifications to calendar sharees as well" : "Erinnerungsbenachrichtigungen auch an die freigegebenen Kalender senden",
"Reminders are always sent to organizers and attendees." : "Erinnerungen werden immer an Organisatoren und Teilnehmer gesendet.",
"Enable notifications for events via push" : "Benachrichtigungen für Termine per Push aktivieren",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installiere außerdem die {calendarappstoreopen}Kalender-App{linkclose} oder {calendardocopen}verbinde Deinen Desktop & Mobilgerät zur Synchronisierung ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Bitte stelle sicher, dass Du {emailopen}den E-Mail Server{linkclose} ordnungsgemäß einrichtest.",
- "There was an error updating your attendance status." : "Es ist ein Fehler beim Aktualisieren Deines Teilnehmerstatus aufgetreten.",
+ "Example content" : "Beispielsinhalt",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Beispielinhalte dienen dazu, die Funktionen von Nextcloud vorzustellen. Standardinhalte werden mit Nextcloud ausgeliefert und können durch benutzerdefinierte Inhalte ersetzt werden.",
+ "There was an error updating your attendance status." : "Es ist ein Fehler beim Aktualisieren deines Teilnehmerstatus aufgetreten.",
"Please contact the organizer directly." : "Bitte den Organisator direkt kontaktieren.",
"Are you accepting the invitation?" : "Die Einladung annehmen?",
"Tentative" : "Vorläufig",
- "Number of guests" : "Anzahl Gäste",
- "Comment" : "Kommentar",
- "Your attendance was updated successfully." : "Dein Teilnehmerstatus wurde aktualisiert.",
- "Calendar and tasks" : "Kalender und Aufgaben"
+ "Your attendance was updated successfully." : "Dein Teilnehmerstatus wurde aktualisiert."
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/de.json b/apps/dav/l10n/de.json
index 9f3f97e0c81..b79cae18699 100644
--- a/apps/dav/l10n/de.json
+++ b/apps/dav/l10n/de.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Kalender",
- "Todos" : "Aufgaben",
+ "Tasks" : "Aufgaben",
"Personal" : "Persönlich",
"{actor} created calendar {calendar}" : "{actor} hat den Kalender {calendar} erstellt",
"You created calendar {calendar}" : "Du hast den Kalender {calendar} erstellt",
@@ -12,10 +12,10 @@
"You restored calendar {calendar}" : "Du hast den Kalender {calendar} wiederhergestellt",
"You shared calendar {calendar} as public link" : "Du hast den Kalender {calendar} als öffentlichen Link geteilt",
"You removed public link for calendar {calendar}" : "Du hast den öffentlichen Link für Kalender {calendar} entfernt",
- "{actor} shared calendar {calendar} with you" : "{actor} hat den Kalender {calendar} mit Dir geteilt",
+ "{actor} shared calendar {calendar} with you" : "{actor} hat den Kalender {calendar} mit dir geteilt",
"You shared calendar {calendar} with {user}" : "Du hast den Kalender {calendar} mit {user} geteilt",
"{actor} shared calendar {calendar} with {user}" : "{actor} hat den Kalender {calendar} mit {user} geteilt",
- "{actor} unshared calendar {calendar} from you" : "{actor} teilt den Kalender {calendar} nicht mehr mit Dir",
+ "{actor} unshared calendar {calendar} from you" : "{actor} teilt den Kalender {calendar} nicht mehr mit dir",
"You unshared calendar {calendar} from {user}" : "Du teilst den Kalender {calendar} nicht mehr mit {user}",
"{actor} unshared calendar {calendar} from {user}" : "{actor} teilt den Kalender {calendar} nicht mehr mit {user}",
"{actor} unshared calendar {calendar} from themselves" : "{actor} teilt den Kalender {calendar} nicht mehr mit sich selbst",
@@ -23,36 +23,41 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} teilt den Kalender {calendar} mit der Gruppe {group}",
"You unshared calendar {calendar} from group {group}" : "Du teilst den Kalender {calendar} nicht mehr mit der Gruppe {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} teilt den Kalender {calendar} nicht mehr mit der Gruppe {group}",
+ "Untitled event" : "Unbenannter Termin",
"{actor} created event {event} in calendar {calendar}" : "{actor} hat den Termin {event} im Kalender {calendar} erstellt",
"You created event {event} in calendar {calendar}" : "Du hast den Termin {event} im Kalender {calendar} erstellt",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} hat den Termin {event} im Kalender {calendar} gelöscht",
"You deleted event {event} from calendar {calendar}" : "Du hast den Termin {event} im Kalender {calendar} gelöscht",
"{actor} updated event {event} in calendar {calendar}" : "{actor} hat den Termin {event} im Kalender {calendar} aktualisiert",
"You updated event {event} in calendar {calendar}" : "Du hast den Termin {event} im Kalender {calendar} aktualisiert",
- "{actor} restored event {event} of calendar {calendar}" : "{actor} hat das Adressbuch {addressbook} mit Dir geteilt",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} hat das Ereignis {event} vom Kalender {sourceCalendar} in den Kalender {targetCalendar} verschoben",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Du hast das Ereignis {event} vom Kalender {sourceCalendar} in den Kalender {targetCalendar} verschoben",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} hat den Termin {event} in Kalender {calendar} wiederhergestellt",
"You restored event {event} of calendar {calendar}" : "Du hast den Termin {event} im Kalender {calendar} wiederhergestellt",
"Busy" : "Beschäftigt",
- "{actor} created todo {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} erstellt",
- "You created todo {todo} in list {calendar}" : "Du hast die Aufgabe {todo} in der Liste {calendar} erstellt",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} gelöscht",
- "You deleted todo {todo} from list {calendar}" : "Du hast die Aufgabe {todo} in der Liste {calendar} gelöscht",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} aktualisiert",
- "You updated todo {todo} in list {calendar}" : "Du hast die Aufgabe {todo} in der Liste {calendar} aktualisiert",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} erledigt",
- "You solved todo {todo} in list {calendar}" : "Du hast die Aufgabe {todo} in der Liste {calendar} erledigt",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} wiedereröffnet",
- "You reopened todo {todo} in list {calendar}" : "Du hast die Aufgabe {todo} in der Liste {calendar} wiedereröffnet",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} erstellt",
+ "You created to-do {todo} in list {calendar}" : "Du hast eine Aufgabe {todo} in der Liste {calendar} erstellt",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} hat die Aufgabe {todo} aus der Liste {calendar} gelöscht",
+ "You deleted to-do {todo} from list {calendar}" : "Du hast die Aufgabe {todo} aus der Liste {calendar} gelöscht",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} aktualisiert",
+ "You updated to-do {todo} in list {calendar}" : "Du hast die Aufgabe {todo} in der Liste {calendar} aktualisiert",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} erledigt",
+ "You solved to-do {todo} in list {calendar}" : "Du hast die Aufgabe {todo} in der Liste {calendar} erledigt",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} wiedereröffnet",
+ "You reopened to-do {todo} in list {calendar}" : "Du hast die Aufgabe {todo} in der Liste {calendar} wiedereröffnet",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} hat die Aufgabe {todo} von der Liste {sourceCalendar} in die Liste {targetCalendar} verschoben",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Du hast die Aufgabe {todo} von der Liste {sourceCalendar} in die Liste {targetCalendar} verschoben",
"Calendar, contacts and tasks" : "Kalender, Kontakte und Aufgaben",
"A <strong>calendar</strong> was modified" : "Ein <strong>Kalender</strong> wurde bearbeitet",
"A calendar <strong>event</strong> was modified" : "Ein Kalender-<strong>Termin</strong> wurde bearbeitet",
- "A calendar <strong>todo</strong> was modified" : "Eine Kalender-<strong>Aufgabe</strong> wurde bearbeitet",
+ "A calendar <strong>to-do</strong> was modified" : "Eine Kalender-<strong>Aufgabe</strong> wurde geändert",
"Contact birthdays" : "Geburtstage von Kontakten",
"Death of %s" : "Todestag von %s",
+ "Untitled calendar" : "Unbenannter Kalender",
"Calendar:" : "Kalender:",
"Date:" : "Datum:",
"Where:" : "Wo:",
"Description:" : "Beschreibung:",
- "Untitled event" : "Termin ohne Titel",
"_%n year_::_%n years_" : ["%n Jahr","%n Jahre"],
"_%n month_::_%n months_" : ["%n Monat","%n Monate"],
"_%n day_::_%n days_" : ["%n Tag","%n Tage"],
@@ -65,22 +70,129 @@
"Description: %s" : "Beschreibung: %s",
"Where: %s" : "Ort: %s",
"%1$s via %2$s" : "%1$s über %2$s",
+ "In the past on %1$s for the entire day" : "In der Vergangenheit am %1$s für den ganzen Tag",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["In einer Minute um %1$s für den ganzen Tag","In %n Minuten um %1$s für den ganzen Tag"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["In einer Stunde um %1$s für den ganzen Tag","In %n Stunden um %1$s für den ganzen Tag"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["In einem Tag am %1$s für den ganzen Tag","In %n Tagen am %1$s für den ganzen Tag"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["In einer Woche am %1$s den ganzen Tag","In %n Wochen am %1$s den ganzen Tag"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["In einem Monat am %1$s den ganzen Tag","In %n Monaten am %1$s den ganzen Tag"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["In einem Jahr am %1$s für den ganzen Tag","In %n Jahren am %1$s für den ganzen Tag"],
+ "In the past on %1$s between %2$s - %3$s" : "In der Vergangenheit am %1$s zwischen %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["In einer Minute am%1$s zwischen %2$s - %3$s","In %n Minuten am %1$s zwischen %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["In einer Stunde am %1$s zwischen %2$s - %3$s","In %n Stunden am%1$s zwischen %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["In einem Tag am %1$s zwischen %2$s - %3$s","In %n Tagen am %1$szwischen %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["In einer Woche am %1$s zwischen%2$s - %3$s","In %n Wochen am%1$s zwischen %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["In einem Monat am %1$s zwischen %2$s - %3$s","In %n Monaten am %1$s zwischen %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["In einem Jahr am %1$s zwischen %2$s - %3$s","In %n Jahren am%1$s zwischen %2$s - %3$s"],
+ "Could not generate when statement" : "Wann-Angabe konnte nicht erzeugt werden.",
+ "Every Day for the entire day" : "Jeden Tag für den ganzen Tag",
+ "Every Day for the entire day until %1$s" : "Jeden Tag für den ganzen Tag bis %1$s",
+ "Every Day between %1$s - %2$s" : "Jeden Tag zwischen %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Jeden Tag zwischen %1$s - %2$s bis %3$s",
+ "Every %1$d Days for the entire day" : "Alle %1$d Tage für den ganzen Tag",
+ "Every %1$d Days for the entire day until %2$s" : "Alle %1$d Tage für den ganzen Tag bis %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Alle %1$d Tage zwischen %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Alle %1$d Tage zwischen %2$s - %3$s bis %4$s",
+ "Could not generate event recurrence statement" : "Angabe für Terminwiederholung konnte nicht erzeugt werden.",
+ "Every Week on %1$s for the entire day" : "Jede Woche am %1$s für den ganzen Tag",
+ "Every Week on %1$s for the entire day until %2$s" : "Jede Woche am %1$s für den ganzen Tag bis %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Jede Woche am %1$s zwischen %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Jede Woche am %1$s zwischen %2$s - %3$s bis %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Alle %1$d Wochen am %2$s für den ganzen Tag",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Alle %1$d Wochen am %2$s für den ganzen Tag bis %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Alle %1$d Wochen am %2$s zwischen %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Alle %1$d Wochen am %2$s zwischen %3$s - %4$s bis %5$s",
+ "Every Month on the %1$s for the entire day" : "Jeden Monat am %1$s für den ganzen Tag",
+ "Every Month on the %1$s for the entire day until %2$s" : "Jeden Monat am %1$s für den ganzen Tag bis %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Jeden Monat am %1$s zwischen %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Jeden Monat am %1$s zwischen %2$s - %3$s bis %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Alle %1$d Monate am %2$s für den ganzen Tag",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Alle %1$d Monate am %2$s für den ganzen Tag bis %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Alle %1$d Monate am %2$s zwischen %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Alle %1$d Monate am %2$s zwischen %3$s - %4$s bis %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Jedes Jahr im %1$s am %2$s für den ganzen Tag",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Jedes Jahr im %1$s am %2$s für den ganzen Tag bis %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Jedes Jahr im %1$s am %2$s zwischen %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Jedes Jahr im %1$s am %2$s zwischen %3$s - %4$s bis %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Alle %1$d Jahre im %2$s am %3$s für den ganzen Tag",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Alle %1$d Jahre im %2$s am %3$s für den ganzen Tag bis %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Alle %1$d Jahre im %2$s am %3$s zwischen %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Alle %1$d Jahre im %2$s am %3$s zwischen %4$s - %5$s bis %6$s",
+ "On specific dates for the entire day until %1$s" : "An bestimmten Tagen für den ganzen Tag bis %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "An bestimmten Tagen zwischen %1$s - %2$s bis %3$s",
+ "In the past on %1$s" : "In der Vergangenheit am %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["In einer Minute am %1$s","In %n Minuten am %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["In einer Stunde am %1$s","In %n Stunden am %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["In einem Tag am %1$s","In %n Tagen am %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["In einer Woche am %1$s","In %n Wochen am %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["In einem Monat am %1$s","In %n Monaten am %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["In einem Jahr am %1$s","In %n Jahren am %1$s"],
+ "In the past on %1$s then on %2$s" : "In der Vergangenheit am %1$s danach am %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["In einer Minute am %1$s danach am %2$s","In %n Minuten am %1$s danach am %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["In einer Stunde am %1$s danach am %2$s","In %n Stunden am %1$s danach am %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["In einem Tag am %1$s danach am %2$s","In %n Tagen am %1$s danach am %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["In einer Woche am %1$s danach am %2$s","In %n Wochen am %1$s danach am %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["In einem Monat am %1$s danach am %2$s","In %n Monaten am %1$s danach am %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["In einem Jahr am %1$s danach am %2$s","In %n Jahren am %1$s danach am %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "In der Vergangenheit am %1$s danach am %2$s und %3$s",
+ "_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_" : ["In einer Minute am %1$s danach am %2$s und %3$s","In %n Minuten am %1$s danach am %2$s und %3$s"],
+ "_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_" : ["In einer Stunde am %1$s danach am %2$s und %3$s","In %n Stunden am %1$s danach am %2$s und %3$s"],
+ "_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_" : ["In einem Tag am %1$s danach am %2$s und %3$s","In %n Tagen am %1$s danach am %2$s und %3$s"],
+ "_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_" : ["In einer Woche am %1$s danach am %2$s und %3$s","In %n Wochen am %1$s danach am %2$s und %3$s"],
+ "_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_" : ["In einem Monat am %1$s danach am %2$s und %3$s","In %n Monaten am %1$s danach am %2$s und %3$s"],
+ "_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_" : ["In einem Jahr am %1$s danach am %2$s und %3$s","In %n Jahren am %1$s danach am %2$s und %3$s"],
+ "Could not generate next recurrence statement" : "Angabe für nächste Wiederholung konnte nicht erzeugt werden.",
"Cancelled: %1$s" : "Abgesagt: %1$s",
- "Invitation canceled" : "Einladung abgebrochen",
+ "\"%1$s\" has been canceled" : "\"%1$s\" wurde abgesagt.",
"Re: %1$s" : "Re: %1$s",
- "Invitation updated" : "Einladung aktualisiert",
+ "%1$s has accepted your invitation" : "%1$s hat deine Einladung angenommen.",
+ "%1$s has tentatively accepted your invitation" : "%1$s hat deine Einladung vorläufig angenommen.",
+ "%1$s has declined your invitation" : "%1$s hat deine Einladung abgelehnt.",
+ "%1$s has responded to your invitation" : "%1$s hat auf deine Einladung geantwortet.",
+ "Invitation updated: %1$s" : "Einladung aktualisiert: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s hat die Veranstaltung \"%2$s\" aktualisiert",
"Invitation: %1$s" : "Einladung: %1$s",
- "Invitation" : "Einladung",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s möchte dich zu \"%2$s\" einladen.",
+ "Organizer:" : "Organisator:",
+ "Attendees:" : "Teilnehmer:",
"Title:" : "Titel:",
- "Time:" : "Zeit:",
+ "When:" : "Wann:",
"Location:" : "Ort:",
"Link:" : "Link:",
- "Organizer:" : "Organisator:",
- "Attendees:" : "Teilnehmer:",
+ "Occurring:" : "Findet statt:",
"Accept" : "Akzeptieren",
"Decline" : "Ablehnen",
"More options …" : "Weitere Optionen …",
"More options at %s" : "Weitere Optionen unter %s",
+ "Monday" : "Montag",
+ "Tuesday" : "Dienstag",
+ "Wednesday" : "Mittwoch",
+ "Thursday" : "Donnerstag",
+ "Friday" : "Freitag",
+ "Saturday" : "Samstag",
+ "Sunday" : "Sonntag",
+ "January" : "Januar",
+ "February" : "Februar",
+ "March" : "März",
+ "April" : "April",
+ "May" : "Mai",
+ "June" : "Juni",
+ "July" : "Juli",
+ "August" : "August",
+ "September" : "September",
+ "October" : "Oktober",
+ "November" : "November",
+ "December" : "Dezember",
+ "First" : "Erste",
+ "Second" : "Zweite",
+ "Third" : "Dritte",
+ "Fourth" : "Vierte",
+ "Fifth" : "Fünften",
+ "Last" : "Letzte",
+ "Second Last" : "Vorletztes",
+ "Third Last" : "Drittletztes",
+ "Fourth Last" : "Viertletztes",
+ "Fifth Last" : "Fünftletztes",
"Contacts" : "Kontakte",
"{actor} created address book {addressbook}" : "{actor} hat das Adressbuch {addressbook} erstellt",
"You created address book {addressbook}" : "Du hast das Adressbuch {addressbook} erstellt",
@@ -88,13 +200,13 @@
"You deleted address book {addressbook}" : "Du hast das Adressbuch {addressbook} gelöscht",
"{actor} updated address book {addressbook}" : "{actor} hat das Adressbuch {addressbook} aktualisiert",
"You updated address book {addressbook}" : "Du hast das Adressbuch {addressbook} aktualisiert",
- "{actor} shared address book {addressbook} with you" : "{actor} hat das Adressbuch {addressbook} mit Dir geteilt",
+ "{actor} shared address book {addressbook} with you" : "{actor} hat das Adressbuch {addressbook} mit dir geteilt",
"You shared address book {addressbook} with {user}" : "Du hast das Adressbuch {addressbook} geteilt",
"{actor} shared address book {addressbook} with {user}" : "{actor} hat das Adressbuch {addressbook} mit {user} geteilt",
- "{actor} unshared address book {addressbook} from you" : "{actor} teilt das Adressbuch {addressbook} nicht mehr mit Dir",
+ "{actor} unshared address book {addressbook} from you" : "{actor} teilt das Adressbuch {addressbook} nicht mehr mit dir.",
"You unshared address book {addressbook} from {user}" : "Du teilst das Adressbuch {addressbook} nicht mehr mit {user}",
"{actor} unshared address book {addressbook} from {user}" : "{actor} teilt das Adressbuch {addressbook} nicht mehr mit {user}",
- "{actor} unshared address book {addressbook} from themselves" : "{actor} teilt das Adressbuch {addressbook} nicht mehr mit Dir",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} teilt das Adressbuch {addressbook} nicht mehr mit dir.",
"You shared address book {addressbook} with group {group}" : "Du hast das Adressbuch {addressbook} mit der Gruppe {group} geteilt",
"{actor} shared address book {addressbook} with group {group}" : "{actor} hat das Adressbuch {addressbook} mit der Gruppe {group} geteilt",
"You unshared address book {addressbook} from group {group}" : "Du teilst das Adressbuch {addressbook} nicht mehr mit der Gruppe {group}",
@@ -106,7 +218,10 @@
"{actor} updated contact {card} in address book {addressbook}" : "{actor} hat den Kontakt {card} im Adressbuch {addressbook} aktualisiert",
"You updated contact {card} in address book {addressbook}" : "Du hast den Kontakt {card} im Adressbuch {addressbook} aktualisiert",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "Ein <strong>Kontakt</strong> oder ein <strong>Adressbuch</strong> wurde geändert",
+ "Accounts" : "Konten",
+ "System address book which holds all accounts" : "Systemadressbuch, das alle Konten enthält",
"File is not updatable: %1$s" : "Datei kann nicht aktualisiert werden: %1$s",
+ "Failed to get storage for file" : "Speicherplatz für Datei konnte nicht abgerufen werden",
"Could not write to final file, canceled by hook" : "In die endgültige Datei konnte nicht geschrieben werden, wurde durch einen Hook abgebrochen",
"Could not write file contents" : "Dateiinhalt konnte nicht geschrieben werden",
"_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
@@ -115,64 +230,107 @@
"Could not rename part file to final file, canceled by hook" : "Konnte temporäre Datei nicht in die endgültige Datei umbenennen, wurde durch einen Hook abgebrochen",
"Could not rename part file to final file" : "Konnte temporäre Datei nicht in die endgültige Datei umbenennen",
"Failed to check file size: %1$s" : "Dateigröße konnte nicht überprüft werden: %1$s",
- "Could not open file" : "Datei konnte nicht geöffnet werden",
+ "Could not open file: %1$s, file does seem to exist" : "Datei konnte nicht geöffnet werden: %1$s, Datei scheint aber zu existieren.",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Datei konnte nicht geöffnet werden: %1$s, Datei scheint nicht zu existieren.",
"Encryption not ready: %1$s" : "Verschlüsselung nicht bereit: %1$s",
"Failed to open file: %1$s" : "Datei konnte nicht geöffnet werden: %1$s",
"Failed to unlink: %1$s" : "Fehler beim Aufheben der Verknüpfung: %1$s",
- "Invalid chunk name" : "Ungültiger Chunk-Name",
- "Could not rename part file assembled from chunks" : "Aus Chunks zusammengesetzte temporäre Datei konnte nicht umbenannt werden",
"Failed to write file contents: %1$s" : "Fehler beim Schreiben des Dateiinhaltes: %1$s",
"File not found: %1$s" : "Datei nicht gefunden: %1$s",
+ "Invalid target path" : "Ungültiger Ziel-Pfad",
"System is in maintenance mode." : "Das System befindet sich im Wartungsmodus.",
"Upgrade needed" : "Aktualisierung erforderlich",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Dein %s muss konfiguriert werden, um HTTPS zusammen mit CalDAV und CardDAV unter iOS/macOS nutzen zu können.",
"Configures a CalDAV account" : "Ein CalDAV-Konto einrichten",
"Configures a CardDAV account" : "Ein CardDAV-Konto einrichten",
"Events" : "Ereignisse",
- "Tasks" : "Aufgaben",
"Untitled task" : "Unbenannte Aufgabe",
"Completed on %s" : "Erledigt am %s",
"Due on %s by %s" : "Fällig am %s von %s",
"Due on %s" : "Fällig am %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Willkommen bei Nextcloud Calendar!\n\nDies ist ein Beispielereignis – entdecke die Flexibilität der Planung mit Nextcloud Calendar und nimm beliebige Änderungen vor!\n\nMit Nextcloud Calendar kannst du:\n– Ereignisse mühelos erstellen, bearbeiten und verwalten.\n– Mehrere Kalender erstellen und mit Teamkollegen, Freunden oder der Familie teilen.\n– Verfügbarkeit prüfen und Ihre Termine anderen anzeigen.\n– Nahtlose Integration mit Apps und Geräten über CalDAV.\n– Individuelle Gestaltung: Plane wiederkehrende Ereignisse, passe Benachrichtigungen und andere Einstellungen an.",
+ "Example event - open me!" : "Beispielereignis – öffne mich!",
+ "System Address Book" : "Systemadressbuch",
+ "The system address book contains contact information for all users in your instance." : "Das Systemadressbuch enthält Kontaktinformationen für alle Benutzer in dieser Instanz.",
+ "Enable System Address Book" : "Systemadressbuch aktivieren",
+ "DAV system address book" : "DAV-Systemadressbuch",
+ "No outstanding DAV system address book sync." : "Keine ausstehende Synchronisierung des DAV-Systemadressbuchs",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Die Synchronisierung des DAV-Systemadressbuchs wurde noch nicht ausgeführt, da diese Instanz mehr als 1000 Benutzer hat oder weil ein Fehler aufgetreten ist. Bitte manuell ausführen, mittels \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "WebDAV-Endpunkt",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Es konnte nicht überprüft werden, ob der Webserver ordnungsgemäß eingerichtet ist, um Dateisynchronisation über WebDAV zu ermöglichen. Bitte dies manuell überprüfen.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Der Webserver ist noch nicht hinreichend für Datei-Synchronisierung konfiguriert, da die WebDAV-Schnittstelle vermutlich nicht funktioniert.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Der Webserver ist ordnungsgemäß eingerichtet um Dateisynchronisation über WebDAV zu ermöglichen.",
"Migrated calendar (%1$s)" : "Migrierter Kalender (%1$s)",
"Calendars including events, details and attendees" : "Kalender mit Terminen, Details und Teilnehmern",
"Contacts and groups" : "Kontakte und Gruppen",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV-Endpunkt",
- "Availability" : "Verfügbarkeit",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Wenn Du Deine Arbeitszeiten konfigurierst, können andere Benutzer sehen, wann Du nicht im Büro bist, wenn sie eine Besprechung buchen.",
+ "Absence saved" : "Abwesenheit gespeichert",
+ "Failed to save your absence settings" : "Deine Abwesenheitseinstellungen konnten nicht gespeichert werden.",
+ "Absence cleared" : "Abwesenheit gelöscht",
+ "Failed to clear your absence settings" : "Deine Abwesenheitseinstellungen konnten nicht gelöscht werden.",
+ "First day" : "Erster Tag",
+ "Last day (inclusive)" : "Letzter Tag (inklusiv)",
+ "Out of office replacement (optional)" : "Abwesenheitsvertretung (optional)",
+ "Name of the replacement" : "Name der Vertretung",
+ "No results." : "Keine Ergebnisse",
+ "Start typing." : "Mit dem Schreiben beginnen.",
+ "Short absence status" : "Kurzer Abwesenheitsstatus",
+ "Long absence Message" : "Lange Abwesenheitsnachricht",
+ "Save" : "Speichern",
+ "Disable absence" : "Abwesenheit deaktivieren",
+ "Failed to load availability" : "Verfügbarkeit konnte nicht geladen werden",
+ "Saved availability" : "Verfügbarkeit gespeichert",
+ "Failed to save availability" : "Verfügbarkeit konnte nicht gespeichert werden",
"Time zone:" : "Zeitzone:",
- "to" : "an",
+ "to" : "bis",
"Delete slot" : "Slot löschen",
"No working hours set" : "Keine Arbeitszeiten konfiguriert",
"Add slot" : "Slot hinzufügen",
- "Monday" : "Montag",
- "Tuesday" : "Dienstag",
- "Wednesday" : "Mittwoch",
- "Thursday" : "Donnerstag",
- "Friday" : "Freitag",
- "Saturday" : "Samstag",
- "Sunday" : "Sonntag",
- "Save" : "Speichern",
+ "Weekdays" : "Wochentage",
+ "Pick a start time for {dayName}" : "Eine Startzeit für {dayName} wählen",
+ "Pick a end time for {dayName}" : "Eine Endezeit für {dayName} wählen",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Setze den Benutzerstatus außerhalb deiner Verfügbarkeit automatisch auf \"Nicht stören\", um alle Benachrichtigungen stumm zu schalten.",
+ "Cancel" : "Abbrechen",
+ "Import" : "Importieren",
+ "Error while saving settings" : "Fehler beim Speichern der Einstellungen",
+ "Contact reset successfully" : "Kontakt zurückgesetzt",
+ "Error while resetting contact" : "Fehler beim Zurücksetzen des Kontakts",
+ "Contact imported successfully" : "Kontakt importiert",
+ "Error while importing contact" : "Fehler beim Import des Kontakts",
+ "Import contact" : "Kontakt importieren",
+ "Reset to default" : "Auf Standard zurücksetzen ",
+ "Import contacts" : "Kontakte importieren",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Durch das Importieren einer neuen VCF-Datei wird der vorhandene Standardkontakt gelöscht und durch den neuen ersetzt. Fortsetzen?",
+ "Failed to save example event creation setting" : "Einstellung für die Beispiels-Ereigniserstellung konnte nicht gespeichert werden",
+ "Failed to upload the example event" : "Das Beispielsereignis konnte nicht hochgeladen werden",
+ "Custom example event was saved successfully" : "Benutzerdefiniertes Beispielereignis gespeichert",
+ "Failed to delete the custom example event" : "Benutzerdefiniertes Beispielsereignis konnte nicht gelöscht werden",
+ "Custom example event was deleted successfully" : "Benutzerdefiniertes Beispielsereignis wurde gelöscht",
+ "Import calendar event" : "Kalenderereignis importieren",
+ "Uploading a new event will overwrite the existing one." : "Das Hochladen eines neuen Ereignisses wird das bestehende Ereignis überschreiben.",
+ "Upload event" : "Ereignis hochladen",
+ "Availability" : "Verfügbarkeit",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Wenn du deine Arbeitszeiten angibst, können andere beim Buchen einer Besprechung sehen, wann du nicht im Büro bist.",
+ "Absence" : "Abwesenheit",
+ "Configure your next absence period." : "Richte deinen nächsten Abwesenheitszeitraum ein.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installiere außerdem die {calendarappstoreopen}Kalender-App{linkclose} oder {calendardocopen}verbinde deinen Desktop & Mobilgerät zur Synchronisierung ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Bitte stelle sicher, dass du {emailopen}den E-Mail Server{linkclose} ordnungsgemäß einrichtest.",
"Calendar server" : "Kalender-Server",
- "Send invitations to attendees" : "Einladungen an die Teilnehmer versenden",
+ "Send invitations to attendees" : "Einladungen an die Teilnehmer senden",
"Automatically generate a birthday calendar" : "Automatisch einen Kalender für Geburtstage erstellen",
- "Birthday calendars will be generated by a background job." : "Kalender für Geburtstage werden von einem Hintergrund-Auftrag erstellt",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Die Einträge werden nicht sofort angezeigt. Nach der Aktivierung wird es ein wenig dauern bis zur Anzeige.",
+ "Birthday calendars will be generated by a background job." : "Kalender für Geburtstage werden von einer Hintergrundaufgabe erstellt",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "Die Einträge werden nicht sofort angezeigt. Nach der Aktivierung wird es bis zur Anzeige ein wenig dauern.",
"Send notifications for events" : "Sende Benachrichtigungen für Termine",
- "Notifications are sent via background jobs, so these must occur often enough." : "Benachrichtigungen werden von Hintergrundjobs versendet, so dass diese häufig genug ausgeführt werden müssen.",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Benachrichtigungen werden von Hintergrundaufgaben gesendet, so dass diese häufig genug ausgeführt werden.",
"Send reminder notifications to calendar sharees as well" : "Erinnerungsbenachrichtigungen auch an die freigegebenen Kalender senden",
"Reminders are always sent to organizers and attendees." : "Erinnerungen werden immer an Organisatoren und Teilnehmer gesendet.",
"Enable notifications for events via push" : "Benachrichtigungen für Termine per Push aktivieren",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installiere außerdem die {calendarappstoreopen}Kalender-App{linkclose} oder {calendardocopen}verbinde Deinen Desktop & Mobilgerät zur Synchronisierung ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Bitte stelle sicher, dass Du {emailopen}den E-Mail Server{linkclose} ordnungsgemäß einrichtest.",
- "There was an error updating your attendance status." : "Es ist ein Fehler beim Aktualisieren Deines Teilnehmerstatus aufgetreten.",
+ "Example content" : "Beispielsinhalt",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Beispielinhalte dienen dazu, die Funktionen von Nextcloud vorzustellen. Standardinhalte werden mit Nextcloud ausgeliefert und können durch benutzerdefinierte Inhalte ersetzt werden.",
+ "There was an error updating your attendance status." : "Es ist ein Fehler beim Aktualisieren deines Teilnehmerstatus aufgetreten.",
"Please contact the organizer directly." : "Bitte den Organisator direkt kontaktieren.",
"Are you accepting the invitation?" : "Die Einladung annehmen?",
"Tentative" : "Vorläufig",
- "Number of guests" : "Anzahl Gäste",
- "Comment" : "Kommentar",
- "Your attendance was updated successfully." : "Dein Teilnehmerstatus wurde aktualisiert.",
- "Calendar and tasks" : "Kalender und Aufgaben"
+ "Your attendance was updated successfully." : "Dein Teilnehmerstatus wurde aktualisiert."
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/dav/l10n/de_DE.js b/apps/dav/l10n/de_DE.js
index 5cee3f3f481..bb622fa32d2 100644
--- a/apps/dav/l10n/de_DE.js
+++ b/apps/dav/l10n/de_DE.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Kalender",
- "Todos" : "Aufgaben",
+ "Tasks" : "Aufgaben",
"Personal" : "Persönlich",
"{actor} created calendar {calendar}" : "{actor} hat den Kalender {calendar} erstellt",
"You created calendar {calendar}" : "Sie haben den Kalender {calendar} erstellt",
@@ -25,36 +25,41 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} teilt den Kalender {calendar} mit der Gruppe {group}",
"You unshared calendar {calendar} from group {group}" : "Sie teilen den Kalender {calendar} nicht mehr mit der Gruppe {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} teilt den Kalender {calendar} nicht mehr mit der Gruppe {group}",
+ "Untitled event" : "Unbenannter Termin",
"{actor} created event {event} in calendar {calendar}" : "{actor} hat den Termin {event} im Kalender {calendar} erstellt",
"You created event {event} in calendar {calendar}" : "Sie haben den Termin {event} im Kalender {calendar} erstellt",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} hat den Termin {event} im Kalender {calendar} gelöscht",
"You deleted event {event} from calendar {calendar}" : "Sie haben den Termin {event} im Kalender {calendar} gelöscht",
"{actor} updated event {event} in calendar {calendar}" : "{actor} hat den Termin {event} im Kalender {calendar} aktualisiert",
"You updated event {event} in calendar {calendar}" : "Sie haben den Termin {event} im Kalender {calendar} aktualisiert",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} hat den Termin {event} vom Kalender {sourceCalendar} in den Kalender {targetCalendar} verschoben",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Sie haben den Termin {event} vom Kalender {sourceCalendar} in den Kalender {targetCalendar} verschoben",
"{actor} restored event {event} of calendar {calendar}" : "{actor} hat den Termin {event} im Kalender {calendar} wiederhergestellt",
"You restored event {event} of calendar {calendar}" : "Sie haben den Termin {event} im Kalender {calendar} wiederhergestellt",
"Busy" : "Beschäftigt",
- "{actor} created todo {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} erstellt",
- "You created todo {todo} in list {calendar}" : "Sie haben die Aufgabe {todo} in der Liste {calendar} erstellt",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} gelöscht",
- "You deleted todo {todo} from list {calendar}" : "Sie haben die Aufgabe {todo} in der Liste {calendar} gelöscht",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} aktualisiert",
- "You updated todo {todo} in list {calendar}" : "Sie haben die Aufgabe {todo} in der Liste {calendar} aktualisiert",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} erledigt",
- "You solved todo {todo} in list {calendar}" : "Sie haben die Aufgabe {todo} in der Liste {calendar} erledigt",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} wiedereröffnet",
- "You reopened todo {todo} in list {calendar}" : "Sie haben die Aufgabe {todo} in der Liste {calendar} wiedereröffnet",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} erstellt",
+ "You created to-do {todo} in list {calendar}" : "Sie haben eine Aufgabe {todo} in der Liste {calendar} erstellt",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} hat die Aufgabe {todo} aus der Liste {calendar} gelöscht",
+ "You deleted to-do {todo} from list {calendar}" : "Sie haben die Aufgabe {todo} aus der Liste {calendar} gelöscht",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} aktualisiert",
+ "You updated to-do {todo} in list {calendar}" : "Sie haben die Aufgabe {todo} in der Liste {calendar} aktualisiert",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} erledigt",
+ "You solved to-do {todo} in list {calendar}" : "Sie haben die Aufgabe {todo} in der Liste {calendar} erledigt",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} wiedereröffnet",
+ "You reopened to-do {todo} in list {calendar}" : "Sie haben die Aufgabe {todo} in der Liste {calendar} wiedereröffnet",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} hat die Aufgabe {todo} von der Liste {sourceCalendar} in die Liste {targetCalendar} verschoben",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Sie haben die Aufgabe {todo} von der Liste {sourceCalendar} in die Liste {targetCalendar} verschoben",
"Calendar, contacts and tasks" : "Kalender, Kontakte und Aufgaben",
"A <strong>calendar</strong> was modified" : "Ein <strong>Kalender</strong> wurde bearbeitet",
"A calendar <strong>event</strong> was modified" : "Ein Kalender-<strong>Termin</strong> wurde bearbeitet",
- "A calendar <strong>todo</strong> was modified" : "Eine Kalender-<strong>Aufgabe</strong> wurde bearbeitet",
+ "A calendar <strong>to-do</strong> was modified" : "Eine Kalender-<strong>Aufgabe</strong> wurde bearbeitet",
"Contact birthdays" : "Geburtstage von Kontakten",
"Death of %s" : "Todestag von %s",
+ "Untitled calendar" : "Kalender ohne Titel",
"Calendar:" : "Kalender:",
"Date:" : "Datum:",
"Where:" : "Wo:",
"Description:" : "Beschreibung:",
- "Untitled event" : "Termin ohne Titel",
"_%n year_::_%n years_" : ["%n Jahr","%n Jahre"],
"_%n month_::_%n months_" : ["%n Monat","%n Monate"],
"_%n day_::_%n days_" : ["%n Tag","%n Tage"],
@@ -67,22 +72,129 @@ OC.L10N.register(
"Description: %s" : "Beschreibung: %s",
"Where: %s" : "Ort: %s",
"%1$s via %2$s" : "%1$s über %2$s",
+ "In the past on %1$s for the entire day" : "In der Vergangenheit am %1$s für den ganzen Tag",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["In einer Minute um %1$s für den ganzen Tag","In %n Minuten um %1$s für den ganzen Tag"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["In einer Stunde um %1$s für den ganzen Tag","In %n Stunden um %1$s für den ganzen Tag"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["In einem Tag am %1$s für den ganzen Tag","In %n Tagen am %1$s für den ganzen Tag"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["In einer Woche am %1$s den ganzen Tag","In %n Wochen am %1$s den ganzen Tag"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["In einem Monat am %1$s den ganzen Tag","In %n Monaten am %1$s den ganzen Tag"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["In einem Jahr am %1$s für den ganzen Tag","In %n Jahren am %1$s für den ganzen Tag"],
+ "In the past on %1$s between %2$s - %3$s" : "In der Vergangenheit am %1$s zwischen %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["In einer Minute am%1$s zwischen %2$s - %3$s","In %n Minuten am %1$s zwischen %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["In einer Stunde am %1$s zwischen %2$s - %3$s","In %n Stunden am%1$s zwischen %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["In einem Tag am %1$s zwischen %2$s - %3$s","In %n Tagen am %1$szwischen %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["In einer Woche am %1$s zwischen%2$s - %3$s","In %n Wochen am%1$s zwischen %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["In einem Monat am %1$s zwischen %2$s - %3$s","In %n Monaten am %1$s zwischen %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["In einem Jahr am %1$s zwischen %2$s - %3$s","In %n Jahren am%1$s zwischen %2$s - %3$s"],
+ "Could not generate when statement" : "Wann-Angabe konnte nicht erzeugt werden",
+ "Every Day for the entire day" : "Jeden Tag für den ganzen Tag",
+ "Every Day for the entire day until %1$s" : "Jeden Tag für den ganzen Tag bis %1$s",
+ "Every Day between %1$s - %2$s" : "Jeden Tag zwischen %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Jeden Tag zwischen %1$s - %2$s bis %3$s",
+ "Every %1$d Days for the entire day" : "Alle %1$d Tage für den ganzen Tag",
+ "Every %1$d Days for the entire day until %2$s" : "Alle %1$d Tage für den ganzen Tag bis %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Alle %1$d Tage zwischen %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Alle %1$d Tage zwischen %2$s - %3$s bis %4$s",
+ "Could not generate event recurrence statement" : "Terminwiederholungsangabe konnte nicht erzeugt werden",
+ "Every Week on %1$s for the entire day" : "Jede Woche am %1$s für den ganzen Tag",
+ "Every Week on %1$s for the entire day until %2$s" : "Jede Woche am %1$s für den ganzen Tag bis %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Jede Woche am %1$s zwischen %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Jede Woche am %1$s zwischen %2$s - %3$s bis %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Alle %1$d Wochen am %2$s für den ganzen Tag",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Alle %1$d Wochen am %2$s für den ganzen Tag bis %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Alle %1$d Wochen am %2$s zwischen %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Alle %1$d Wochen am %2$s zwischen %3$s - %4$s bis %5$s",
+ "Every Month on the %1$s for the entire day" : "Jeden Monat am %1$s für den ganzen Tag",
+ "Every Month on the %1$s for the entire day until %2$s" : "Jeden Monat am %1$s für den ganzen Tag bis %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Jeden Monat am %1$s zwischen %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Jeden Monat am %1$s zwischen %2$s - %3$s bis %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Alle %1$d Monate am %2$s für den ganzen Tag",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Alle %1$d Monate am %2$s für den ganzen Tag bis %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Alle %1$d Monate am %2$s zwischen %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Alle %1$d Monate am %2$s zwischen %3$s - %4$s bis %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Jedes Jahr im %1$s am %2$s für den ganzen Tag",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Jedes Jahr im %1$s am %2$s für den ganzen Tag bis %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Jedes Jahr im %1$s am %2$s zwischen %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Jedes Jahr im %1$s am %2$s zwischen %3$s - %4$s bis %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Alle %1$d Jahre im %2$s am %3$s für den ganzen Tag",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Alle %1$d Jahre im %2$s am %3$s für den ganzen Tag bis %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Alle %1$d Jahre im %2$s am %3$s zwischen %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Alle %1$d Jahre im %2$s am %3$s zwischen %4$s - %5$s bis %6$s",
+ "On specific dates for the entire day until %1$s" : "An bestimmten Tagen für den ganzen Tag bis %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "An bestimmten Tagen zwischen %1$s - %2$s bis %3$s",
+ "In the past on %1$s" : "In der Vergangenheit am %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["In einer Minute am %1$s","In %n Minuten am %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["In einer Stunde am %1$s","In %n Stunden am %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["In einem Tag am %1$s","In %n Tagen am %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["In einer Woche am %1$s","In %n Wochen am %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["In einem Monat am %1$s","In %n Monaten am %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["In einem Jahr am %1$s","In %n Jahren am %1$s"],
+ "In the past on %1$s then on %2$s" : "In der Vergangenheit am %1$s danach am %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["In einer Minute am %1$s danach am %2$s","In %n Minuten am %1$s danach am %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["In einer Stunde am %1$s danach am %2$s","In %n Stunden am %1$s danach am %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["In einem Tag am %1$s danach am %2$s","In %n Tagen am %1$s danach am %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["In einer Woche am %1$s danach am %2$s","In %n Wochen am %1$s danach am %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["In einem Monat am %1$s danach am %2$s","In %n Monaten am %1$s danach am %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["In einem Jahr am %1$s danach am %2$s","In %n Jahren am %1$s danach am %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "In der Vergangenheit am %1$s danach am %2$s und %3$s",
+ "_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_" : ["In einer Minute am %1$s danach am %2$s und %3$s","In %n Minuten am %1$s danach am %2$s und %3$s"],
+ "_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_" : ["In einer Stunde am %1$s danach am %2$s und %3$s","In %n Stunden am %1$s danach am %2$s und %3$s"],
+ "_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_" : ["In einem Tag am %1$s danach am %2$s und %3$s","In %n Tagen am %1$s danach am %2$s und %3$s"],
+ "_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_" : ["In einer Woche am %1$s danach am %2$s und %3$s","In %n Wochen am %1$s danach am %2$s und %3$s"],
+ "_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_" : ["In einem Monat am %1$s danach am %2$s und %3$s","In %n Monaten am %1$s danach am %2$s und %3$s"],
+ "_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_" : ["In einem Jahr am %1$s danach am %2$s und %3$s","In %n Jahren am %1$s danach am %2$s und %3$s"],
+ "Could not generate next recurrence statement" : "Nächste Wiederholungsangabe konnte nicht erzeugt werden",
"Cancelled: %1$s" : "Abgesagt: %1$s",
- "Invitation canceled" : "Einladung abgebrochen",
+ "\"%1$s\" has been canceled" : "\"%1$s\" wurde abgesagt.",
"Re: %1$s" : "Re: %1$s",
- "Invitation updated" : "Einladung aktualisiert",
+ "%1$s has accepted your invitation" : "%1$s hat Ihre Einladung angenommen",
+ "%1$s has tentatively accepted your invitation" : "%1$s hat Ihre Einladung vorläufig angenommen",
+ "%1$s has declined your invitation" : "%1$s hat Ihre Einladung abgelehnt",
+ "%1$s has responded to your invitation" : "%1$s hat auf Ihre Einladung geantwortet",
+ "Invitation updated: %1$s" : "Einladung aktualisiert: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s hat die Veranstaltung \"%2$s\" aktualisiert",
"Invitation: %1$s" : "Einladung: %1$s",
- "Invitation" : "Einladung",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s möchte Sie zu \"%2$s\" einladen",
+ "Organizer:" : "Organisator:",
+ "Attendees:" : "Teilnehmer:",
"Title:" : "Titel:",
- "Time:" : "Zeit:",
+ "When:" : "Wann:",
"Location:" : "Ort:",
"Link:" : "Link:",
- "Organizer:" : "Organisator:",
- "Attendees:" : "Teilnehmer:",
+ "Occurring:" : "Findet statt:",
"Accept" : "Akzeptieren",
"Decline" : "Ablehnen",
"More options …" : "Weitere Optionen …",
"More options at %s" : "Weitere Optionen unter %s",
+ "Monday" : "Montag",
+ "Tuesday" : "Dienstag",
+ "Wednesday" : "Mittwoch",
+ "Thursday" : "Donnerstag",
+ "Friday" : "Freitag",
+ "Saturday" : "Samstag",
+ "Sunday" : "Sonntag",
+ "January" : "Januar",
+ "February" : "Februar",
+ "March" : "März",
+ "April" : "April",
+ "May" : "Mai",
+ "June" : "Juni",
+ "July" : "Juli",
+ "August" : "August",
+ "September" : "September",
+ "October" : "Oktober",
+ "November" : "November",
+ "December" : "Dezember",
+ "First" : "Erstes",
+ "Second" : "Zweites",
+ "Third" : "Drittes",
+ "Fourth" : "Viertes",
+ "Fifth" : "Fünftes",
+ "Last" : "Letztes",
+ "Second Last" : "Vorletztes",
+ "Third Last" : "Drittletztes",
+ "Fourth Last" : "Viertletztes",
+ "Fifth Last" : "Fünftletztes",
"Contacts" : "Kontakte",
"{actor} created address book {addressbook}" : "{actor} hat das Adressbuch {addressbook} erstellt",
"You created address book {addressbook}" : "Sie haben das Adressbuch {addressbook} erstellt",
@@ -108,7 +220,10 @@ OC.L10N.register(
"{actor} updated contact {card} in address book {addressbook}" : "{actor} hat den Kontakt {card} im Adressbuch {addressbook} aktualisiert",
"You updated contact {card} in address book {addressbook}" : "Sie haben den Kontakt {card} im Adressbuch {addressbook} aktualisiert",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "Ein <strong>Kontakt</strong> oder ein <strong>Adressbuch</strong> wurde geändert",
+ "Accounts" : "Konten",
+ "System address book which holds all accounts" : "Systemadressbuch, das alle Konten enthält",
"File is not updatable: %1$s" : "Datei kann nicht aktualisiert werden: %1$s",
+ "Failed to get storage for file" : "Speicherplatz für Datei konnte nicht abgerufen werden",
"Could not write to final file, canceled by hook" : "Konnte nicht in die endgültige Datei schreiben, wurde durch Hook abgebrochen.",
"Could not write file contents" : "Dateiinhalt konnte nicht geschrieben werden",
"_%n byte_::_%n bytes_" : ["%n Byte","%n Bytes"],
@@ -117,64 +232,107 @@ OC.L10N.register(
"Could not rename part file to final file, canceled by hook" : "Konnte Teildatei nicht in endgültige Datei umbenennen, wurde durch Hook abgebrochen.",
"Could not rename part file to final file" : "Konnte Teildatei nicht in endgültige Datei umbenennen",
"Failed to check file size: %1$s" : "Dateigröße konnte nicht überprüft werden: %1$s",
- "Could not open file" : "Datei konnte nicht geöffnet werden",
+ "Could not open file: %1$s, file does seem to exist" : "Datei konnte nicht geöffnet werden: %1$s, Datei scheint zu existieren",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Datei konnte nicht geöffnet werden: %1$s, Datei scheint nicht zu existieren",
"Encryption not ready: %1$s" : "Verschlüsselung nicht bereit: %1$s",
"Failed to open file: %1$s" : "Datei konnte nicht geöffnet werden: %1$s",
"Failed to unlink: %1$s" : "Fehler beim Aufheben der Verknüpfung: %1$s",
- "Invalid chunk name" : "Ungültiger Chunk-Name",
- "Could not rename part file assembled from chunks" : "Aus Chunks zusammengesetzte Teildatei konnte nicht umbenannt werden",
"Failed to write file contents: %1$s" : "Fehler beim Schreiben des Dateiinhalts: %1$s",
"File not found: %1$s" : "Datei nicht gefunden: %1$s",
+ "Invalid target path" : "Ungültiger Ziel-Pfad",
"System is in maintenance mode." : "Das System befindet sich im Wartungsmodus.",
"Upgrade needed" : "Aktualisierung erforderlich",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Ihr %s muss konfiguriert werden, um HTTPS zusammen mit CalDAV und CardDAV unter iOS/macOS nutzen zu können.",
"Configures a CalDAV account" : "Ein CalDAV-Konto einrichten",
"Configures a CardDAV account" : "Ein CardDAV-Konto einrichten",
"Events" : "Ereignisse",
- "Tasks" : "Aufgaben",
"Untitled task" : "Unbenannte Aufgabe",
"Completed on %s" : "Erledigt am %s",
"Due on %s by %s" : "Fällig am %s von %s",
"Due on %s" : "Fällig am %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Willkommen bei Nextcloud Calendar!\n\nDies ist ein Beispielereignis – entdecken Sie die Flexibilität der Planung mit Nextcloud Calendar und nehmen Sie beliebige Änderungen vor!\n\nMit Nextcloud Calendar können Sie:\n– Ereignisse mühelos erstellen, bearbeiten und verwalten.\n– Mehrere Kalender erstellen und mit Teamkollegen, Freunden oder der Familie teilen.\n– Verfügbarkeit prüfen und Ihre Termine anderen anzeigen.\n– Nahtlose Integration mit Apps und Geräten über CalDAV.\n– Individuelle Gestaltung: Planen Sie wiederkehrende Ereignisse, passen Sie Benachrichtigungen und andere Einstellungen an.",
+ "Example event - open me!" : "Beispielereignis – öffne mich!",
+ "System Address Book" : "Systemadressbuch",
+ "The system address book contains contact information for all users in your instance." : "Das Systemadressbuch enthält Kontaktinformationen für alle Benutzer in dieser Instanz.",
+ "Enable System Address Book" : "Systemadressbuch aktivieren",
+ "DAV system address book" : "DAV-Systemadressbuch",
+ "No outstanding DAV system address book sync." : "Keine ausstehende Synchronisierung des DAV-Systemadressbuchs.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Die Synchronisierung des DAV-Systemadressbuchs wurde noch nicht ausgeführt, da Ihre Instanz mehr als 1000 Benutzer hat oder weil ein Fehler aufgetreten ist. Bitte führen Sie sie manuell aus, indem Sie \"occ dav:sync-system-addressbook\" aufrufen.",
+ "WebDAV endpoint" : "WebDAV-Endpunkt",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Es konnte nicht überprüft werden, ob Ihr Webserver ordnungsgemäß eingerichtet ist, um Dateisynchronisation über WebDAV zu ermöglichen. Bitte überprüfen Sie dies manuell.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Ihr Webserver ist noch nicht hinreichend für Datei-Synchronisierung konfiguriert. Die WebDAV-Schnittstelle ist vermutlich defekt.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Ihr Webserver ist ordnungsgemäß eingerichtet um Dateisynchronisation über WebDAV zu ermöglichen.",
"Migrated calendar (%1$s)" : "Migrierter Kalender (%1$s)",
"Calendars including events, details and attendees" : "Kalender mit Terminen, Details und Teilnehmern",
"Contacts and groups" : "Kontakte und Gruppen",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV-Endpunkt",
- "Availability" : "Verfügbarkeit",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Wenn Sie Ihre Arbeitszeiten konfigurieren, können andere Benutzer sehen, wann Sie nicht im Büro sind, wenn sie eine Besprechung buchen.",
+ "Absence saved" : "Abwesenheit gespeichert",
+ "Failed to save your absence settings" : "Ihre Abwesenheitseinstellungen konnten nicht gespeichert werden",
+ "Absence cleared" : "Abwesenheit gelöscht",
+ "Failed to clear your absence settings" : "Ihre Abwesenheitseinstellungen konnten nicht gelöscht werden",
+ "First day" : "Erster Tag",
+ "Last day (inclusive)" : "Letzter Tag (inklusiv)",
+ "Out of office replacement (optional)" : "Abwesenheitsvertretung (optional)",
+ "Name of the replacement" : "Name der Vertretung",
+ "No results." : "Keine Ergebnisse.",
+ "Start typing." : "Anfangen zu tippen.",
+ "Short absence status" : "Kurzer Abwesenheitsstatus",
+ "Long absence Message" : "Lange Abwesenheitsnachricht",
+ "Save" : "Speichern",
+ "Disable absence" : "Abwesenheit deaktivieren",
+ "Failed to load availability" : "Verfügbarkeit konnte nicht geladen werden",
+ "Saved availability" : "Verfügbarkeit gespeichert",
+ "Failed to save availability" : "Verfügbarkeit konnte nicht gespeichert werden",
"Time zone:" : "Zeitzone:",
- "to" : "an",
+ "to" : "bis",
"Delete slot" : "Zeitfenster löschen",
- "No working hours set" : "Arbeitsfreie Stunden gesetzt",
+ "No working hours set" : "Keine Arbeitszeiten konfiguriert",
"Add slot" : "Zeitfenster hinzufügen",
- "Monday" : "Montag",
- "Tuesday" : "Dienstag",
- "Wednesday" : "Mittwoch",
- "Thursday" : "Donnerstag",
- "Friday" : "Freitag",
- "Saturday" : "Samstag",
- "Sunday" : "Sonntag",
- "Save" : "Speichern",
+ "Weekdays" : "Wochentage",
+ "Pick a start time for {dayName}" : "Eine Startzeit für {dayName} wählen",
+ "Pick a end time for {dayName}" : "Eine Endezeit für {dayName} wählen",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Setzen Sie den Benutzerstatus außerhalb Ihrer Verfügbarkeit automatisch auf \"Nicht stören\", um alle Benachrichtigungen stumm zu schalten.",
+ "Cancel" : "Abbrechen",
+ "Import" : "Importieren",
+ "Error while saving settings" : "Fehler beim Speichern der Einstellungen",
+ "Contact reset successfully" : "Kontakt zurückgesetzt",
+ "Error while resetting contact" : "Fehler beim Zurücksetzen des Kontakts",
+ "Contact imported successfully" : "Kontakt importiert",
+ "Error while importing contact" : "Fehler beim Import des Kontakts",
+ "Import contact" : "Kontakt importieren",
+ "Reset to default" : "Auf Standard zurücksetzen ",
+ "Import contacts" : "Kontakte importieren",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Durch das Importieren einer neuen VCF-Datei wird der vorhandene Standardkontakt gelöscht und durch den neuen ersetzt. Fortsetzen?",
+ "Failed to save example event creation setting" : "Einstellung für die Beispiels-Terminerstellung konnte nicht gespeichert werden",
+ "Failed to upload the example event" : "Der Beispieltermin konnte nicht hochgeladen werden",
+ "Custom example event was saved successfully" : "Benutzerdefinierter Beispieltermin gespeichert",
+ "Failed to delete the custom example event" : "Benutzerdefinierter Beispieltermin konnte nicht gelöscht werden",
+ "Custom example event was deleted successfully" : "Benutzerdefinierter Beispieltermin wurde gelöscht",
+ "Import calendar event" : "Kalendertermin importieren",
+ "Uploading a new event will overwrite the existing one." : "Das Hochladen eines neuen Termins wird den bestehenden Termin überschreiben.",
+ "Upload event" : "Termin hochladen",
+ "Availability" : "Verfügbarkeit",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Wenn Sie Ihre Arbeitszeiten angeben, können andere beim Buchen einer Besprechung sehen, wann Sie nicht im Büro sind.",
+ "Absence" : "Abwesenheit",
+ "Configure your next absence period." : "Richten Sie ihren nächsten Abwesenheitszeitraum ein.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installieren Sie außerdem die {calendarappstoreopen}Kalender-App{linkclose} oder {calendardocopen}verbinden Sie Ihren Desktop & Mobilgerät zur Synchronisierung ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Bitte stellen Sie sicher, dass Sie {emailopen}den E-Mail Server{linkclose} ordnungsgemäß eingerichtet haben.",
"Calendar server" : "Kalender-Server",
"Send invitations to attendees" : "Einladungen an die Teilnehmer versenden",
"Automatically generate a birthday calendar" : "Automatisch einen Kalender für Geburtstage erstellen",
"Birthday calendars will be generated by a background job." : "Kalender für Geburtstage werden von einem Hintergrund-Auftrag erstellt",
"Hence they will not be available immediately after enabling but will show up after some time." : "Die Einträge werden nicht sofort angezeigt. Nach der Aktivierung wird es ein wenig dauern bis zur Anzeige.",
"Send notifications for events" : "Sende Benachrichtigungen für Termine",
- "Notifications are sent via background jobs, so these must occur often enough." : "Benachrichtigungen werden von Hintergrundjobs versendet, so dass diese häufig genug ausgeführt werden müssen.",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Benachrichtigungen werden von Hintergrundaufgaben versendet, so dass diese häufig genug ausgeführt werden müssen.",
"Send reminder notifications to calendar sharees as well" : "Erinnerungsbenachrichtigungen auch an die Kalenderfreigaben senden",
"Reminders are always sent to organizers and attendees." : "Erinnerungen werden immer an Organisatoren und Teilnehmer gesendet.",
"Enable notifications for events via push" : "Benachrichtigungen für Termine per Push aktivieren",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installieren Sie außerdem die {calendarappstoreopen}Kalender-App{linkclose} oder {calendardocopen}verbinden Sie Ihren Desktop & Mobilgerät zur Synchronisierung ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Bitte stellen Sie sicher, dass Sie {emailopen}den E-Mail Server{linkclose} ordnungsgemäß eingerichtet haben.",
+ "Example content" : "Beispielsinhalt",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Beispielinhalte dienen dazu, die Funktionen von Nextcloud vorzustellen. Standardinhalte werden mit Nextcloud ausgeliefert und können durch benutzerdefinierte Inhalte ersetzt werden.",
"There was an error updating your attendance status." : "Es ist ein Fehler beim Aktualisieren Ihres Teilnehmerstatus aufgetreten.",
"Please contact the organizer directly." : "Bitte den Organisator direkt kontaktieren.",
"Are you accepting the invitation?" : "Die Einladung annehmen?",
"Tentative" : "Vorläufig",
- "Number of guests" : "Anzahl Gäste",
- "Comment" : "Kommentar",
- "Your attendance was updated successfully." : "Ihr Teilnehmerstatus wurde aktualisiert.",
- "Calendar and tasks" : "Kalender und Aufgaben"
+ "Your attendance was updated successfully." : "Ihr Teilnehmerstatus wurde aktualisiert."
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/de_DE.json b/apps/dav/l10n/de_DE.json
index fc4c0992f3c..a30215c7071 100644
--- a/apps/dav/l10n/de_DE.json
+++ b/apps/dav/l10n/de_DE.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Kalender",
- "Todos" : "Aufgaben",
+ "Tasks" : "Aufgaben",
"Personal" : "Persönlich",
"{actor} created calendar {calendar}" : "{actor} hat den Kalender {calendar} erstellt",
"You created calendar {calendar}" : "Sie haben den Kalender {calendar} erstellt",
@@ -23,36 +23,41 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} teilt den Kalender {calendar} mit der Gruppe {group}",
"You unshared calendar {calendar} from group {group}" : "Sie teilen den Kalender {calendar} nicht mehr mit der Gruppe {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} teilt den Kalender {calendar} nicht mehr mit der Gruppe {group}",
+ "Untitled event" : "Unbenannter Termin",
"{actor} created event {event} in calendar {calendar}" : "{actor} hat den Termin {event} im Kalender {calendar} erstellt",
"You created event {event} in calendar {calendar}" : "Sie haben den Termin {event} im Kalender {calendar} erstellt",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} hat den Termin {event} im Kalender {calendar} gelöscht",
"You deleted event {event} from calendar {calendar}" : "Sie haben den Termin {event} im Kalender {calendar} gelöscht",
"{actor} updated event {event} in calendar {calendar}" : "{actor} hat den Termin {event} im Kalender {calendar} aktualisiert",
"You updated event {event} in calendar {calendar}" : "Sie haben den Termin {event} im Kalender {calendar} aktualisiert",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} hat den Termin {event} vom Kalender {sourceCalendar} in den Kalender {targetCalendar} verschoben",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Sie haben den Termin {event} vom Kalender {sourceCalendar} in den Kalender {targetCalendar} verschoben",
"{actor} restored event {event} of calendar {calendar}" : "{actor} hat den Termin {event} im Kalender {calendar} wiederhergestellt",
"You restored event {event} of calendar {calendar}" : "Sie haben den Termin {event} im Kalender {calendar} wiederhergestellt",
"Busy" : "Beschäftigt",
- "{actor} created todo {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} erstellt",
- "You created todo {todo} in list {calendar}" : "Sie haben die Aufgabe {todo} in der Liste {calendar} erstellt",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} gelöscht",
- "You deleted todo {todo} from list {calendar}" : "Sie haben die Aufgabe {todo} in der Liste {calendar} gelöscht",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} aktualisiert",
- "You updated todo {todo} in list {calendar}" : "Sie haben die Aufgabe {todo} in der Liste {calendar} aktualisiert",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} erledigt",
- "You solved todo {todo} in list {calendar}" : "Sie haben die Aufgabe {todo} in der Liste {calendar} erledigt",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} wiedereröffnet",
- "You reopened todo {todo} in list {calendar}" : "Sie haben die Aufgabe {todo} in der Liste {calendar} wiedereröffnet",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} erstellt",
+ "You created to-do {todo} in list {calendar}" : "Sie haben eine Aufgabe {todo} in der Liste {calendar} erstellt",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} hat die Aufgabe {todo} aus der Liste {calendar} gelöscht",
+ "You deleted to-do {todo} from list {calendar}" : "Sie haben die Aufgabe {todo} aus der Liste {calendar} gelöscht",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} aktualisiert",
+ "You updated to-do {todo} in list {calendar}" : "Sie haben die Aufgabe {todo} in der Liste {calendar} aktualisiert",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} erledigt",
+ "You solved to-do {todo} in list {calendar}" : "Sie haben die Aufgabe {todo} in der Liste {calendar} erledigt",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} hat die Aufgabe {todo} in der Liste {calendar} wiedereröffnet",
+ "You reopened to-do {todo} in list {calendar}" : "Sie haben die Aufgabe {todo} in der Liste {calendar} wiedereröffnet",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} hat die Aufgabe {todo} von der Liste {sourceCalendar} in die Liste {targetCalendar} verschoben",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Sie haben die Aufgabe {todo} von der Liste {sourceCalendar} in die Liste {targetCalendar} verschoben",
"Calendar, contacts and tasks" : "Kalender, Kontakte und Aufgaben",
"A <strong>calendar</strong> was modified" : "Ein <strong>Kalender</strong> wurde bearbeitet",
"A calendar <strong>event</strong> was modified" : "Ein Kalender-<strong>Termin</strong> wurde bearbeitet",
- "A calendar <strong>todo</strong> was modified" : "Eine Kalender-<strong>Aufgabe</strong> wurde bearbeitet",
+ "A calendar <strong>to-do</strong> was modified" : "Eine Kalender-<strong>Aufgabe</strong> wurde bearbeitet",
"Contact birthdays" : "Geburtstage von Kontakten",
"Death of %s" : "Todestag von %s",
+ "Untitled calendar" : "Kalender ohne Titel",
"Calendar:" : "Kalender:",
"Date:" : "Datum:",
"Where:" : "Wo:",
"Description:" : "Beschreibung:",
- "Untitled event" : "Termin ohne Titel",
"_%n year_::_%n years_" : ["%n Jahr","%n Jahre"],
"_%n month_::_%n months_" : ["%n Monat","%n Monate"],
"_%n day_::_%n days_" : ["%n Tag","%n Tage"],
@@ -65,22 +70,129 @@
"Description: %s" : "Beschreibung: %s",
"Where: %s" : "Ort: %s",
"%1$s via %2$s" : "%1$s über %2$s",
+ "In the past on %1$s for the entire day" : "In der Vergangenheit am %1$s für den ganzen Tag",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["In einer Minute um %1$s für den ganzen Tag","In %n Minuten um %1$s für den ganzen Tag"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["In einer Stunde um %1$s für den ganzen Tag","In %n Stunden um %1$s für den ganzen Tag"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["In einem Tag am %1$s für den ganzen Tag","In %n Tagen am %1$s für den ganzen Tag"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["In einer Woche am %1$s den ganzen Tag","In %n Wochen am %1$s den ganzen Tag"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["In einem Monat am %1$s den ganzen Tag","In %n Monaten am %1$s den ganzen Tag"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["In einem Jahr am %1$s für den ganzen Tag","In %n Jahren am %1$s für den ganzen Tag"],
+ "In the past on %1$s between %2$s - %3$s" : "In der Vergangenheit am %1$s zwischen %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["In einer Minute am%1$s zwischen %2$s - %3$s","In %n Minuten am %1$s zwischen %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["In einer Stunde am %1$s zwischen %2$s - %3$s","In %n Stunden am%1$s zwischen %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["In einem Tag am %1$s zwischen %2$s - %3$s","In %n Tagen am %1$szwischen %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["In einer Woche am %1$s zwischen%2$s - %3$s","In %n Wochen am%1$s zwischen %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["In einem Monat am %1$s zwischen %2$s - %3$s","In %n Monaten am %1$s zwischen %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["In einem Jahr am %1$s zwischen %2$s - %3$s","In %n Jahren am%1$s zwischen %2$s - %3$s"],
+ "Could not generate when statement" : "Wann-Angabe konnte nicht erzeugt werden",
+ "Every Day for the entire day" : "Jeden Tag für den ganzen Tag",
+ "Every Day for the entire day until %1$s" : "Jeden Tag für den ganzen Tag bis %1$s",
+ "Every Day between %1$s - %2$s" : "Jeden Tag zwischen %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Jeden Tag zwischen %1$s - %2$s bis %3$s",
+ "Every %1$d Days for the entire day" : "Alle %1$d Tage für den ganzen Tag",
+ "Every %1$d Days for the entire day until %2$s" : "Alle %1$d Tage für den ganzen Tag bis %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Alle %1$d Tage zwischen %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Alle %1$d Tage zwischen %2$s - %3$s bis %4$s",
+ "Could not generate event recurrence statement" : "Terminwiederholungsangabe konnte nicht erzeugt werden",
+ "Every Week on %1$s for the entire day" : "Jede Woche am %1$s für den ganzen Tag",
+ "Every Week on %1$s for the entire day until %2$s" : "Jede Woche am %1$s für den ganzen Tag bis %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Jede Woche am %1$s zwischen %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Jede Woche am %1$s zwischen %2$s - %3$s bis %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Alle %1$d Wochen am %2$s für den ganzen Tag",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Alle %1$d Wochen am %2$s für den ganzen Tag bis %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Alle %1$d Wochen am %2$s zwischen %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Alle %1$d Wochen am %2$s zwischen %3$s - %4$s bis %5$s",
+ "Every Month on the %1$s for the entire day" : "Jeden Monat am %1$s für den ganzen Tag",
+ "Every Month on the %1$s for the entire day until %2$s" : "Jeden Monat am %1$s für den ganzen Tag bis %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Jeden Monat am %1$s zwischen %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Jeden Monat am %1$s zwischen %2$s - %3$s bis %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Alle %1$d Monate am %2$s für den ganzen Tag",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Alle %1$d Monate am %2$s für den ganzen Tag bis %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Alle %1$d Monate am %2$s zwischen %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Alle %1$d Monate am %2$s zwischen %3$s - %4$s bis %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Jedes Jahr im %1$s am %2$s für den ganzen Tag",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Jedes Jahr im %1$s am %2$s für den ganzen Tag bis %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Jedes Jahr im %1$s am %2$s zwischen %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Jedes Jahr im %1$s am %2$s zwischen %3$s - %4$s bis %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Alle %1$d Jahre im %2$s am %3$s für den ganzen Tag",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Alle %1$d Jahre im %2$s am %3$s für den ganzen Tag bis %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Alle %1$d Jahre im %2$s am %3$s zwischen %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Alle %1$d Jahre im %2$s am %3$s zwischen %4$s - %5$s bis %6$s",
+ "On specific dates for the entire day until %1$s" : "An bestimmten Tagen für den ganzen Tag bis %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "An bestimmten Tagen zwischen %1$s - %2$s bis %3$s",
+ "In the past on %1$s" : "In der Vergangenheit am %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["In einer Minute am %1$s","In %n Minuten am %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["In einer Stunde am %1$s","In %n Stunden am %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["In einem Tag am %1$s","In %n Tagen am %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["In einer Woche am %1$s","In %n Wochen am %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["In einem Monat am %1$s","In %n Monaten am %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["In einem Jahr am %1$s","In %n Jahren am %1$s"],
+ "In the past on %1$s then on %2$s" : "In der Vergangenheit am %1$s danach am %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["In einer Minute am %1$s danach am %2$s","In %n Minuten am %1$s danach am %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["In einer Stunde am %1$s danach am %2$s","In %n Stunden am %1$s danach am %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["In einem Tag am %1$s danach am %2$s","In %n Tagen am %1$s danach am %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["In einer Woche am %1$s danach am %2$s","In %n Wochen am %1$s danach am %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["In einem Monat am %1$s danach am %2$s","In %n Monaten am %1$s danach am %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["In einem Jahr am %1$s danach am %2$s","In %n Jahren am %1$s danach am %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "In der Vergangenheit am %1$s danach am %2$s und %3$s",
+ "_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_" : ["In einer Minute am %1$s danach am %2$s und %3$s","In %n Minuten am %1$s danach am %2$s und %3$s"],
+ "_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_" : ["In einer Stunde am %1$s danach am %2$s und %3$s","In %n Stunden am %1$s danach am %2$s und %3$s"],
+ "_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_" : ["In einem Tag am %1$s danach am %2$s und %3$s","In %n Tagen am %1$s danach am %2$s und %3$s"],
+ "_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_" : ["In einer Woche am %1$s danach am %2$s und %3$s","In %n Wochen am %1$s danach am %2$s und %3$s"],
+ "_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_" : ["In einem Monat am %1$s danach am %2$s und %3$s","In %n Monaten am %1$s danach am %2$s und %3$s"],
+ "_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_" : ["In einem Jahr am %1$s danach am %2$s und %3$s","In %n Jahren am %1$s danach am %2$s und %3$s"],
+ "Could not generate next recurrence statement" : "Nächste Wiederholungsangabe konnte nicht erzeugt werden",
"Cancelled: %1$s" : "Abgesagt: %1$s",
- "Invitation canceled" : "Einladung abgebrochen",
+ "\"%1$s\" has been canceled" : "\"%1$s\" wurde abgesagt.",
"Re: %1$s" : "Re: %1$s",
- "Invitation updated" : "Einladung aktualisiert",
+ "%1$s has accepted your invitation" : "%1$s hat Ihre Einladung angenommen",
+ "%1$s has tentatively accepted your invitation" : "%1$s hat Ihre Einladung vorläufig angenommen",
+ "%1$s has declined your invitation" : "%1$s hat Ihre Einladung abgelehnt",
+ "%1$s has responded to your invitation" : "%1$s hat auf Ihre Einladung geantwortet",
+ "Invitation updated: %1$s" : "Einladung aktualisiert: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s hat die Veranstaltung \"%2$s\" aktualisiert",
"Invitation: %1$s" : "Einladung: %1$s",
- "Invitation" : "Einladung",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s möchte Sie zu \"%2$s\" einladen",
+ "Organizer:" : "Organisator:",
+ "Attendees:" : "Teilnehmer:",
"Title:" : "Titel:",
- "Time:" : "Zeit:",
+ "When:" : "Wann:",
"Location:" : "Ort:",
"Link:" : "Link:",
- "Organizer:" : "Organisator:",
- "Attendees:" : "Teilnehmer:",
+ "Occurring:" : "Findet statt:",
"Accept" : "Akzeptieren",
"Decline" : "Ablehnen",
"More options …" : "Weitere Optionen …",
"More options at %s" : "Weitere Optionen unter %s",
+ "Monday" : "Montag",
+ "Tuesday" : "Dienstag",
+ "Wednesday" : "Mittwoch",
+ "Thursday" : "Donnerstag",
+ "Friday" : "Freitag",
+ "Saturday" : "Samstag",
+ "Sunday" : "Sonntag",
+ "January" : "Januar",
+ "February" : "Februar",
+ "March" : "März",
+ "April" : "April",
+ "May" : "Mai",
+ "June" : "Juni",
+ "July" : "Juli",
+ "August" : "August",
+ "September" : "September",
+ "October" : "Oktober",
+ "November" : "November",
+ "December" : "Dezember",
+ "First" : "Erstes",
+ "Second" : "Zweites",
+ "Third" : "Drittes",
+ "Fourth" : "Viertes",
+ "Fifth" : "Fünftes",
+ "Last" : "Letztes",
+ "Second Last" : "Vorletztes",
+ "Third Last" : "Drittletztes",
+ "Fourth Last" : "Viertletztes",
+ "Fifth Last" : "Fünftletztes",
"Contacts" : "Kontakte",
"{actor} created address book {addressbook}" : "{actor} hat das Adressbuch {addressbook} erstellt",
"You created address book {addressbook}" : "Sie haben das Adressbuch {addressbook} erstellt",
@@ -106,7 +218,10 @@
"{actor} updated contact {card} in address book {addressbook}" : "{actor} hat den Kontakt {card} im Adressbuch {addressbook} aktualisiert",
"You updated contact {card} in address book {addressbook}" : "Sie haben den Kontakt {card} im Adressbuch {addressbook} aktualisiert",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "Ein <strong>Kontakt</strong> oder ein <strong>Adressbuch</strong> wurde geändert",
+ "Accounts" : "Konten",
+ "System address book which holds all accounts" : "Systemadressbuch, das alle Konten enthält",
"File is not updatable: %1$s" : "Datei kann nicht aktualisiert werden: %1$s",
+ "Failed to get storage for file" : "Speicherplatz für Datei konnte nicht abgerufen werden",
"Could not write to final file, canceled by hook" : "Konnte nicht in die endgültige Datei schreiben, wurde durch Hook abgebrochen.",
"Could not write file contents" : "Dateiinhalt konnte nicht geschrieben werden",
"_%n byte_::_%n bytes_" : ["%n Byte","%n Bytes"],
@@ -115,64 +230,107 @@
"Could not rename part file to final file, canceled by hook" : "Konnte Teildatei nicht in endgültige Datei umbenennen, wurde durch Hook abgebrochen.",
"Could not rename part file to final file" : "Konnte Teildatei nicht in endgültige Datei umbenennen",
"Failed to check file size: %1$s" : "Dateigröße konnte nicht überprüft werden: %1$s",
- "Could not open file" : "Datei konnte nicht geöffnet werden",
+ "Could not open file: %1$s, file does seem to exist" : "Datei konnte nicht geöffnet werden: %1$s, Datei scheint zu existieren",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Datei konnte nicht geöffnet werden: %1$s, Datei scheint nicht zu existieren",
"Encryption not ready: %1$s" : "Verschlüsselung nicht bereit: %1$s",
"Failed to open file: %1$s" : "Datei konnte nicht geöffnet werden: %1$s",
"Failed to unlink: %1$s" : "Fehler beim Aufheben der Verknüpfung: %1$s",
- "Invalid chunk name" : "Ungültiger Chunk-Name",
- "Could not rename part file assembled from chunks" : "Aus Chunks zusammengesetzte Teildatei konnte nicht umbenannt werden",
"Failed to write file contents: %1$s" : "Fehler beim Schreiben des Dateiinhalts: %1$s",
"File not found: %1$s" : "Datei nicht gefunden: %1$s",
+ "Invalid target path" : "Ungültiger Ziel-Pfad",
"System is in maintenance mode." : "Das System befindet sich im Wartungsmodus.",
"Upgrade needed" : "Aktualisierung erforderlich",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Ihr %s muss konfiguriert werden, um HTTPS zusammen mit CalDAV und CardDAV unter iOS/macOS nutzen zu können.",
"Configures a CalDAV account" : "Ein CalDAV-Konto einrichten",
"Configures a CardDAV account" : "Ein CardDAV-Konto einrichten",
"Events" : "Ereignisse",
- "Tasks" : "Aufgaben",
"Untitled task" : "Unbenannte Aufgabe",
"Completed on %s" : "Erledigt am %s",
"Due on %s by %s" : "Fällig am %s von %s",
"Due on %s" : "Fällig am %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Willkommen bei Nextcloud Calendar!\n\nDies ist ein Beispielereignis – entdecken Sie die Flexibilität der Planung mit Nextcloud Calendar und nehmen Sie beliebige Änderungen vor!\n\nMit Nextcloud Calendar können Sie:\n– Ereignisse mühelos erstellen, bearbeiten und verwalten.\n– Mehrere Kalender erstellen und mit Teamkollegen, Freunden oder der Familie teilen.\n– Verfügbarkeit prüfen und Ihre Termine anderen anzeigen.\n– Nahtlose Integration mit Apps und Geräten über CalDAV.\n– Individuelle Gestaltung: Planen Sie wiederkehrende Ereignisse, passen Sie Benachrichtigungen und andere Einstellungen an.",
+ "Example event - open me!" : "Beispielereignis – öffne mich!",
+ "System Address Book" : "Systemadressbuch",
+ "The system address book contains contact information for all users in your instance." : "Das Systemadressbuch enthält Kontaktinformationen für alle Benutzer in dieser Instanz.",
+ "Enable System Address Book" : "Systemadressbuch aktivieren",
+ "DAV system address book" : "DAV-Systemadressbuch",
+ "No outstanding DAV system address book sync." : "Keine ausstehende Synchronisierung des DAV-Systemadressbuchs.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Die Synchronisierung des DAV-Systemadressbuchs wurde noch nicht ausgeführt, da Ihre Instanz mehr als 1000 Benutzer hat oder weil ein Fehler aufgetreten ist. Bitte führen Sie sie manuell aus, indem Sie \"occ dav:sync-system-addressbook\" aufrufen.",
+ "WebDAV endpoint" : "WebDAV-Endpunkt",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Es konnte nicht überprüft werden, ob Ihr Webserver ordnungsgemäß eingerichtet ist, um Dateisynchronisation über WebDAV zu ermöglichen. Bitte überprüfen Sie dies manuell.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Ihr Webserver ist noch nicht hinreichend für Datei-Synchronisierung konfiguriert. Die WebDAV-Schnittstelle ist vermutlich defekt.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Ihr Webserver ist ordnungsgemäß eingerichtet um Dateisynchronisation über WebDAV zu ermöglichen.",
"Migrated calendar (%1$s)" : "Migrierter Kalender (%1$s)",
"Calendars including events, details and attendees" : "Kalender mit Terminen, Details und Teilnehmern",
"Contacts and groups" : "Kontakte und Gruppen",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV-Endpunkt",
- "Availability" : "Verfügbarkeit",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Wenn Sie Ihre Arbeitszeiten konfigurieren, können andere Benutzer sehen, wann Sie nicht im Büro sind, wenn sie eine Besprechung buchen.",
+ "Absence saved" : "Abwesenheit gespeichert",
+ "Failed to save your absence settings" : "Ihre Abwesenheitseinstellungen konnten nicht gespeichert werden",
+ "Absence cleared" : "Abwesenheit gelöscht",
+ "Failed to clear your absence settings" : "Ihre Abwesenheitseinstellungen konnten nicht gelöscht werden",
+ "First day" : "Erster Tag",
+ "Last day (inclusive)" : "Letzter Tag (inklusiv)",
+ "Out of office replacement (optional)" : "Abwesenheitsvertretung (optional)",
+ "Name of the replacement" : "Name der Vertretung",
+ "No results." : "Keine Ergebnisse.",
+ "Start typing." : "Anfangen zu tippen.",
+ "Short absence status" : "Kurzer Abwesenheitsstatus",
+ "Long absence Message" : "Lange Abwesenheitsnachricht",
+ "Save" : "Speichern",
+ "Disable absence" : "Abwesenheit deaktivieren",
+ "Failed to load availability" : "Verfügbarkeit konnte nicht geladen werden",
+ "Saved availability" : "Verfügbarkeit gespeichert",
+ "Failed to save availability" : "Verfügbarkeit konnte nicht gespeichert werden",
"Time zone:" : "Zeitzone:",
- "to" : "an",
+ "to" : "bis",
"Delete slot" : "Zeitfenster löschen",
- "No working hours set" : "Arbeitsfreie Stunden gesetzt",
+ "No working hours set" : "Keine Arbeitszeiten konfiguriert",
"Add slot" : "Zeitfenster hinzufügen",
- "Monday" : "Montag",
- "Tuesday" : "Dienstag",
- "Wednesday" : "Mittwoch",
- "Thursday" : "Donnerstag",
- "Friday" : "Freitag",
- "Saturday" : "Samstag",
- "Sunday" : "Sonntag",
- "Save" : "Speichern",
+ "Weekdays" : "Wochentage",
+ "Pick a start time for {dayName}" : "Eine Startzeit für {dayName} wählen",
+ "Pick a end time for {dayName}" : "Eine Endezeit für {dayName} wählen",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Setzen Sie den Benutzerstatus außerhalb Ihrer Verfügbarkeit automatisch auf \"Nicht stören\", um alle Benachrichtigungen stumm zu schalten.",
+ "Cancel" : "Abbrechen",
+ "Import" : "Importieren",
+ "Error while saving settings" : "Fehler beim Speichern der Einstellungen",
+ "Contact reset successfully" : "Kontakt zurückgesetzt",
+ "Error while resetting contact" : "Fehler beim Zurücksetzen des Kontakts",
+ "Contact imported successfully" : "Kontakt importiert",
+ "Error while importing contact" : "Fehler beim Import des Kontakts",
+ "Import contact" : "Kontakt importieren",
+ "Reset to default" : "Auf Standard zurücksetzen ",
+ "Import contacts" : "Kontakte importieren",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Durch das Importieren einer neuen VCF-Datei wird der vorhandene Standardkontakt gelöscht und durch den neuen ersetzt. Fortsetzen?",
+ "Failed to save example event creation setting" : "Einstellung für die Beispiels-Terminerstellung konnte nicht gespeichert werden",
+ "Failed to upload the example event" : "Der Beispieltermin konnte nicht hochgeladen werden",
+ "Custom example event was saved successfully" : "Benutzerdefinierter Beispieltermin gespeichert",
+ "Failed to delete the custom example event" : "Benutzerdefinierter Beispieltermin konnte nicht gelöscht werden",
+ "Custom example event was deleted successfully" : "Benutzerdefinierter Beispieltermin wurde gelöscht",
+ "Import calendar event" : "Kalendertermin importieren",
+ "Uploading a new event will overwrite the existing one." : "Das Hochladen eines neuen Termins wird den bestehenden Termin überschreiben.",
+ "Upload event" : "Termin hochladen",
+ "Availability" : "Verfügbarkeit",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Wenn Sie Ihre Arbeitszeiten angeben, können andere beim Buchen einer Besprechung sehen, wann Sie nicht im Büro sind.",
+ "Absence" : "Abwesenheit",
+ "Configure your next absence period." : "Richten Sie ihren nächsten Abwesenheitszeitraum ein.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installieren Sie außerdem die {calendarappstoreopen}Kalender-App{linkclose} oder {calendardocopen}verbinden Sie Ihren Desktop & Mobilgerät zur Synchronisierung ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Bitte stellen Sie sicher, dass Sie {emailopen}den E-Mail Server{linkclose} ordnungsgemäß eingerichtet haben.",
"Calendar server" : "Kalender-Server",
"Send invitations to attendees" : "Einladungen an die Teilnehmer versenden",
"Automatically generate a birthday calendar" : "Automatisch einen Kalender für Geburtstage erstellen",
"Birthday calendars will be generated by a background job." : "Kalender für Geburtstage werden von einem Hintergrund-Auftrag erstellt",
"Hence they will not be available immediately after enabling but will show up after some time." : "Die Einträge werden nicht sofort angezeigt. Nach der Aktivierung wird es ein wenig dauern bis zur Anzeige.",
"Send notifications for events" : "Sende Benachrichtigungen für Termine",
- "Notifications are sent via background jobs, so these must occur often enough." : "Benachrichtigungen werden von Hintergrundjobs versendet, so dass diese häufig genug ausgeführt werden müssen.",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Benachrichtigungen werden von Hintergrundaufgaben versendet, so dass diese häufig genug ausgeführt werden müssen.",
"Send reminder notifications to calendar sharees as well" : "Erinnerungsbenachrichtigungen auch an die Kalenderfreigaben senden",
"Reminders are always sent to organizers and attendees." : "Erinnerungen werden immer an Organisatoren und Teilnehmer gesendet.",
"Enable notifications for events via push" : "Benachrichtigungen für Termine per Push aktivieren",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installieren Sie außerdem die {calendarappstoreopen}Kalender-App{linkclose} oder {calendardocopen}verbinden Sie Ihren Desktop & Mobilgerät zur Synchronisierung ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Bitte stellen Sie sicher, dass Sie {emailopen}den E-Mail Server{linkclose} ordnungsgemäß eingerichtet haben.",
+ "Example content" : "Beispielsinhalt",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Beispielinhalte dienen dazu, die Funktionen von Nextcloud vorzustellen. Standardinhalte werden mit Nextcloud ausgeliefert und können durch benutzerdefinierte Inhalte ersetzt werden.",
"There was an error updating your attendance status." : "Es ist ein Fehler beim Aktualisieren Ihres Teilnehmerstatus aufgetreten.",
"Please contact the organizer directly." : "Bitte den Organisator direkt kontaktieren.",
"Are you accepting the invitation?" : "Die Einladung annehmen?",
"Tentative" : "Vorläufig",
- "Number of guests" : "Anzahl Gäste",
- "Comment" : "Kommentar",
- "Your attendance was updated successfully." : "Ihr Teilnehmerstatus wurde aktualisiert.",
- "Calendar and tasks" : "Kalender und Aufgaben"
+ "Your attendance was updated successfully." : "Ihr Teilnehmerstatus wurde aktualisiert."
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/dav/l10n/el.js b/apps/dav/l10n/el.js
deleted file mode 100644
index 40491eafd08..00000000000
--- a/apps/dav/l10n/el.js
+++ /dev/null
@@ -1,119 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Ημερολόγιο",
- "Todos" : "Εργασίες προς εκτέλεση",
- "Personal" : "Προσωπικά",
- "{actor} created calendar {calendar}" : "{actor} δημιουργήθηκε το ημερολόγιο {calendar}",
- "You created calendar {calendar}" : "Δημιουργήσατε ημερολόγιο {ημερολόγιο}",
- "{actor} deleted calendar {calendar}" : "{actor} διέγραψε το ημερολόγιο {calendar}",
- "You deleted calendar {calendar}" : "Διαγράψατε το ημερολόγιο {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} ενημέρωσε το ημερολόγιο {calendar}",
- "You updated calendar {calendar}" : "Έχετε ενημερώσει το ημερολόγιο {calendar}",
- "You shared calendar {calendar} as public link" : "Μοιραστήκατε το ημερολόγιο {calendar} με δημόσιο σύνδεσμο.",
- "You removed public link for calendar {calendar}" : "Αφαιρέσατε τον δημόσιο σύνδεσμο για το ημερολόγιο {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} διαμοιράστηκε το ημερολόγιο {calendar} με εσάς",
- "You shared calendar {calendar} with {user}" : "Διαμοιραστήκατε το ημερολογίου {calendar} με {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} διαμοίρασε το ημερολόγιο {calendar} με {user}",
- "{actor} unshared calendar {calendar} from you" : "Ο {actor} σταμάτησε τον διαμοιρασμό του ημερολογίου {calendar} από εσάς",
- "You unshared calendar {calendar} from {user}" : "Σταματήσατε τον διαμοιρασμό ημερολογίου {calendar} από {user}",
- "{actor} unshared calendar {calendar} from {user}" : "Ο {actor} σταμάτησε τον διαμοιρασμό του ημερολογίου {calendar} από τον χρήστη {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} σταμάτησε το διαμοιρασμένο ημερολόγιο {calendar} από τον εαυτό τους",
- "You shared calendar {calendar} with group {group}" : "Διαμοιραστείκατε ένα ημερολόγιο {calendar} με την ομάδα {group}",
- "{actor} shared calendar {calendar} with group {group}" : "Ο {actor} διαμοιράστηκε το ημερολόγιο {calendar} με την ομάδα {group}",
- "You unshared calendar {calendar} from group {group}" : "Σταματήσατε τον διαμοιρασμό του ημερολογίου {calendar} από την ομάδα {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} σταμάτησε το διαμοιρασμένο ημερολόγιο {calendar} από την ομάδα {group}",
- "{actor} created event {event} in calendar {calendar}" : "Ο {actor} δημιούργησε το γεγονός {event} στο ημερολόγιο {calendar}",
- "You created event {event} in calendar {calendar}" : "Δημιουργήσατε το γεγονός {event} στο ημερολόγιο {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "Ο {actor} διέγραψε το γεγονός {event} από το ημερολόγιο {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Διαγράψατε το συμβάν {event} από το ημερολόγιο {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "Ο {actor} ενημέρωσε το γεγονός {event} στο ημερολόγιο {calendar}",
- "You updated event {event} in calendar {calendar}" : "Ενημερώσατε το συμβάν {event} στο ημερολόγιο {calendar}",
- "Busy" : "Απασχολημένος",
- "{actor} created todo {todo} in list {calendar}" : "{actor} δημιούργησε την εκκρεμότητα {todo} στη λίστα {ημερολόγιο}",
- "You created todo {todo} in list {calendar}" : "Δημιουργήσατε την εκκρεμότητα {todo} στη λίστα {ημερολόγιο}",
- "{actor} deleted todo {todo} from list {calendar}" : "Ο {actor} διέγραψε την εκκρεμότητα {todo} από τη λίστα {ημερολόγιο}",
- "You deleted todo {todo} from list {calendar}" : "Διέγραψες την εκκρεμότητα {todo} από τη λίστα {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} ενημέρωσε την εκκρεμότητα {todo} στη λίστα {calendar}",
- "You updated todo {todo} in list {calendar}" : "Ενημέρωσες την εκκρεμότητα {todo} στη λίστα {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} επίλυσε την εκκρεμότητα {todo} στην λίστα {calendar}",
- "You solved todo {todo} in list {calendar}" : "Επίλυσες την εκκρεμότητα {todo} στην λίστα {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} άνοιξε ξανά την εκκρεμότητα {todo} στην λίστα {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Άνοιξες ξανά την εκκρεμότητα {todo} στην λίστα {calendar}",
- "A <strong>calendar</strong> was modified" : "Τροποποιήθηκε ένα <strong>ημερολόγιο</strong> ",
- "A calendar <strong>event</strong> was modified" : "Τροποποιήθηκε ένα <strong>γεγονός</strong> του ημερολογίου",
- "A calendar <strong>todo</strong> was modified" : "Ενός ημερολογίου η <strong>εκκρεμότητα</strong> τροποποιήθηκε",
- "Contact birthdays" : "Γενέθλια επαφών",
- "Death of %s" : "Θάνατος του %s",
- "Calendar:" : "Ημερολόγιο:",
- "Date:" : "Ημερομηνία:",
- "Where:" : "Που:",
- "Description:" : "Περιγραφή:",
- "Untitled event" : "Συμβάν χωρίς τίτλο",
- "_%n year_::_%n years_" : ["%n χρόνος/χρονιά","%n χρόνια"],
- "_%n month_::_%n months_" : ["%n μήνας","%d μήνες"],
- "_%n day_::_%n days_" : ["%n ημέρα","%n ημέρες"],
- "_%n hour_::_%n hours_" : ["%nώρα","%nώρες"],
- "_%n minute_::_%n minutes_" : ["%n λεπτό","%n λεπτά"],
- "%s (in %s)" : "%s (σε %s)",
- "%s (%s ago)" : "%s (%s πριν)",
- "Calendar: %s" : "Ημερολόγιο:%s",
- "Date: %s" : "Ημερομηνία:%s",
- "Description: %s" : "Περιγραφή:%s",
- "Where: %s" : "Που:%s",
- "%1$s via %2$s" : "%1$s μέσω %2$s",
- "Invitation canceled" : "Η πρόσκληση ακυρώθηκε.",
- "Invitation updated" : "Ενημερώθηκε η πρόσκληση.",
- "Invitation" : "Πρόσκληση",
- "Title:" : "Τίτλος:",
- "Time:" : "Ώρα:",
- "Location:" : "Τοποθεσία:",
- "Link:" : "Σύνδεσμος:",
- "Organizer:" : "Διοργανωτής:",
- "Attendees:" : "Συμμετέχοντες:",
- "Accept" : "Αποδοχή",
- "Decline" : "Απόρριψη",
- "More options …" : "Περισσότερες επιλογές...",
- "More options at %s" : "Περισσότερες επιλογές στο %s",
- "Contacts" : "Επαφές",
- "Upgrade needed" : "Απαιτείται αναβάθμιση",
- "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Το %s θα πρέπει να ρυθμιστεί για να χρησιμοποιεί HTTPS για την χρήση του CalDAV και του CardDAV με το iOS/macOS.",
- "Configures a CalDAV account" : "Ρυθμίσεις λογαριασμού CalDAV",
- "Configures a CardDAV account" : "Ρυθμίσεις λογαριασμού CardDAV",
- "Events" : "Συμβάντα",
- "Tasks" : "Εργασίες",
- "Untitled task" : "Εργασία χωρίς όνομα",
- "Completed on %s" : "Ολοκληρώθηκε %s",
- "Contacts and groups" : "Επαφές και ομάδες",
- "WebDAV" : "WebDAV",
- "WebDAV endpoint" : "Τερματικό WebDAV",
- "Time zone:" : "Ζώνη ώρας:",
- "to" : "προς",
- "Delete slot" : "Διαγραφή θέσης",
- "Monday" : "Δευτέρα",
- "Tuesday" : "Τρίτη",
- "Wednesday" : "Τετάρτη",
- "Thursday" : "Πέμπτη",
- "Friday" : "Παρασκευή",
- "Saturday" : "Σάββατο",
- "Sunday" : "Κυριακή",
- "Save" : "Αποθήκευση",
- "Calendar server" : "Διακομιστής ημερολογίου",
- "Send invitations to attendees" : "Αποστολή προσκλήσεων στους συμμετέχοντες.",
- "Automatically generate a birthday calendar" : "Δημιουργία ημερολογίου γενεθλίων αυτόματα",
- "Birthday calendars will be generated by a background job." : "Τα ημερολόγια γενεθλίων θα δημιουργηθούν από μία εργασία παρασκηνίου.",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Ως εκ τούτου, δεν θα είναι διαθέσιμα αμέσως μετά την ενεργοποίηση, αλλά θα εμφανιστούν μετά από λίγη ώρα.",
- "Send notifications for events" : "Αποστολή ειδοποιήσεων για γεγονότα",
- "Notifications are sent via background jobs, so these must occur often enough." : "Οι ειδοποιήσεις αποστέλλονται μέσω εργασιών παρασκηνίου, οπότε πρέπει να εμφανίζονται αρκετά συχνά.",
- "Enable notifications for events via push" : "Ενεργοποίηση ειδοποιήσεων μέσω push",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Εγκαταστήστε επίσης την {calendarappstoreopen}Εφαρμογή ημερολογίου{linkclose}, ή {calendardocopen}συνδέστε τον υπολογιστή & το κινητό σας για συγχρονισμό ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Παρακαλώ σιγουρευτείτε για την σωστή ρύθμιση {emailopen}του διακομιστή αλληλογραφίας{linkclose}.",
- "There was an error updating your attendance status." : "Σφάλμα ενημέρωσης κατάστασής σας.",
- "Please contact the organizer directly." : "Παρακαλώ επικοινωνήστε απ' ευθείας με τον διοργανωτή.",
- "Are you accepting the invitation?" : "Αποδέχεστε την πρόσκληση;",
- "Tentative" : "Δοκιμαστικό",
- "Comment" : "Σχόλιο",
- "Your attendance was updated successfully." : "Η παρουσία σας ενημερώθηκε με επιτυχία.",
- "Calendar and tasks" : "Ημερολόγιο και εργασίες"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/el.json b/apps/dav/l10n/el.json
deleted file mode 100644
index 2310001e03e..00000000000
--- a/apps/dav/l10n/el.json
+++ /dev/null
@@ -1,117 +0,0 @@
-{ "translations": {
- "Calendar" : "Ημερολόγιο",
- "Todos" : "Εργασίες προς εκτέλεση",
- "Personal" : "Προσωπικά",
- "{actor} created calendar {calendar}" : "{actor} δημιουργήθηκε το ημερολόγιο {calendar}",
- "You created calendar {calendar}" : "Δημιουργήσατε ημερολόγιο {ημερολόγιο}",
- "{actor} deleted calendar {calendar}" : "{actor} διέγραψε το ημερολόγιο {calendar}",
- "You deleted calendar {calendar}" : "Διαγράψατε το ημερολόγιο {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} ενημέρωσε το ημερολόγιο {calendar}",
- "You updated calendar {calendar}" : "Έχετε ενημερώσει το ημερολόγιο {calendar}",
- "You shared calendar {calendar} as public link" : "Μοιραστήκατε το ημερολόγιο {calendar} με δημόσιο σύνδεσμο.",
- "You removed public link for calendar {calendar}" : "Αφαιρέσατε τον δημόσιο σύνδεσμο για το ημερολόγιο {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} διαμοιράστηκε το ημερολόγιο {calendar} με εσάς",
- "You shared calendar {calendar} with {user}" : "Διαμοιραστήκατε το ημερολογίου {calendar} με {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} διαμοίρασε το ημερολόγιο {calendar} με {user}",
- "{actor} unshared calendar {calendar} from you" : "Ο {actor} σταμάτησε τον διαμοιρασμό του ημερολογίου {calendar} από εσάς",
- "You unshared calendar {calendar} from {user}" : "Σταματήσατε τον διαμοιρασμό ημερολογίου {calendar} από {user}",
- "{actor} unshared calendar {calendar} from {user}" : "Ο {actor} σταμάτησε τον διαμοιρασμό του ημερολογίου {calendar} από τον χρήστη {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} σταμάτησε το διαμοιρασμένο ημερολόγιο {calendar} από τον εαυτό τους",
- "You shared calendar {calendar} with group {group}" : "Διαμοιραστείκατε ένα ημερολόγιο {calendar} με την ομάδα {group}",
- "{actor} shared calendar {calendar} with group {group}" : "Ο {actor} διαμοιράστηκε το ημερολόγιο {calendar} με την ομάδα {group}",
- "You unshared calendar {calendar} from group {group}" : "Σταματήσατε τον διαμοιρασμό του ημερολογίου {calendar} από την ομάδα {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} σταμάτησε το διαμοιρασμένο ημερολόγιο {calendar} από την ομάδα {group}",
- "{actor} created event {event} in calendar {calendar}" : "Ο {actor} δημιούργησε το γεγονός {event} στο ημερολόγιο {calendar}",
- "You created event {event} in calendar {calendar}" : "Δημιουργήσατε το γεγονός {event} στο ημερολόγιο {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "Ο {actor} διέγραψε το γεγονός {event} από το ημερολόγιο {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Διαγράψατε το συμβάν {event} από το ημερολόγιο {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "Ο {actor} ενημέρωσε το γεγονός {event} στο ημερολόγιο {calendar}",
- "You updated event {event} in calendar {calendar}" : "Ενημερώσατε το συμβάν {event} στο ημερολόγιο {calendar}",
- "Busy" : "Απασχολημένος",
- "{actor} created todo {todo} in list {calendar}" : "{actor} δημιούργησε την εκκρεμότητα {todo} στη λίστα {ημερολόγιο}",
- "You created todo {todo} in list {calendar}" : "Δημιουργήσατε την εκκρεμότητα {todo} στη λίστα {ημερολόγιο}",
- "{actor} deleted todo {todo} from list {calendar}" : "Ο {actor} διέγραψε την εκκρεμότητα {todo} από τη λίστα {ημερολόγιο}",
- "You deleted todo {todo} from list {calendar}" : "Διέγραψες την εκκρεμότητα {todo} από τη λίστα {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} ενημέρωσε την εκκρεμότητα {todo} στη λίστα {calendar}",
- "You updated todo {todo} in list {calendar}" : "Ενημέρωσες την εκκρεμότητα {todo} στη λίστα {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} επίλυσε την εκκρεμότητα {todo} στην λίστα {calendar}",
- "You solved todo {todo} in list {calendar}" : "Επίλυσες την εκκρεμότητα {todo} στην λίστα {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} άνοιξε ξανά την εκκρεμότητα {todo} στην λίστα {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Άνοιξες ξανά την εκκρεμότητα {todo} στην λίστα {calendar}",
- "A <strong>calendar</strong> was modified" : "Τροποποιήθηκε ένα <strong>ημερολόγιο</strong> ",
- "A calendar <strong>event</strong> was modified" : "Τροποποιήθηκε ένα <strong>γεγονός</strong> του ημερολογίου",
- "A calendar <strong>todo</strong> was modified" : "Ενός ημερολογίου η <strong>εκκρεμότητα</strong> τροποποιήθηκε",
- "Contact birthdays" : "Γενέθλια επαφών",
- "Death of %s" : "Θάνατος του %s",
- "Calendar:" : "Ημερολόγιο:",
- "Date:" : "Ημερομηνία:",
- "Where:" : "Που:",
- "Description:" : "Περιγραφή:",
- "Untitled event" : "Συμβάν χωρίς τίτλο",
- "_%n year_::_%n years_" : ["%n χρόνος/χρονιά","%n χρόνια"],
- "_%n month_::_%n months_" : ["%n μήνας","%d μήνες"],
- "_%n day_::_%n days_" : ["%n ημέρα","%n ημέρες"],
- "_%n hour_::_%n hours_" : ["%nώρα","%nώρες"],
- "_%n minute_::_%n minutes_" : ["%n λεπτό","%n λεπτά"],
- "%s (in %s)" : "%s (σε %s)",
- "%s (%s ago)" : "%s (%s πριν)",
- "Calendar: %s" : "Ημερολόγιο:%s",
- "Date: %s" : "Ημερομηνία:%s",
- "Description: %s" : "Περιγραφή:%s",
- "Where: %s" : "Που:%s",
- "%1$s via %2$s" : "%1$s μέσω %2$s",
- "Invitation canceled" : "Η πρόσκληση ακυρώθηκε.",
- "Invitation updated" : "Ενημερώθηκε η πρόσκληση.",
- "Invitation" : "Πρόσκληση",
- "Title:" : "Τίτλος:",
- "Time:" : "Ώρα:",
- "Location:" : "Τοποθεσία:",
- "Link:" : "Σύνδεσμος:",
- "Organizer:" : "Διοργανωτής:",
- "Attendees:" : "Συμμετέχοντες:",
- "Accept" : "Αποδοχή",
- "Decline" : "Απόρριψη",
- "More options …" : "Περισσότερες επιλογές...",
- "More options at %s" : "Περισσότερες επιλογές στο %s",
- "Contacts" : "Επαφές",
- "Upgrade needed" : "Απαιτείται αναβάθμιση",
- "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Το %s θα πρέπει να ρυθμιστεί για να χρησιμοποιεί HTTPS για την χρήση του CalDAV και του CardDAV με το iOS/macOS.",
- "Configures a CalDAV account" : "Ρυθμίσεις λογαριασμού CalDAV",
- "Configures a CardDAV account" : "Ρυθμίσεις λογαριασμού CardDAV",
- "Events" : "Συμβάντα",
- "Tasks" : "Εργασίες",
- "Untitled task" : "Εργασία χωρίς όνομα",
- "Completed on %s" : "Ολοκληρώθηκε %s",
- "Contacts and groups" : "Επαφές και ομάδες",
- "WebDAV" : "WebDAV",
- "WebDAV endpoint" : "Τερματικό WebDAV",
- "Time zone:" : "Ζώνη ώρας:",
- "to" : "προς",
- "Delete slot" : "Διαγραφή θέσης",
- "Monday" : "Δευτέρα",
- "Tuesday" : "Τρίτη",
- "Wednesday" : "Τετάρτη",
- "Thursday" : "Πέμπτη",
- "Friday" : "Παρασκευή",
- "Saturday" : "Σάββατο",
- "Sunday" : "Κυριακή",
- "Save" : "Αποθήκευση",
- "Calendar server" : "Διακομιστής ημερολογίου",
- "Send invitations to attendees" : "Αποστολή προσκλήσεων στους συμμετέχοντες.",
- "Automatically generate a birthday calendar" : "Δημιουργία ημερολογίου γενεθλίων αυτόματα",
- "Birthday calendars will be generated by a background job." : "Τα ημερολόγια γενεθλίων θα δημιουργηθούν από μία εργασία παρασκηνίου.",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Ως εκ τούτου, δεν θα είναι διαθέσιμα αμέσως μετά την ενεργοποίηση, αλλά θα εμφανιστούν μετά από λίγη ώρα.",
- "Send notifications for events" : "Αποστολή ειδοποιήσεων για γεγονότα",
- "Notifications are sent via background jobs, so these must occur often enough." : "Οι ειδοποιήσεις αποστέλλονται μέσω εργασιών παρασκηνίου, οπότε πρέπει να εμφανίζονται αρκετά συχνά.",
- "Enable notifications for events via push" : "Ενεργοποίηση ειδοποιήσεων μέσω push",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Εγκαταστήστε επίσης την {calendarappstoreopen}Εφαρμογή ημερολογίου{linkclose}, ή {calendardocopen}συνδέστε τον υπολογιστή & το κινητό σας για συγχρονισμό ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Παρακαλώ σιγουρευτείτε για την σωστή ρύθμιση {emailopen}του διακομιστή αλληλογραφίας{linkclose}.",
- "There was an error updating your attendance status." : "Σφάλμα ενημέρωσης κατάστασής σας.",
- "Please contact the organizer directly." : "Παρακαλώ επικοινωνήστε απ' ευθείας με τον διοργανωτή.",
- "Are you accepting the invitation?" : "Αποδέχεστε την πρόσκληση;",
- "Tentative" : "Δοκιμαστικό",
- "Comment" : "Σχόλιο",
- "Your attendance was updated successfully." : "Η παρουσία σας ενημερώθηκε με επιτυχία.",
- "Calendar and tasks" : "Ημερολόγιο και εργασίες"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/en_GB.js b/apps/dav/l10n/en_GB.js
index 11229abe9f4..1ca2aaafee7 100644
--- a/apps/dav/l10n/en_GB.js
+++ b/apps/dav/l10n/en_GB.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Calendar",
- "Todos" : "Todos",
+ "Tasks" : "Tasks",
"Personal" : "Personal",
"{actor} created calendar {calendar}" : "{actor} created calendar {calendar}",
"You created calendar {calendar}" : "You created calendar {calendar}",
@@ -10,6 +10,8 @@ OC.L10N.register(
"You deleted calendar {calendar}" : "You deleted calendar {calendar}",
"{actor} updated calendar {calendar}" : "{actor} updated calendar {calendar}",
"You updated calendar {calendar}" : "You updated calendar {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} restored calendar {calendar}",
+ "You restored calendar {calendar}" : "You restored calendar {calendar}",
"You shared calendar {calendar} as public link" : "You shared calendar {calendar} as public link",
"You removed public link for calendar {calendar}" : "You removed public link for calendar {calendar}",
"{actor} shared calendar {calendar} with you" : "{actor} shared calendar {calendar} with you",
@@ -23,47 +25,314 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} shared calendar {calendar} with group {group}",
"You unshared calendar {calendar} from group {group}" : "You unshared calendar {calendar} from group {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} unshared calendar {calendar} from group {group}",
+ "Untitled event" : "Untitled event",
"{actor} created event {event} in calendar {calendar}" : "{actor} created event {event} in calendar {calendar}",
"You created event {event} in calendar {calendar}" : "You created event {event} in calendar {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} deleted event {event} from calendar {calendar}",
"You deleted event {event} from calendar {calendar}" : "You deleted event {event} from calendar {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} updated event {event} in calendar {calendar}",
"You updated event {event} in calendar {calendar}" : "You updated event {event} in calendar {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} restored event {event} of calendar {calendar}",
+ "You restored event {event} of calendar {calendar}" : "You restored event {event} of calendar {calendar}",
"Busy" : "Busy",
- "{actor} created todo {todo} in list {calendar}" : "{actor} created todo {todo} in list {calendar}",
- "You created todo {todo} in list {calendar}" : "You created todo {todo} in list {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} deleted todo {todo} from list {calendar}",
- "You deleted todo {todo} from list {calendar}" : "You deleted todo {todo} from list {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} updated todo {todo} in list {calendar}",
- "You updated todo {todo} in list {calendar}" : "You updated todo {todo} in list {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} solved todo {todo} in list {calendar}",
- "You solved todo {todo} in list {calendar}" : "You solved todo {todo} in list {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reopened todo {todo} in list {calendar}",
- "You reopened todo {todo} in list {calendar}" : "You reopened todo {todo} in list {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} created to-do {todo} in list {calendar}",
+ "You created to-do {todo} in list {calendar}" : "You created to-do {todo} in list {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} deleted to-do {todo} from list {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "You deleted to-do {todo} from list {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} updated to-do {todo} in list {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "You updated to-do {todo} in list {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} solved to-do {todo} in list {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "You solved to-do {todo} in list {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} reopened to-do {todo} in list {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "You reopened to-do {todo} in list {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}",
+ "Calendar, contacts and tasks" : "Calendar, contacts and tasks",
"A <strong>calendar</strong> was modified" : "A <strong>calendar</strong> was modified",
"A calendar <strong>event</strong> was modified" : "A calendar <strong>event</strong> was modified",
- "A calendar <strong>todo</strong> was modified" : "A calendar <strong>todo</strong> was modified",
+ "A calendar <strong>to-do</strong> was modified" : "A calendar <strong>to-do</strong> was modified",
"Contact birthdays" : "Contact birthdays",
+ "Death of %s" : "Death of %s",
+ "Untitled calendar" : "Untitled calendar",
+ "Calendar:" : "Calendar:",
+ "Date:" : "Date:",
"Where:" : "Where:",
"Description:" : "Description:",
+ "_%n year_::_%n years_" : ["%n year","%n years"],
+ "_%n month_::_%n months_" : ["%n month","%n months"],
+ "_%n day_::_%n days_" : ["%n day","%n days"],
+ "_%n hour_::_%n hours_" : ["%n hour","%n hours"],
+ "_%n minute_::_%n minutes_" : ["%n minute","%n minutes"],
+ "%s (in %s)" : "%s (in %s)",
+ "%s (%s ago)" : "%s (%s ago)",
+ "Calendar: %s" : "Calendar: %s",
+ "Date: %s" : "Date: %s",
+ "Description: %s" : "Description: %s",
+ "Where: %s" : "Where: %s",
"%1$s via %2$s" : "%1$s via %2$s",
- "Invitation canceled" : "Invitation canceled",
- "Invitation updated" : "Invitation updated",
+ "In the past on %1$s for the entire day" : "In the past on %1$s for the entire day",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["In a minute on %1$s for the entire day","In %n minutes on %1$s for the entire day"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["In a hour on %1$s for the entire day","In %n hours on %1$s for the entire day"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["In a day on %1$s for the entire day","In %n days on %1$s for the entire day"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["In a week on %1$s for the entire day","In %n weeks on %1$s for the entire day"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["In a month on %1$s for the entire day","In %n months on %1$s for the entire day"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["In a year on %1$s for the entire day","In %n years on %1$s for the entire day"],
+ "In the past on %1$s between %2$s - %3$s" : "In the past on %1$s between %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["In a minute on %1$s between %2$s - %3$s","In %n minutes on %1$s between %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["In a hour on %1$s between %2$s - %3$s","In %n hours on %1$s between %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["In a day on %1$s between %2$s - %3$s","In %n days on %1$s between %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["In a week on %1$s between %2$s - %3$s","In %n weeks on %1$s between %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["In a month on %1$s between %2$s - %3$s","In %n months on %1$s between %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["In a year on %1$s between %2$s - %3$s","In %n years on %1$s between %2$s - %3$s"],
+ "Could not generate when statement" : "Could not generate when statement",
+ "Every Day for the entire day" : "Every Day for the entire day",
+ "Every Day for the entire day until %1$s" : "Every Day for the entire day until %1$s",
+ "Every Day between %1$s - %2$s" : "Every Day between %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Every Day between %1$s - %2$s until %3$s",
+ "Every %1$d Days for the entire day" : "Every %1$d Days for the entire day",
+ "Every %1$d Days for the entire day until %2$s" : "Every %1$d Days for the entire day until %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Every %1$d Days between %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Every %1$d Days between %2$s - %3$s until %4$s",
+ "Could not generate event recurrence statement" : "Could not generate event recurrence statement",
+ "Every Week on %1$s for the entire day" : "Every Week on %1$s for the entire day",
+ "Every Week on %1$s for the entire day until %2$s" : "Every Week on %1$s for the entire day until %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Every Week on %1$s between %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Every Week on %1$s between %2$s - %3$s until %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Every %1$d Weeks on %2$s for the entire day",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Every %1$d Weeks on %2$s for the entire day until %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Every %1$d Weeks on %2$s between %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s",
+ "Every Month on the %1$s for the entire day" : "Every Month on the %1$s for the entire day",
+ "Every Month on the %1$s for the entire day until %2$s" : "Every Month on the %1$s for the entire day until %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Every Month on the %1$s between %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Every Month on the %1$s between %2$s - %3$s until %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Every %1$d Months on the %2$s for the entire day",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Every %1$d Months on the %2$s for the entire day until %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Every %1$d Months on the %2$s between %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Every Year in %1$s on the %2$s for the entire day",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Every Year in %1$s on the %2$s for the entire day until %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Every Year in %1$s on the %2$s between %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Every %1$d Years in %2$s on the %3$s for the entire day",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s",
+ "On specific dates for the entire day until %1$s" : "On specific dates for the entire day until %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "On specific dates between %1$s - %2$s until %3$s",
+ "In the past on %1$s" : "In the past on %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["In a minute on %1$s","In %n minutes on %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["In a hour on %1$s","In %n hours on %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["In a day on %1$s","In %n days on %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["In a week on %1$s","In %n weeks on %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["In a month on %1$s","In %n months on %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["In a year on %1$s","In %n years on %1$s"],
+ "In the past on %1$s then on %2$s" : "In the past on %1$s then on %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["In a minute on %1$s then on %2$s","In %n minutes on %1$s then on %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["In a hour on %1$s then on %2$s","In %n hours on %1$s then on %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["In a day on %1$s then on %2$s","In %n days on %1$s then on %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["In a week on %1$s then on %2$s","In %n weeks on %1$s then on %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["In a month on %1$s then on %2$s","In %n months on %1$s then on %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["In a year on %1$s then on %2$s","In %n years on %1$s then on %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "In the past on %1$s then on %2$s and %3$s",
+ "_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_" : ["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"],
+ "_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_" : ["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"],
+ "_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_" : ["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"],
+ "_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_" : ["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"],
+ "_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_" : ["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"],
+ "_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_" : ["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"],
+ "Could not generate next recurrence statement" : "Could not generate next recurrence statement",
+ "Cancelled: %1$s" : "Cancelled: %1$s",
+ "\"%1$s\" has been canceled" : "\"%1$s\" has been cancelled",
+ "Re: %1$s" : "Re: %1$s",
+ "%1$s has accepted your invitation" : "%1$s has accepted your invitation",
+ "%1$s has tentatively accepted your invitation" : "%1$s has tentatively accepted your invitation",
+ "%1$s has declined your invitation" : "%1$s has declined your invitation",
+ "%1$s has responded to your invitation" : "%1$s has responded to your invitation",
+ "Invitation updated: %1$s" : "Invitation updated: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s updated the event \"%2$s\"",
+ "Invitation: %1$s" : "Invitation: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s would like to invite you to \"%2$s\"",
+ "Organizer:" : "Organiser:",
+ "Attendees:" : "Attendees:",
+ "Title:" : "Title:",
+ "When:" : "When:",
"Location:" : "Location:",
"Link:" : "Link:",
+ "Occurring:" : "Occurring:",
"Accept" : "Accept",
"Decline" : "Decline",
+ "More options …" : "More options …",
+ "More options at %s" : "More options at %s",
+ "Monday" : "Monday",
+ "Tuesday" : "Tuesday",
+ "Wednesday" : "Wednesday",
+ "Thursday" : "Thursday",
+ "Friday" : "Friday",
+ "Saturday" : "Saturday",
+ "Sunday" : "Sunday",
+ "January" : "January",
+ "February" : "February",
+ "March" : "March",
+ "April" : "April",
+ "May" : "May",
+ "June" : "June",
+ "July" : "July",
+ "August" : "August",
+ "September" : "September",
+ "October" : "October",
+ "November" : "November",
+ "December" : "December",
+ "First" : "First",
+ "Second" : "Second",
+ "Third" : "Third",
+ "Fourth" : "Fourth",
+ "Fifth" : "Fifth",
+ "Last" : "Last",
+ "Second Last" : "Second Last",
+ "Third Last" : "Third Last",
+ "Fourth Last" : "Fourth Last",
+ "Fifth Last" : "Fifth Last",
"Contacts" : "Contacts",
- "Tasks" : "Tasks",
- "WebDAV" : "WebDAV",
+ "{actor} created address book {addressbook}" : "{actor} created address book {addressbook}",
+ "You created address book {addressbook}" : "You created address book {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} deleted address book {addressbook}",
+ "You deleted address book {addressbook}" : "You deleted address book {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} updated address book {addressbook}",
+ "You updated address book {addressbook}" : "You updated address book {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} shared address book {addressbook} with you",
+ "You shared address book {addressbook} with {user}" : "You shared address book {addressbook} with {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} shared address book {addressbook} with {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} unshared address book {addressbook} from you",
+ "You unshared address book {addressbook} from {user}" : "You unshared address book {addressbook} from {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} unshared address book {addressbook} from {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} unshared address book {addressbook} from themselves",
+ "You shared address book {addressbook} with group {group}" : "You shared address book {addressbook} with group {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} shared address book {addressbook} with group {group}",
+ "You unshared address book {addressbook} from group {group}" : "You unshared address book {addressbook} from group {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} unshared address book {addressbook} from group {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} created contact {card} in address book {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "You created contact {card} in address book {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} deleted contact {card} from address book {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "You deleted contact {card} from address book {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} updated contact {card} in address book {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "You updated contact {card} in address book {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "A <strong>contact</strong> or <strong>address book</strong> was modified",
+ "Accounts" : "Accounts",
+ "System address book which holds all accounts" : "System address book which holds all accounts",
+ "File is not updatable: %1$s" : "File is not updatable: %1$s",
+ "Failed to get storage for file" : "Failed to get storage for file",
+ "Could not write to final file, canceled by hook" : "Could not write to final file, cancelled by hook",
+ "Could not write file contents" : "Could not write file contents",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)",
+ "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." : "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.",
+ "Could not rename part file to final file, canceled by hook" : "Could not rename part file to final file, cancelled by hook",
+ "Could not rename part file to final file" : "Could not rename part file to final file",
+ "Failed to check file size: %1$s" : "Failed to check file size: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "Could not open file: %1$s, file does seem to exist",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Could not open file: %1$s, file doesn't seem to exist",
+ "Encryption not ready: %1$s" : "Encryption not ready: %1$s",
+ "Failed to open file: %1$s" : "Failed to open file: %1$s",
+ "Failed to unlink: %1$s" : "Failed to unlink: %1$s",
+ "Failed to write file contents: %1$s" : "Failed to write file contents: %1$s",
+ "File not found: %1$s" : "File not found: %1$s",
+ "Invalid target path" : "Invalid target path",
+ "System is in maintenance mode." : "System is in maintenance mode.",
+ "Upgrade needed" : "Upgrade needed",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS.",
+ "Configures a CalDAV account" : "Configures a CalDAV account",
+ "Configures a CardDAV account" : "Configures a CardDAV account",
+ "Events" : "Events",
+ "Untitled task" : "Untitled task",
+ "Completed on %s" : "Completed on %s",
+ "Due on %s by %s" : "Due on %s by %s",
+ "Due on %s" : "Due on %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings.",
+ "Example event - open me!" : "Example event - open me!",
+ "System Address Book" : "System Address Book",
+ "The system address book contains contact information for all users in your instance." : "The system address book contains contact information for all users in your instance.",
+ "Enable System Address Book" : "Enable System Address Book",
+ "DAV system address book" : "DAV system address book",
+ "No outstanding DAV system address book sync." : "No outstanding DAV system address book sync.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\".",
"WebDAV endpoint" : "WebDAV endpoint",
- "Tentative" : "Tentative",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Your web server is not yet properly set up to allow file synchronisation, because the WebDAV interface seems to be broken.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Your web server is properly set up to allow file synchronization over WebDAV.",
+ "Migrated calendar (%1$s)" : "Migrated calendar (%1$s)",
+ "Calendars including events, details and attendees" : "Calendars including events, details and attendees",
+ "Contacts and groups" : "Contacts and groups",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "Absence saved",
+ "Failed to save your absence settings" : "Failed to save your absence settings",
+ "Absence cleared" : "Absence cleared",
+ "Failed to clear your absence settings" : "Failed to clear your absence settings",
+ "First day" : "First day",
+ "Last day (inclusive)" : "Last day (inclusive)",
+ "Out of office replacement (optional)" : "Out of office replacement (optional)",
+ "Name of the replacement" : "Name of the replacement",
+ "No results." : "No results.",
+ "Start typing." : "Start typing.",
+ "Short absence status" : "Short absence status",
+ "Long absence Message" : "Long absence Message",
"Save" : "Save",
+ "Disable absence" : "Disable absence",
+ "Failed to load availability" : "Failed to load availability",
+ "Saved availability" : "Saved availability",
+ "Failed to save availability" : "Failed to save availability",
+ "Time zone:" : "Time zone:",
+ "to" : "to",
+ "Delete slot" : "Delete slot",
+ "No working hours set" : "No working hours set",
+ "Add slot" : "Add slot",
+ "Weekdays" : "Weekdays",
+ "Pick a start time for {dayName}" : "Pick a start time for {dayName}",
+ "Pick a end time for {dayName}" : "Pick a end time for {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications.",
+ "Cancel" : "Cancel",
+ "Import" : "Import",
+ "Error while saving settings" : "Error while saving settings",
+ "Contact reset successfully" : "Contact reset successfully",
+ "Error while resetting contact" : "Error while resetting contact",
+ "Contact imported successfully" : "Contact imported successfully",
+ "Error while importing contact" : "Error while importing contact",
+ "Import contact" : "Import contact",
+ "Reset to default" : "Reset to default",
+ "Import contacts" : "Import contacts",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?",
+ "Failed to save example event creation setting" : "Failed to save example event creation setting",
+ "Failed to upload the example event" : "Failed to upload the example event",
+ "Custom example event was saved successfully" : "Custom example event was saved successfully",
+ "Failed to delete the custom example event" : "Failed to delete the custom example event",
+ "Custom example event was deleted successfully" : "Custom example event was deleted successfully",
+ "Import calendar event" : "Import calendar event",
+ "Uploading a new event will overwrite the existing one." : "Uploading a new event will overwrite the existing one.",
+ "Upload event" : "Upload event",
+ "Availability" : "Availability",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "If you configure your working hours, other people will see when you are out of office when they book a meeting.",
+ "Absence" : "Absence",
+ "Configure your next absence period." : "Configure your next absence period.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Please make sure to properly set up {emailopen}the email server{linkclose}.",
+ "Calendar server" : "Calendar server",
"Send invitations to attendees" : "Send invitations to attendees",
"Automatically generate a birthday calendar" : "Automatically generate a birthday calendar",
"Birthday calendars will be generated by a background job." : "Birthday calendars will be generated by a background job.",
"Hence they will not be available immediately after enabling but will show up after some time." : "Hence they will not be available immediately after enabling but will show up after some time.",
- "Hello %s," : "Hello %s,",
- "When:" : "When:"
+ "Send notifications for events" : "Send notifications for events",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Notifications are sent via background jobs, so these must occur often enough.",
+ "Send reminder notifications to calendar sharees as well" : "Send reminder notifications to calendar sharees as well",
+ "Reminders are always sent to organizers and attendees." : "Reminders are always sent to organisers and attendees.",
+ "Enable notifications for events via push" : "Enable notifications for events via push",
+ "Example content" : "Example content",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content.",
+ "There was an error updating your attendance status." : "There was an error updating your attendance status.",
+ "Please contact the organizer directly." : "Please contact the organiser directly.",
+ "Are you accepting the invitation?" : "Are you accepting the invitation?",
+ "Tentative" : "Tentative",
+ "Your attendance was updated successfully." : "Your attendance was updated successfully."
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/en_GB.json b/apps/dav/l10n/en_GB.json
index 2acf739fa33..5d2955e7995 100644
--- a/apps/dav/l10n/en_GB.json
+++ b/apps/dav/l10n/en_GB.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Calendar",
- "Todos" : "Todos",
+ "Tasks" : "Tasks",
"Personal" : "Personal",
"{actor} created calendar {calendar}" : "{actor} created calendar {calendar}",
"You created calendar {calendar}" : "You created calendar {calendar}",
@@ -8,6 +8,8 @@
"You deleted calendar {calendar}" : "You deleted calendar {calendar}",
"{actor} updated calendar {calendar}" : "{actor} updated calendar {calendar}",
"You updated calendar {calendar}" : "You updated calendar {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} restored calendar {calendar}",
+ "You restored calendar {calendar}" : "You restored calendar {calendar}",
"You shared calendar {calendar} as public link" : "You shared calendar {calendar} as public link",
"You removed public link for calendar {calendar}" : "You removed public link for calendar {calendar}",
"{actor} shared calendar {calendar} with you" : "{actor} shared calendar {calendar} with you",
@@ -21,47 +23,314 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} shared calendar {calendar} with group {group}",
"You unshared calendar {calendar} from group {group}" : "You unshared calendar {calendar} from group {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} unshared calendar {calendar} from group {group}",
+ "Untitled event" : "Untitled event",
"{actor} created event {event} in calendar {calendar}" : "{actor} created event {event} in calendar {calendar}",
"You created event {event} in calendar {calendar}" : "You created event {event} in calendar {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} deleted event {event} from calendar {calendar}",
"You deleted event {event} from calendar {calendar}" : "You deleted event {event} from calendar {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} updated event {event} in calendar {calendar}",
"You updated event {event} in calendar {calendar}" : "You updated event {event} in calendar {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} restored event {event} of calendar {calendar}",
+ "You restored event {event} of calendar {calendar}" : "You restored event {event} of calendar {calendar}",
"Busy" : "Busy",
- "{actor} created todo {todo} in list {calendar}" : "{actor} created todo {todo} in list {calendar}",
- "You created todo {todo} in list {calendar}" : "You created todo {todo} in list {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} deleted todo {todo} from list {calendar}",
- "You deleted todo {todo} from list {calendar}" : "You deleted todo {todo} from list {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} updated todo {todo} in list {calendar}",
- "You updated todo {todo} in list {calendar}" : "You updated todo {todo} in list {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} solved todo {todo} in list {calendar}",
- "You solved todo {todo} in list {calendar}" : "You solved todo {todo} in list {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reopened todo {todo} in list {calendar}",
- "You reopened todo {todo} in list {calendar}" : "You reopened todo {todo} in list {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} created to-do {todo} in list {calendar}",
+ "You created to-do {todo} in list {calendar}" : "You created to-do {todo} in list {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} deleted to-do {todo} from list {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "You deleted to-do {todo} from list {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} updated to-do {todo} in list {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "You updated to-do {todo} in list {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} solved to-do {todo} in list {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "You solved to-do {todo} in list {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} reopened to-do {todo} in list {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "You reopened to-do {todo} in list {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}",
+ "Calendar, contacts and tasks" : "Calendar, contacts and tasks",
"A <strong>calendar</strong> was modified" : "A <strong>calendar</strong> was modified",
"A calendar <strong>event</strong> was modified" : "A calendar <strong>event</strong> was modified",
- "A calendar <strong>todo</strong> was modified" : "A calendar <strong>todo</strong> was modified",
+ "A calendar <strong>to-do</strong> was modified" : "A calendar <strong>to-do</strong> was modified",
"Contact birthdays" : "Contact birthdays",
+ "Death of %s" : "Death of %s",
+ "Untitled calendar" : "Untitled calendar",
+ "Calendar:" : "Calendar:",
+ "Date:" : "Date:",
"Where:" : "Where:",
"Description:" : "Description:",
+ "_%n year_::_%n years_" : ["%n year","%n years"],
+ "_%n month_::_%n months_" : ["%n month","%n months"],
+ "_%n day_::_%n days_" : ["%n day","%n days"],
+ "_%n hour_::_%n hours_" : ["%n hour","%n hours"],
+ "_%n minute_::_%n minutes_" : ["%n minute","%n minutes"],
+ "%s (in %s)" : "%s (in %s)",
+ "%s (%s ago)" : "%s (%s ago)",
+ "Calendar: %s" : "Calendar: %s",
+ "Date: %s" : "Date: %s",
+ "Description: %s" : "Description: %s",
+ "Where: %s" : "Where: %s",
"%1$s via %2$s" : "%1$s via %2$s",
- "Invitation canceled" : "Invitation canceled",
- "Invitation updated" : "Invitation updated",
+ "In the past on %1$s for the entire day" : "In the past on %1$s for the entire day",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["In a minute on %1$s for the entire day","In %n minutes on %1$s for the entire day"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["In a hour on %1$s for the entire day","In %n hours on %1$s for the entire day"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["In a day on %1$s for the entire day","In %n days on %1$s for the entire day"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["In a week on %1$s for the entire day","In %n weeks on %1$s for the entire day"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["In a month on %1$s for the entire day","In %n months on %1$s for the entire day"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["In a year on %1$s for the entire day","In %n years on %1$s for the entire day"],
+ "In the past on %1$s between %2$s - %3$s" : "In the past on %1$s between %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["In a minute on %1$s between %2$s - %3$s","In %n minutes on %1$s between %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["In a hour on %1$s between %2$s - %3$s","In %n hours on %1$s between %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["In a day on %1$s between %2$s - %3$s","In %n days on %1$s between %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["In a week on %1$s between %2$s - %3$s","In %n weeks on %1$s between %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["In a month on %1$s between %2$s - %3$s","In %n months on %1$s between %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["In a year on %1$s between %2$s - %3$s","In %n years on %1$s between %2$s - %3$s"],
+ "Could not generate when statement" : "Could not generate when statement",
+ "Every Day for the entire day" : "Every Day for the entire day",
+ "Every Day for the entire day until %1$s" : "Every Day for the entire day until %1$s",
+ "Every Day between %1$s - %2$s" : "Every Day between %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Every Day between %1$s - %2$s until %3$s",
+ "Every %1$d Days for the entire day" : "Every %1$d Days for the entire day",
+ "Every %1$d Days for the entire day until %2$s" : "Every %1$d Days for the entire day until %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Every %1$d Days between %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Every %1$d Days between %2$s - %3$s until %4$s",
+ "Could not generate event recurrence statement" : "Could not generate event recurrence statement",
+ "Every Week on %1$s for the entire day" : "Every Week on %1$s for the entire day",
+ "Every Week on %1$s for the entire day until %2$s" : "Every Week on %1$s for the entire day until %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Every Week on %1$s between %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Every Week on %1$s between %2$s - %3$s until %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Every %1$d Weeks on %2$s for the entire day",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Every %1$d Weeks on %2$s for the entire day until %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Every %1$d Weeks on %2$s between %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s",
+ "Every Month on the %1$s for the entire day" : "Every Month on the %1$s for the entire day",
+ "Every Month on the %1$s for the entire day until %2$s" : "Every Month on the %1$s for the entire day until %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Every Month on the %1$s between %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Every Month on the %1$s between %2$s - %3$s until %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Every %1$d Months on the %2$s for the entire day",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Every %1$d Months on the %2$s for the entire day until %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Every %1$d Months on the %2$s between %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Every Year in %1$s on the %2$s for the entire day",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Every Year in %1$s on the %2$s for the entire day until %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Every Year in %1$s on the %2$s between %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Every %1$d Years in %2$s on the %3$s for the entire day",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s",
+ "On specific dates for the entire day until %1$s" : "On specific dates for the entire day until %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "On specific dates between %1$s - %2$s until %3$s",
+ "In the past on %1$s" : "In the past on %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["In a minute on %1$s","In %n minutes on %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["In a hour on %1$s","In %n hours on %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["In a day on %1$s","In %n days on %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["In a week on %1$s","In %n weeks on %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["In a month on %1$s","In %n months on %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["In a year on %1$s","In %n years on %1$s"],
+ "In the past on %1$s then on %2$s" : "In the past on %1$s then on %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["In a minute on %1$s then on %2$s","In %n minutes on %1$s then on %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["In a hour on %1$s then on %2$s","In %n hours on %1$s then on %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["In a day on %1$s then on %2$s","In %n days on %1$s then on %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["In a week on %1$s then on %2$s","In %n weeks on %1$s then on %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["In a month on %1$s then on %2$s","In %n months on %1$s then on %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["In a year on %1$s then on %2$s","In %n years on %1$s then on %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "In the past on %1$s then on %2$s and %3$s",
+ "_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_" : ["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"],
+ "_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_" : ["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"],
+ "_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_" : ["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"],
+ "_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_" : ["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"],
+ "_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_" : ["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"],
+ "_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_" : ["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"],
+ "Could not generate next recurrence statement" : "Could not generate next recurrence statement",
+ "Cancelled: %1$s" : "Cancelled: %1$s",
+ "\"%1$s\" has been canceled" : "\"%1$s\" has been cancelled",
+ "Re: %1$s" : "Re: %1$s",
+ "%1$s has accepted your invitation" : "%1$s has accepted your invitation",
+ "%1$s has tentatively accepted your invitation" : "%1$s has tentatively accepted your invitation",
+ "%1$s has declined your invitation" : "%1$s has declined your invitation",
+ "%1$s has responded to your invitation" : "%1$s has responded to your invitation",
+ "Invitation updated: %1$s" : "Invitation updated: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s updated the event \"%2$s\"",
+ "Invitation: %1$s" : "Invitation: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s would like to invite you to \"%2$s\"",
+ "Organizer:" : "Organiser:",
+ "Attendees:" : "Attendees:",
+ "Title:" : "Title:",
+ "When:" : "When:",
"Location:" : "Location:",
"Link:" : "Link:",
+ "Occurring:" : "Occurring:",
"Accept" : "Accept",
"Decline" : "Decline",
+ "More options …" : "More options …",
+ "More options at %s" : "More options at %s",
+ "Monday" : "Monday",
+ "Tuesday" : "Tuesday",
+ "Wednesday" : "Wednesday",
+ "Thursday" : "Thursday",
+ "Friday" : "Friday",
+ "Saturday" : "Saturday",
+ "Sunday" : "Sunday",
+ "January" : "January",
+ "February" : "February",
+ "March" : "March",
+ "April" : "April",
+ "May" : "May",
+ "June" : "June",
+ "July" : "July",
+ "August" : "August",
+ "September" : "September",
+ "October" : "October",
+ "November" : "November",
+ "December" : "December",
+ "First" : "First",
+ "Second" : "Second",
+ "Third" : "Third",
+ "Fourth" : "Fourth",
+ "Fifth" : "Fifth",
+ "Last" : "Last",
+ "Second Last" : "Second Last",
+ "Third Last" : "Third Last",
+ "Fourth Last" : "Fourth Last",
+ "Fifth Last" : "Fifth Last",
"Contacts" : "Contacts",
- "Tasks" : "Tasks",
- "WebDAV" : "WebDAV",
+ "{actor} created address book {addressbook}" : "{actor} created address book {addressbook}",
+ "You created address book {addressbook}" : "You created address book {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} deleted address book {addressbook}",
+ "You deleted address book {addressbook}" : "You deleted address book {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} updated address book {addressbook}",
+ "You updated address book {addressbook}" : "You updated address book {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} shared address book {addressbook} with you",
+ "You shared address book {addressbook} with {user}" : "You shared address book {addressbook} with {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} shared address book {addressbook} with {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} unshared address book {addressbook} from you",
+ "You unshared address book {addressbook} from {user}" : "You unshared address book {addressbook} from {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} unshared address book {addressbook} from {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} unshared address book {addressbook} from themselves",
+ "You shared address book {addressbook} with group {group}" : "You shared address book {addressbook} with group {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} shared address book {addressbook} with group {group}",
+ "You unshared address book {addressbook} from group {group}" : "You unshared address book {addressbook} from group {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} unshared address book {addressbook} from group {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} created contact {card} in address book {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "You created contact {card} in address book {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} deleted contact {card} from address book {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "You deleted contact {card} from address book {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} updated contact {card} in address book {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "You updated contact {card} in address book {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "A <strong>contact</strong> or <strong>address book</strong> was modified",
+ "Accounts" : "Accounts",
+ "System address book which holds all accounts" : "System address book which holds all accounts",
+ "File is not updatable: %1$s" : "File is not updatable: %1$s",
+ "Failed to get storage for file" : "Failed to get storage for file",
+ "Could not write to final file, canceled by hook" : "Could not write to final file, cancelled by hook",
+ "Could not write file contents" : "Could not write file contents",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)",
+ "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." : "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.",
+ "Could not rename part file to final file, canceled by hook" : "Could not rename part file to final file, cancelled by hook",
+ "Could not rename part file to final file" : "Could not rename part file to final file",
+ "Failed to check file size: %1$s" : "Failed to check file size: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "Could not open file: %1$s, file does seem to exist",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Could not open file: %1$s, file doesn't seem to exist",
+ "Encryption not ready: %1$s" : "Encryption not ready: %1$s",
+ "Failed to open file: %1$s" : "Failed to open file: %1$s",
+ "Failed to unlink: %1$s" : "Failed to unlink: %1$s",
+ "Failed to write file contents: %1$s" : "Failed to write file contents: %1$s",
+ "File not found: %1$s" : "File not found: %1$s",
+ "Invalid target path" : "Invalid target path",
+ "System is in maintenance mode." : "System is in maintenance mode.",
+ "Upgrade needed" : "Upgrade needed",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS.",
+ "Configures a CalDAV account" : "Configures a CalDAV account",
+ "Configures a CardDAV account" : "Configures a CardDAV account",
+ "Events" : "Events",
+ "Untitled task" : "Untitled task",
+ "Completed on %s" : "Completed on %s",
+ "Due on %s by %s" : "Due on %s by %s",
+ "Due on %s" : "Due on %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings.",
+ "Example event - open me!" : "Example event - open me!",
+ "System Address Book" : "System Address Book",
+ "The system address book contains contact information for all users in your instance." : "The system address book contains contact information for all users in your instance.",
+ "Enable System Address Book" : "Enable System Address Book",
+ "DAV system address book" : "DAV system address book",
+ "No outstanding DAV system address book sync." : "No outstanding DAV system address book sync.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\".",
"WebDAV endpoint" : "WebDAV endpoint",
- "Tentative" : "Tentative",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Your web server is not yet properly set up to allow file synchronisation, because the WebDAV interface seems to be broken.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Your web server is properly set up to allow file synchronization over WebDAV.",
+ "Migrated calendar (%1$s)" : "Migrated calendar (%1$s)",
+ "Calendars including events, details and attendees" : "Calendars including events, details and attendees",
+ "Contacts and groups" : "Contacts and groups",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "Absence saved",
+ "Failed to save your absence settings" : "Failed to save your absence settings",
+ "Absence cleared" : "Absence cleared",
+ "Failed to clear your absence settings" : "Failed to clear your absence settings",
+ "First day" : "First day",
+ "Last day (inclusive)" : "Last day (inclusive)",
+ "Out of office replacement (optional)" : "Out of office replacement (optional)",
+ "Name of the replacement" : "Name of the replacement",
+ "No results." : "No results.",
+ "Start typing." : "Start typing.",
+ "Short absence status" : "Short absence status",
+ "Long absence Message" : "Long absence Message",
"Save" : "Save",
+ "Disable absence" : "Disable absence",
+ "Failed to load availability" : "Failed to load availability",
+ "Saved availability" : "Saved availability",
+ "Failed to save availability" : "Failed to save availability",
+ "Time zone:" : "Time zone:",
+ "to" : "to",
+ "Delete slot" : "Delete slot",
+ "No working hours set" : "No working hours set",
+ "Add slot" : "Add slot",
+ "Weekdays" : "Weekdays",
+ "Pick a start time for {dayName}" : "Pick a start time for {dayName}",
+ "Pick a end time for {dayName}" : "Pick a end time for {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications.",
+ "Cancel" : "Cancel",
+ "Import" : "Import",
+ "Error while saving settings" : "Error while saving settings",
+ "Contact reset successfully" : "Contact reset successfully",
+ "Error while resetting contact" : "Error while resetting contact",
+ "Contact imported successfully" : "Contact imported successfully",
+ "Error while importing contact" : "Error while importing contact",
+ "Import contact" : "Import contact",
+ "Reset to default" : "Reset to default",
+ "Import contacts" : "Import contacts",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?",
+ "Failed to save example event creation setting" : "Failed to save example event creation setting",
+ "Failed to upload the example event" : "Failed to upload the example event",
+ "Custom example event was saved successfully" : "Custom example event was saved successfully",
+ "Failed to delete the custom example event" : "Failed to delete the custom example event",
+ "Custom example event was deleted successfully" : "Custom example event was deleted successfully",
+ "Import calendar event" : "Import calendar event",
+ "Uploading a new event will overwrite the existing one." : "Uploading a new event will overwrite the existing one.",
+ "Upload event" : "Upload event",
+ "Availability" : "Availability",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "If you configure your working hours, other people will see when you are out of office when they book a meeting.",
+ "Absence" : "Absence",
+ "Configure your next absence period." : "Configure your next absence period.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Please make sure to properly set up {emailopen}the email server{linkclose}.",
+ "Calendar server" : "Calendar server",
"Send invitations to attendees" : "Send invitations to attendees",
"Automatically generate a birthday calendar" : "Automatically generate a birthday calendar",
"Birthday calendars will be generated by a background job." : "Birthday calendars will be generated by a background job.",
"Hence they will not be available immediately after enabling but will show up after some time." : "Hence they will not be available immediately after enabling but will show up after some time.",
- "Hello %s," : "Hello %s,",
- "When:" : "When:"
+ "Send notifications for events" : "Send notifications for events",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Notifications are sent via background jobs, so these must occur often enough.",
+ "Send reminder notifications to calendar sharees as well" : "Send reminder notifications to calendar sharees as well",
+ "Reminders are always sent to organizers and attendees." : "Reminders are always sent to organisers and attendees.",
+ "Enable notifications for events via push" : "Enable notifications for events via push",
+ "Example content" : "Example content",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content.",
+ "There was an error updating your attendance status." : "There was an error updating your attendance status.",
+ "Please contact the organizer directly." : "Please contact the organiser directly.",
+ "Are you accepting the invitation?" : "Are you accepting the invitation?",
+ "Tentative" : "Tentative",
+ "Your attendance was updated successfully." : "Your attendance was updated successfully."
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/dav/l10n/eo.js b/apps/dav/l10n/eo.js
deleted file mode 100644
index f17ea331a7f..00000000000
--- a/apps/dav/l10n/eo.js
+++ /dev/null
@@ -1,105 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Kalendaro",
- "Todos" : "Taskoj",
- "Personal" : "Persona",
- "{actor} created calendar {calendar}" : "{actor} kreis kalendaron {calendar}",
- "You created calendar {calendar}" : "Vi kreis kalendaron {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} forigis kalendaron {calendar}",
- "You deleted calendar {calendar}" : "Vi forigis kalendaron {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} ĝisdatigis kalendaron {calendar}",
- "You updated calendar {calendar}" : "Vi ĝisdatigis kalendaron {calendar}",
- "You shared calendar {calendar} as public link" : "Vi kunhavigis kalendaron {calendar} per publika ligilo",
- "You removed public link for calendar {calendar}" : "Vi forigis publikan ligilon por kalendaro {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} kunhavigis kalendaron {calendar} kun vi",
- "You shared calendar {calendar} with {user}" : "Vi kunhavigis kalendaron {calendar} kun {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} kunhavigis kalendaron {calendar} kun {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} malkunhavigis kalendaron {calendar} kun vi",
- "You unshared calendar {calendar} from {user}" : "Vi malkunhavigis kalendaron {calendar} kun {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} malkunhavigis kalendaron {calendar} kun {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} malkunhavigis kalendaron {calendar} kun si mem",
- "You shared calendar {calendar} with group {group}" : "Vi kunhavigis kalendaron {calendar} kun grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} kunhavigis kalendaron {calendar} kun grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Vi malkunhavigis kalendaron {calendar} kun grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} malkunhavigis kalendaron {calendar} kun grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} kreis okazaĵon {event} en kalendaro {calendar}",
- "You created event {event} in calendar {calendar}" : "Vi kreis okazaĵon {event} en kalendaro {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} forigis okazaĵon {event} en kalendaro {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Vi forigis okazaĵon {event} en kalendaro {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} ĝisdatigis okazaĵon {event} en kalendaro {calendar}",
- "You updated event {event} in calendar {calendar}" : "Vi ĝisdatigis okazaĵon {event} en kalendaro {calendar}",
- "Busy" : "Okupita",
- "{actor} created todo {todo} in list {calendar}" : "{actor} kreis farendaĵon {todo} en listo {calendar}",
- "You created todo {todo} in list {calendar}" : "Vi kreis farendaĵon {todo} en listo {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} forigis farendaĵon {todo} en listo {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Vi forigis farendaĵon {todo} en listo {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} ĝisdatigis farendaĵon {todo} en listo {calendar}",
- "You updated todo {todo} in list {calendar}" : "Vi ĝisdatigis farendaĵon {todo} en listo {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} plenumis farendaĵon {todo} en listo {calendar}",
- "You solved todo {todo} in list {calendar}" : "Vi plenumis farendaĵon {todo} en listo {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} remalfermis farendaĵon {todo} en listo {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Vi remalfermis farendaĵon {todo} en listo {calendar}",
- "A <strong>calendar</strong> was modified" : "<strong>Kalendaro</strong> estis modifita",
- "A calendar <strong>event</strong> was modified" : "Kalendara <strong>okazaĵo</strong> estis modifita",
- "A calendar <strong>todo</strong> was modified" : "Kalendara <strong>farendaĵo</strong> estis modifita",
- "Contact birthdays" : "Kontaktaj datrevenoj",
- "Death of %s" : "Morto de %s",
- "Calendar:" : "Kalendaro:",
- "Date:" : "Dato:",
- "Where:" : "Kie:",
- "Description:" : "Priskribo:",
- "Untitled event" : "Sentitola okazaĵo",
- "_%n year_::_%n years_" : ["%n jaro","%n jaroj"],
- "_%n month_::_%n months_" : ["%n monato","%n monatoj"],
- "_%n day_::_%n days_" : ["%n tago","%n tagoj"],
- "_%n hour_::_%n hours_" : ["%n horo","%n horoj"],
- "_%n minute_::_%n minutes_" : ["%n minuto","%n minutoj"],
- "%s (in %s)" : "%s (en %s)",
- "%s (%s ago)" : "%s (antaŭ %s)",
- "Calendar: %s" : "Kalendaro: %s",
- "Date: %s" : "Dato: %s",
- "Description: %s" : "Priskribo: %s",
- "Where: %s" : "Kie: %s",
- "%1$s via %2$s" : "%1$s pere de %2$s",
- "Invitation canceled" : "Invito nuligita",
- "Invitation updated" : "Invito ĝisdatigita",
- "Location:" : "Loko:",
- "Link:" : "Ligilo:",
- "Accept" : "Akcepti",
- "Decline" : "Malakcepti",
- "More options …" : "Pli da opcioj...",
- "More options at %s" : "Pli da opcioj je %s",
- "Contacts" : "Kontaktoj",
- "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Via %s uzu HTTPS, por ke vi povu uzi CalDAV kaj CardDAV kun iOS aŭ macOS.",
- "Configures a CalDAV account" : "Agordas CalDAV-konton.",
- "Configures a CardDAV account" : "Agordas CardlDAV-konton.",
- "Tasks" : "Taskoj",
- "WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV-finpunkto",
- "to" : "al",
- "Monday" : "lundo",
- "Tuesday" : "mardo",
- "Wednesday" : "merkredo",
- "Thursday" : "ĵaŭdo",
- "Friday" : "vendredo",
- "Saturday" : "sabato",
- "Sunday" : "dimanĉo",
- "Save" : "Konservi",
- "Calendar server" : "Kalendara servilo",
- "Send invitations to attendees" : "Sendi invitojn al ĉeestantoj",
- "Automatically generate a birthday calendar" : "Aŭtomate estigi datrevenan kalendaron",
- "Birthday calendars will be generated by a background job." : "Datrevenaj kalendaroj estos kreitaj de fona tasko.",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Tial, ili disponeblos nur post kelke da tempo.",
- "Send notifications for events" : "Sendi sciigojn pri okazaĵoj",
- "Enable notifications for events via push" : "Ebligi sciigojn pri okazaĵoj per puŝteĥniko",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Ankaŭ instalu la aplikaĵon {calendarappstoreopen}Kalendaro{linkclose} aŭ {calendardocopen}konektu vian surtablan kaj porteblan aparaton por eksinkronigi ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Bv. certigi, ke via {emailopen}retpoŝtserva servilo{linkclose} estas bone agordita.",
- "There was an error updating your attendance status." : "Estis eraro dum ĝisdatigo de via ĉeesta stato.",
- "Please contact the organizer directly." : "Bv. senpere kontakti la organizanton.",
- "Are you accepting the invitation?" : "Ĉu vi akceptas la inviton?",
- "Tentative" : "Nekonfirmita",
- "Comment" : "Komento",
- "Your attendance was updated successfully." : "Via ĉeesto sukcese ĝisdatiĝis."
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/eo.json b/apps/dav/l10n/eo.json
deleted file mode 100644
index 3f616e499b4..00000000000
--- a/apps/dav/l10n/eo.json
+++ /dev/null
@@ -1,103 +0,0 @@
-{ "translations": {
- "Calendar" : "Kalendaro",
- "Todos" : "Taskoj",
- "Personal" : "Persona",
- "{actor} created calendar {calendar}" : "{actor} kreis kalendaron {calendar}",
- "You created calendar {calendar}" : "Vi kreis kalendaron {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} forigis kalendaron {calendar}",
- "You deleted calendar {calendar}" : "Vi forigis kalendaron {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} ĝisdatigis kalendaron {calendar}",
- "You updated calendar {calendar}" : "Vi ĝisdatigis kalendaron {calendar}",
- "You shared calendar {calendar} as public link" : "Vi kunhavigis kalendaron {calendar} per publika ligilo",
- "You removed public link for calendar {calendar}" : "Vi forigis publikan ligilon por kalendaro {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} kunhavigis kalendaron {calendar} kun vi",
- "You shared calendar {calendar} with {user}" : "Vi kunhavigis kalendaron {calendar} kun {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} kunhavigis kalendaron {calendar} kun {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} malkunhavigis kalendaron {calendar} kun vi",
- "You unshared calendar {calendar} from {user}" : "Vi malkunhavigis kalendaron {calendar} kun {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} malkunhavigis kalendaron {calendar} kun {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} malkunhavigis kalendaron {calendar} kun si mem",
- "You shared calendar {calendar} with group {group}" : "Vi kunhavigis kalendaron {calendar} kun grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} kunhavigis kalendaron {calendar} kun grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Vi malkunhavigis kalendaron {calendar} kun grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} malkunhavigis kalendaron {calendar} kun grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} kreis okazaĵon {event} en kalendaro {calendar}",
- "You created event {event} in calendar {calendar}" : "Vi kreis okazaĵon {event} en kalendaro {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} forigis okazaĵon {event} en kalendaro {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Vi forigis okazaĵon {event} en kalendaro {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} ĝisdatigis okazaĵon {event} en kalendaro {calendar}",
- "You updated event {event} in calendar {calendar}" : "Vi ĝisdatigis okazaĵon {event} en kalendaro {calendar}",
- "Busy" : "Okupita",
- "{actor} created todo {todo} in list {calendar}" : "{actor} kreis farendaĵon {todo} en listo {calendar}",
- "You created todo {todo} in list {calendar}" : "Vi kreis farendaĵon {todo} en listo {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} forigis farendaĵon {todo} en listo {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Vi forigis farendaĵon {todo} en listo {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} ĝisdatigis farendaĵon {todo} en listo {calendar}",
- "You updated todo {todo} in list {calendar}" : "Vi ĝisdatigis farendaĵon {todo} en listo {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} plenumis farendaĵon {todo} en listo {calendar}",
- "You solved todo {todo} in list {calendar}" : "Vi plenumis farendaĵon {todo} en listo {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} remalfermis farendaĵon {todo} en listo {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Vi remalfermis farendaĵon {todo} en listo {calendar}",
- "A <strong>calendar</strong> was modified" : "<strong>Kalendaro</strong> estis modifita",
- "A calendar <strong>event</strong> was modified" : "Kalendara <strong>okazaĵo</strong> estis modifita",
- "A calendar <strong>todo</strong> was modified" : "Kalendara <strong>farendaĵo</strong> estis modifita",
- "Contact birthdays" : "Kontaktaj datrevenoj",
- "Death of %s" : "Morto de %s",
- "Calendar:" : "Kalendaro:",
- "Date:" : "Dato:",
- "Where:" : "Kie:",
- "Description:" : "Priskribo:",
- "Untitled event" : "Sentitola okazaĵo",
- "_%n year_::_%n years_" : ["%n jaro","%n jaroj"],
- "_%n month_::_%n months_" : ["%n monato","%n monatoj"],
- "_%n day_::_%n days_" : ["%n tago","%n tagoj"],
- "_%n hour_::_%n hours_" : ["%n horo","%n horoj"],
- "_%n minute_::_%n minutes_" : ["%n minuto","%n minutoj"],
- "%s (in %s)" : "%s (en %s)",
- "%s (%s ago)" : "%s (antaŭ %s)",
- "Calendar: %s" : "Kalendaro: %s",
- "Date: %s" : "Dato: %s",
- "Description: %s" : "Priskribo: %s",
- "Where: %s" : "Kie: %s",
- "%1$s via %2$s" : "%1$s pere de %2$s",
- "Invitation canceled" : "Invito nuligita",
- "Invitation updated" : "Invito ĝisdatigita",
- "Location:" : "Loko:",
- "Link:" : "Ligilo:",
- "Accept" : "Akcepti",
- "Decline" : "Malakcepti",
- "More options …" : "Pli da opcioj...",
- "More options at %s" : "Pli da opcioj je %s",
- "Contacts" : "Kontaktoj",
- "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Via %s uzu HTTPS, por ke vi povu uzi CalDAV kaj CardDAV kun iOS aŭ macOS.",
- "Configures a CalDAV account" : "Agordas CalDAV-konton.",
- "Configures a CardDAV account" : "Agordas CardlDAV-konton.",
- "Tasks" : "Taskoj",
- "WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV-finpunkto",
- "to" : "al",
- "Monday" : "lundo",
- "Tuesday" : "mardo",
- "Wednesday" : "merkredo",
- "Thursday" : "ĵaŭdo",
- "Friday" : "vendredo",
- "Saturday" : "sabato",
- "Sunday" : "dimanĉo",
- "Save" : "Konservi",
- "Calendar server" : "Kalendara servilo",
- "Send invitations to attendees" : "Sendi invitojn al ĉeestantoj",
- "Automatically generate a birthday calendar" : "Aŭtomate estigi datrevenan kalendaron",
- "Birthday calendars will be generated by a background job." : "Datrevenaj kalendaroj estos kreitaj de fona tasko.",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Tial, ili disponeblos nur post kelke da tempo.",
- "Send notifications for events" : "Sendi sciigojn pri okazaĵoj",
- "Enable notifications for events via push" : "Ebligi sciigojn pri okazaĵoj per puŝteĥniko",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Ankaŭ instalu la aplikaĵon {calendarappstoreopen}Kalendaro{linkclose} aŭ {calendardocopen}konektu vian surtablan kaj porteblan aparaton por eksinkronigi ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Bv. certigi, ke via {emailopen}retpoŝtserva servilo{linkclose} estas bone agordita.",
- "There was an error updating your attendance status." : "Estis eraro dum ĝisdatigo de via ĉeesta stato.",
- "Please contact the organizer directly." : "Bv. senpere kontakti la organizanton.",
- "Are you accepting the invitation?" : "Ĉu vi akceptas la inviton?",
- "Tentative" : "Nekonfirmita",
- "Comment" : "Komento",
- "Your attendance was updated successfully." : "Via ĉeesto sukcese ĝisdatiĝis."
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/es.js b/apps/dav/l10n/es.js
index 005c44564c9..610c2f03dc2 100644
--- a/apps/dav/l10n/es.js
+++ b/apps/dav/l10n/es.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Calendario",
- "Todos" : "Todos",
+ "Tasks" : "Tareas",
"Personal" : "Personal",
"{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
"You created calendar {calendar}" : "Usted creó el calendario {calendar}",
@@ -10,12 +10,12 @@ OC.L10N.register(
"You deleted calendar {calendar}" : "Usted eliminó el calendario {calendar}",
"{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
"You updated calendar {calendar}" : "Usted actualizó el calendario {calendar}",
- "{actor} restored calendar {calendar}" : "{actor} ha restablecido el calendario {calendar}",
- "You restored calendar {calendar}" : "Has restablecido el calendario {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} ha restaurado el calendario {calendar}",
+ "You restored calendar {calendar}" : "Ud. ha restaurado el calendario {calendar}",
"You shared calendar {calendar} as public link" : "Has compartido el calendario {calendar} con un enlace público",
"You removed public link for calendar {calendar}" : "Has eliminado el enlace público al calendario {calendar}",
"{actor} shared calendar {calendar} with you" : "{actor} compartió el calendario {calendar} con usted",
- "You shared calendar {calendar} with {user}" : "Usted compartió el calendario {calendar} con {usuario}",
+ "You shared calendar {calendar} with {user}" : "Usted compartió el calendario {calendar} con {user}",
"{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
"{actor} unshared calendar {calendar} from you" : "{actor} dejó de compartir el calendario {calendar} con usted",
"You unshared calendar {calendar} from {user}" : "Usted dejó de compartir el calendario {calendar} de {user}",
@@ -25,41 +25,46 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
"You unshared calendar {calendar} from group {group}" : "Usted dejó de compartir el calendario {calendar} del grupo {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendario {calendar} del grupo {group}",
+ "Untitled event" : "Evento sin título",
"{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
"You created event {event} in calendar {calendar}" : "Usted creó el evento {event} en el calendario {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} eliminó el evento {event} del calendario {calendar}",
"You deleted event {event} from calendar {calendar}" : "Usted eliminó el evento {event} del calendario {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
"You updated event {event} in calendar {calendar}" : "Usted actualizó el evento {event} en el calendario {calendar}",
- "{actor} restored event {event} of calendar {calendar}" : "{actor} ha restablecido el evento {event} del calendario {calendar}",
- "You restored event {event} of calendar {calendar}" : "Has reestablecido el evento {event} del calendario {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} ha movido el evento {event} del calendario {sourceCalendar} al calendario {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Ud. ha movido el evento {event} del calendario {sourceCalendar} al calendario {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} ha restaurado el evento {event} del calendario {calendar}",
+ "You restored event {event} of calendar {calendar}" : "Ud. ha restaurado el evento {event} del calendario {calendar}",
"Busy" : "Ocupado",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó la tarea {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Usted creó la tarea {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} eliminó la tarea {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Usted eliminó la tarea {tod} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó la tarea {todo }en la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Usted actualizó la tarea {todo} en la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} completó la tarea {todo} en la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Usted completó la tarea {todo} en la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió la tarea {todo} en la lista {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Usted reabrió la tarea {todo} en la lista {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} ha creado la tarea {todo} en la lista {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Ud. ha creado la tarea {todo} en la lista {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} ha eliminado la tarea{todo} de la lista {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Ud. ha eliminado la tarea {todo} de la lista {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} ha actualizado la tarea {todo} en la lista {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Ud. ha actualizado la tarea {todo} en la lista {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} ha resuelto la tarea {todo} de la lista {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Ud. ha resuelto la tarea {todo} de la lista {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} ha reabierto la tarea {todo} en la lista {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Ud. ha reabierto la tarea {todo} en la lista {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} ha movido la tarea {todo} de la lista {sourceCalendar} a la lista{targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Ud. ha movido la tarea {todo} de la lista {sourceCalendar} a la lista {targetCalendar}",
"Calendar, contacts and tasks" : "Calendario, contactos y tareas",
"A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado.",
"A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> del calendario fue modificado.",
- "A calendar <strong>todo</strong> was modified" : "Una <strong>lista de tareas</strong> fue modificada",
+ "A calendar <strong>to-do</strong> was modified" : "Se ha modificado una <strong>tarea</strong> de calendario",
"Contact birthdays" : "Cumpleaños del contacto",
"Death of %s" : "Muerte de %s",
+ "Untitled calendar" : "Calendario sin título",
"Calendar:" : "Calendario:",
"Date:" : "Fecha:",
"Where:" : "Dónde:",
"Description:" : "Descripción:",
- "Untitled event" : "Evento sin título",
- "_%n year_::_%n years_" : ["%n año","%n años"],
- "_%n month_::_%n months_" : ["%n mes","%n meses"],
- "_%n day_::_%n days_" : ["%n día","%n días"],
- "_%n hour_::_%n hours_" : ["%n hora","%n horas"],
- "_%n minute_::_%n minutes_" : ["%n minuto","%n minutos"],
+ "_%n year_::_%n years_" : ["%n año","%n años","%n años"],
+ "_%n month_::_%n months_" : ["%n mes","%n meses","%n meses"],
+ "_%n day_::_%n days_" : ["%n día","%n días","%n días"],
+ "_%n hour_::_%n hours_" : ["%n hora","%n horas","%n horas"],
+ "_%n minute_::_%n minutes_" : ["%n minuto","%n minutos","%n minutos"],
"%s (in %s)" : "%s (en %s)",
"%s (%s ago)" : "%s (hace %s)",
"Calendar: %s" : "Calendario: %s",
@@ -67,108 +72,267 @@ OC.L10N.register(
"Description: %s" : "Descripción: %s",
"Where: %s" : "Lugar: %s",
"%1$s via %2$s" : "%1$s vía %2$s",
+ "In the past on %1$s for the entire day" : "En el pasado el %1$s durante todo el día",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["En un minuto el %1$s durante todo el día","En %n minutos el %1$s durante todo el día","En %n minutos el %1$s durante todo el día"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["En una hora el %1$s durante todo el día","En %n horas el %1$s durante todo el día","En %n horas el %1$s durante todo el día"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["En un día el %1$s durante todo el día","En %n días el %1$s durante todo el día","En %n días el %1$s durante todo el día"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["En una semana el %1$s durante todo el día","En %n semanas el %1$s durante todo el día","En %n semanas el %1$s durante todo el día"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["En un mes el %1$s durante todo el día","En %n meses el %1$s durante todo el día","En %n meses el %1$s durante todo el día"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["En un año el %1$s durante todo el día","En %n años el %1$s durante todo el día","En %n años el %1$s durante todo el día"],
+ "In the past on %1$s between %2$s - %3$s" : "En el pasado el %1$s entre %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["En un minuto el %1$s entre %2$s - %3$s","En %n minutos el %1$s entre %2$s - %3$s","En %n minutos el %1$s entre %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["En una hora el %1$s entre %2$s - %3$s","En %n horas el %1$s entre %2$s - %3$s","En %n horas el %1$s entre %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["En un día el %1$s entre %2$s - %3$s","En %n días el %1$s entre %2$s - %3$s","En %n días el %1$s entre %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["En una semana el %1$s entre %2$s - %3$s","En %n semanas el %1$s entre %2$s - %3$s","En %n semanas el %1$s entre %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["En un mes el %1$s entre %2$s - %3$s","En %n meses el l%1$s entre %2$s - %3$s ","En %n meses el %1$s entre %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["En un año el %1$s entre %2$s - %3$s","En %n años el %1$s entre %2$s - %3$s","En %n años el %1$s entre %2$s - %3$s"],
+ "Could not generate when statement" : "No se pudo generar la declaración de cuándo",
+ "Every Day for the entire day" : "Todos los días, durante todo el día",
+ "Every Day for the entire day until %1$s" : "Cada día, todo el día hasta el %1$s",
+ "Every Day between %1$s - %2$s" : "Todos los días entre %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Todos los días entre %1$s - %2$s hasta %3$s",
+ "Every %1$d Days for the entire day" : "Cada %1$d días, durante todo el día",
+ "Every %1$d Days for the entire day until %2$s" : "Cada %1$d días, durante todo el día, hasta el %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Cada %1$d días, entre las %2$s y %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Cada %1$d días, entre las %2$s y %3$s hasta el %4$s",
+ "Could not generate event recurrence statement" : "No se ha podido generar la declaración de recurrencia del evento",
+ "Every Week on %1$s for the entire day" : "Cada semana el %1$s, durante todo el día",
+ "Every Week on %1$s for the entire day until %2$s" : "Cada %1$s días, durante todo el día, hasta el %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Cada semana el %1$s entre %2$s y %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Cada semana el %1$s entre %2$s y %3$s hasta el %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Cada %1$d semanas el %2$s todo el día",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Cada %1$d semanas el %2$s durante todo el día hasta %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Cada %1$d semanas el %2$s entre %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Cada %1$d semanas el %2$s entre %3$s - %4$s hasta %5$s",
+ "Every Month on the %1$s for the entire day" : "Cada mes el día %1$s, durante todo el día",
+ "Every Month on the %1$s for the entire day until %2$s" : "Cada mes el día %1$s durante todo el día hasta %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Cada mes el día %1$s entre %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Cada mes el día %1$s entre %2$s - %3$s hasta %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Cada %1$d meses el día %2$s, durante todo el día",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Cada %1$d meses el día %2$s, durante todo el día hasta %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Cada %1$d meses el día %2$s entre %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Cada %1$d meses el día %2$s entre %3$s - %4$s hasta %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Cada año en %1$s el día %2$s, durante todo el día",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Cada año en %1$s el día %2$s, durante todo el día, hasta %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Cada año en %1$s el día %2$s entre %3$s - %4$s.",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Cada año en %1$s el día %2$s entre %3$s - %4$s hasta %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Cada %1$d años en %2$s el día %3$s, durante todo el día",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Cada %1$d años en %2$s el día %3$s, durante todo el día, hasta %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Cada %1$d años en %2$s el día %3$s entre %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Cada %1$d años en %2$s el día %3$s entre %4$s - %5$s hasta %6$s",
+ "On specific dates for the entire day until %1$s" : "En fechas específicas, durante todo el día, hasta %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "En fechas específicas entre %1$s - %2$s hasta %3$s",
+ "In the past on %1$s" : "En el pasado el %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["En un minuto el %1$s","En %n minutos el %1$s","En %n minutos el %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["En una hora el %1$s","En %n horas el %1$s","En %n horas el %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["En un día el %1$s","En %n días el %1$s","En %n días el %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["En una semana el %1$s","En %n semanas el %1$s","En %n semanas el %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["En un mes el %1$s","En %n meses el %1$s","En %n meses el %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["En un año el %1$s","En %n años el %1$s","En %n años el %1$s"],
+ "In the past on %1$s then on %2$s" : "En el pasado el %1$s y luego el %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["En un minuto el %1$s y luego el %2$s","En %n minutos el %1$s y luego el %2$s","En %n minutos el %1$s y luego el %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["En una hora el %1$s y luego el %2$s ","En %n horas el %1$s y luego el %2$s","En %n horas el %1$s y luego el %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["En un día el %1$s y luego el %2$s","En %n días el %1$s y luego el %2$s","En %n días el %1$s y luego el %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["En una semana el %1$s y luego el %2$s","En %n semanas el %1$s y luego el %2$s","En %n semanas el %1$s y luego el %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["En un mes el %1$s y luego el %2$s","En %n meses el %1$s y luego el %2$s","En %n meses el %1$s y luego el %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["En un año el %1$s y luego el %2$s","En %n años el %1$s y luego el %2$s","En %n años el %1$s y luego el %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "En el pasado el %1$s, luego el %2$s y %3$s",
+ "_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_" : ["En un minuto el %1$s y luego el %2$s y %3$s","En %n minutos el %1$s y luego el %2$s y %3$s","En %n minutos el %1$s y luego el %2$s y %3$s"],
+ "_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_" : ["En una hora el %1$s y luego el %2$s y %3$s","En %n horas el %1$s y luego el %2$s y %3$s","En %n horas el %1$s y luego el %2$s y %3$s"],
+ "_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_" : ["En un día el %1$s y luego el %2$s y %3$s","En %n días el %1$s y luego el %2$s y %3$s","En %n días el %1$s y luego el %2$s y %3$s"],
+ "_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_" : ["En una semana el %1$s y luego el %2$s y %3$s","En %n semanas el %1$s y luego el %2$s y %3$s","En %n semanas el %1$s y luego el %2$s y %3$s"],
+ "_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_" : ["En un mes el %1$s y luego el %2$s y %3$s","En %n meses el %1$s y luego el %2$s y %3$s","En %n meses el %1$s y luego el %2$s y %3$s"],
+ "_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_" : ["En un año el %1$s y luego el %2$s y %3$s","En %n años el %1$s y luego el %2$s y %3$s","En %n años el %1$s y luego el %2$s y %3$s"],
+ "Could not generate next recurrence statement" : "No se ha podido generar la declaración de la siguiente recurrencia",
"Cancelled: %1$s" : "Cancelado: %1$s",
- "Invitation canceled" : "Invitación cancelada",
+ "\"%1$s\" has been canceled" : "\"%1$s\" ha sido cancelada",
"Re: %1$s" : "Re: %1$s",
- "Invitation updated" : "Invitación actualizada",
+ "%1$s has accepted your invitation" : "%1$s ha aceptado su invitación",
+ "%1$s has tentatively accepted your invitation" : "%1$s ha aceptado su invitación de forma tentativa",
+ "%1$s has declined your invitation" : "%1$s ha rechazado su invitación",
+ "%1$s has responded to your invitation" : "%1$s ha respondido a su invitación",
+ "Invitation updated: %1$s" : "Invitación actualizada: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s actualizó el evento \"%2$s\"",
"Invitation: %1$s" : "Invitación: %1$s",
- "Invitation" : "Invitación",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s desea invitarle a \"%2$s\"",
+ "Organizer:" : "Organizador:",
+ "Attendees:" : "Asistentes:",
"Title:" : "Título:",
- "Time:" : "Hora:",
+ "When:" : "Cuándo:",
"Location:" : "Ubicación:",
"Link:" : "Enlace:",
- "Organizer:" : "Organizador:",
- "Attendees:" : "Asistentes:",
+ "Occurring:" : "Repetición:",
"Accept" : "Aceptar",
"Decline" : "Rechazar",
"More options …" : "Más opciones...",
"More options at %s" : "Más opciones en %s",
+ "Monday" : "Lunes",
+ "Tuesday" : "Martes",
+ "Wednesday" : "Miércoles",
+ "Thursday" : "Jueves",
+ "Friday" : "Viernes",
+ "Saturday" : "Sábado",
+ "Sunday" : "Domingo",
+ "January" : "Enero",
+ "February" : "Febrero",
+ "March" : "Marzo",
+ "April" : "Abril",
+ "May" : "Mayo",
+ "June" : "Junio",
+ "July" : "Julio",
+ "August" : "Agosto",
+ "September" : "Septiembre",
+ "October" : "Octubre",
+ "November" : "Noviembre",
+ "December" : "Diciembre",
+ "First" : "Primero",
+ "Second" : "Segundo",
+ "Third" : "Tercero",
+ "Fourth" : "Cuarto",
+ "Fifth" : "Quinto",
+ "Last" : "Último",
+ "Second Last" : "Penúltima",
+ "Third Last" : "Antepenúltima",
+ "Fourth Last" : "Ante antepenúltimo",
+ "Fifth Last" : "Quinto último",
"Contacts" : "Contactos",
"{actor} created address book {addressbook}" : "{actor} ha creado la libreta de direcciones {addressbook}",
- "You created address book {addressbook}" : "Has creado la libreta de direcciones {adressbook}",
+ "You created address book {addressbook}" : "Ud. ha creado la libreta de direcciones {addressbook}",
"{actor} deleted address book {addressbook}" : "{actor} ha eliminado la libreta de direcciones {addressbook}",
- "You deleted address book {addressbook}" : "Has eliminado la libreta de direcciones {addressbook}",
+ "You deleted address book {addressbook}" : "Ud. ha eliminado la libreta de direcciones {addressbook}",
"{actor} updated address book {addressbook}" : "{actor} ha actualizado la libreta de direcciones {addressbook}",
- "You updated address book {addressbook}" : "Has actualizado la libreta de direcciones {addressbook}",
+ "You updated address book {addressbook}" : "Ud. ha actualizado la libreta de direcciones {addressbook}",
"{actor} shared address book {addressbook} with you" : "{actor} ha compartido la libreta de direcciones {addressbook} contigo",
- "You shared address book {addressbook} with {user}" : "Has compartido la libreta de direcciones {addressbook} con {user}",
+ "You shared address book {addressbook} with {user}" : "Ud. ha compartido la libreta de direcciones {addressbook} con {user}",
"{actor} shared address book {addressbook} with {user}" : "{actor} ha compartido la libreta de direcciones {addressbook} con {user}",
- "{actor} unshared address book {addressbook} from you" : "{actor} ha descompartido la libreta de direcciones {addressbook} contigo",
- "You unshared address book {addressbook} from {user}" : "Has descompartido la libreta de direcciones {addressbook} con {user}",
- "{actor} unshared address book {addressbook} from {user}" : "{actor} ha descompartido la libreta de direcciones {addressbook} con {user}",
- "{actor} unshared address book {addressbook} from themselves" : "{actor} ha descompartido la libreta de direcciones {addressbook} con ellos mismos",
- "You shared address book {addressbook} with group {group}" : "Has compartido la libreta de direcciones {addressbook} con el grupo {group}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} ha dejado de compartir la libreta de direcciones {addressbook} con Ud.",
+ "You unshared address book {addressbook} from {user}" : "Ud. ha dejado de compartir la libreta de direcciones {addressbook} con {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} ha dejado de compartir la libreta de direcciones {addressbook} con {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} dejó de compartir su propia libreta de direcciones {addressbook}",
+ "You shared address book {addressbook} with group {group}" : "Ud. ha compartido la libreta de direcciones {addressbook} con el grupo {group}",
"{actor} shared address book {addressbook} with group {group}" : "{actor} ha compartido la libreta de direcciones {addressbook} con el grupo {group}",
- "You unshared address book {addressbook} from group {group}" : "Has descompartido la libreta de direcciones {addressbook} con el grupo {group}",
- "{actor} unshared address book {addressbook} from group {group}" : "{actor} ha descompartido la libreta de direcciones {addressbook} con el grupo {group}",
- "{actor} created contact {card} in address book {addressbook}" : "{actor} ha creado el contacto {card} en la libreta de direcciones {addressbook}",
- "You created contact {card} in address book {addressbook}" : "Has creado un contacto {card} en la libreta de direcciones {addressbook}",
+ "You unshared address book {addressbook} from group {group}" : "Ud. ha dejado de compartir la libreta de direcciones {addressbook} con el grupo {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} ha dejado de compartir la libreta de direcciones {addressbook} con el grupo {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} ha creado el contacto {card} en la libreta de direcciones {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Ud. ha creado un contacto {card} en la libreta de direcciones {addressbook}",
"{actor} deleted contact {card} from address book {addressbook}" : "{actor} ha eliminado el contacto {card} de la libreta de direcciones {addressbook}",
- "You deleted contact {card} from address book {addressbook}" : "Has eliminado el contacto {card} de la libreta de direcciones {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Ud. ha eliminado el contacto {card} de la libreta de direcciones {addressbook}",
"{actor} updated contact {card} in address book {addressbook}" : "{actor} ha actualizado el contacto {card} en la libreta de direcciones {addressbook}",
- "You updated contact {card} in address book {addressbook}" : "Has actualizado el contacto {card} en la libreta de direcciones {addressbook}",
- "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Se ha modificado un <strong>contacto</strong> o una <strong>libreta de direcciones</strong> ",
+ "You updated contact {card} in address book {addressbook}" : "Ud. ha actualizado el contacto {card} en la libreta de direcciones {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Se ha modificado un <strong>contacto</strong> o una <strong>libreta de direcciones</strong>",
+ "Accounts" : "Cuentas",
+ "System address book which holds all accounts" : "Libreta de direcciones del sistema que contiene todas las cuentas",
"File is not updatable: %1$s" : "El archivo no se puede actualizar: %1$s",
+ "Failed to get storage for file" : "Error al obtener datos de almacenamiento para el archivo",
+ "Could not write to final file, canceled by hook" : "No se pudo escribir en el archivo final, cancelado por el sistema",
"Could not write file contents" : "No se han podido escribir los contenidos del archivo",
- "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes","%n bytes"],
"Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Error al copiar el archivo al destino (copiado: %1$s, tamaño esperado: %2$s)",
+ "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." : "Se esperaba un tamaño de archivo de %1$s pero se leyó (desde el cliente Nextcloud) y se escribió (en el almacenamiento Nextcloud) %2$s. Podría ser un problema de red en el lado del envío o un problema de escritura en el almacenamiento en el lado del servidor.",
+ "Could not rename part file to final file, canceled by hook" : "No se pudo renombrar del archivo parcial como el archivo final, cancelado por el sistema.",
"Could not rename part file to final file" : "No se ha podido renombrar el archivo parcial como el archivo final",
"Failed to check file size: %1$s" : "Fallo al comprobar el tamaño del archivo: %1$s",
- "Could not open file" : "No se ha podido abrir el archivo",
+ "Could not open file: %1$s, file does seem to exist" : "No se pudo abrir el archivo: %1$s, parece que el archivo existe",
+ "Could not open file: %1$s, file doesn't seem to exist" : "No se pudo abrir el archivo: %1$s, parece que el archivo no existe",
+ "Encryption not ready: %1$s" : "El cifrado no está listo: %1$s",
"Failed to open file: %1$s" : "Fallo al abrir el archivo: %1$s",
"Failed to unlink: %1$s" : "Fallo al desenlazar: %1$s",
- "Could not rename part file assembled from chunks" : "No se ha podido renombrar el archivo parcial formado por los fragmentos",
+ "Failed to write file contents: %1$s" : "Fallo al escribir el contenido del archivo: %1$s",
"File not found: %1$s" : "Archivo no encontrado: %1$s",
+ "Invalid target path" : "Ruta de destino inválida",
"System is in maintenance mode." : "Sistema está en modo de mantenimiento.",
"Upgrade needed" : "Se necesita actualizar",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Tu %s necesita configurarse para usar HTTPS en caso de usar CalDAV y CardDAV con iOS/macOS.",
"Configures a CalDAV account" : "Configura una cuenta CalDAV",
"Configures a CardDAV account" : "Configura una cuenta CardDAV",
"Events" : "Eventos",
- "Tasks" : "Tareas",
"Untitled task" : "Tarea sin título",
"Completed on %s" : "Completado el %s",
"Due on %s by %s" : "Finaliza el %s por %s",
"Due on %s" : "Finaliza el %s",
- "Migrated calendar (%1$s)" : "Calendario migrado (%1$s)",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "¡Bienvenido a Nextcloud Calendar!\n\nEste es un evento de ejemplo - ¡explore la flexibilidad de planear con Nextcloud Calendar editando cuantas veces quiera!\n\nCon Nextcloud Calendar, podrá:\n- Crear, editar, y administrar eventos fácilmente.\n- Crear múltiples calendarios y compartirlos con su equipo de trabajo, amigos, o su familia.\n- Verificar la disponibilidad y mostrar sus tiempos de ocupado a otros.\n- Se integra de manera transparente con otras apps y dispositivos via CalDAV.\n- Personalice su experiencia: Programe eventos recurrentes, ajuste las notificaciones, así como otros ajustes.",
+ "Example event - open me!" : "Evento de ejemplo - ¡ábralo!",
+ "System Address Book" : "Libreta de Direcciones del Sistema",
+ "The system address book contains contact information for all users in your instance." : "La libreta de direcciones del sistema contiene la información de contacto de todos los usuarios de su instancia.",
+ "Enable System Address Book" : "Habilitar la Libreta de Direcciones del Sistema",
+ "DAV system address book" : "Libreta de direcciónes DAV del sistema",
+ "No outstanding DAV system address book sync." : "No hay una sincronización pendiente en la libreta de direcciones DAV del sistema.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "La sincronización DAV de la libreta de direcciones del sistema no se ha ejecutado ya que su instancia tiene más de 1000 usuarios o por que ha ocurrido un error. Por favor, ejecútela manualmente llamando al comando: \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "Endpoint WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "No se pudo verificar si su servidor web está adecuadamente configurado para permitir la sincronización de archivos a través de WebDAV. Por favor, verifique manualmente.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Su servidor web todavía no está configurado correctamente para permitir la sincronización de archivos, porque la interfaz WebDAV parece estar rota.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Su servidor web está adecuadamente configurado para permitir la sincronización de archivos a través de WebDAV.",
+ "Migrated calendar (%1$s)" : "Se migró el calendario (%1$s)",
"Calendars including events, details and attendees" : "Calendarios que incluyen eventos, detalles y asistentes",
"Contacts and groups" : "Contactos y grupos",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "Extremo de WebDAV",
- "Availability" : "Disponibilidad",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Si configura su horario de trabajo, otros usuarios verán cuándo está fuera de la oficina cuando planifiquen una reunión.",
+ "Absence saved" : "Ausencia guardada",
+ "Failed to save your absence settings" : "Error al guardar sus ajustes de ausencia",
+ "Absence cleared" : "Ausencia limpiada",
+ "Failed to clear your absence settings" : "Error al borrar sus ajustes de ausencia",
+ "First day" : "Primer día",
+ "Last day (inclusive)" : "Último día (incluido)",
+ "Out of office replacement (optional)" : "Sustituto durante vacaciones/ausencia (opcional)",
+ "Name of the replacement" : "Nombre del sustituto",
+ "No results." : "Sin resultados.",
+ "Start typing." : "Empiece a escribir.",
+ "Short absence status" : "Estado de ausencia corta",
+ "Long absence Message" : "Mensaje de ausencia larga",
+ "Save" : "Guardar",
+ "Disable absence" : "Deshabilitar ausencia",
+ "Failed to load availability" : "No se ha podido cargar la disponibilidad",
+ "Saved availability" : "Disponibilidad guardada",
+ "Failed to save availability" : "No se ha podido guardar la disponibilidad",
"Time zone:" : "Zona horaria:",
"to" : "para",
- "Delete slot" : "Eliminar espacio",
- "No working hours set" : "No se han establecido horas de funcionamiento",
- "Add slot" : "Añadir espacio",
- "Monday" : "Lunes",
- "Tuesday" : "Martes",
- "Wednesday" : "Miércoles",
- "Thursday" : "Jueves",
- "Friday" : "Viernes",
- "Saturday" : "Sábado",
- "Sunday" : "Domingo",
- "Save" : "Guardar",
+ "Delete slot" : "Eliminar franja de tiempo",
+ "No working hours set" : "No se han establecido horas laborales",
+ "Add slot" : "Añadir franja horaria",
+ "Weekdays" : "Días de semana",
+ "Pick a start time for {dayName}" : "Elija una hora de inicio para {dayName}",
+ "Pick a end time for {dayName}" : "Elija una hora fin para {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Cambiar automáticamente el estado del usuario a \"No molestar\" fuera de las horas de disponibilidad para silenciar todas las notificaciones.",
+ "Cancel" : "Cancelar",
+ "Import" : "Importar",
+ "Error while saving settings" : "Error al guardar los ajustes",
+ "Contact reset successfully" : "El contacto fue restablecido exitosamente",
+ "Error while resetting contact" : "Error al restablecer el contacto",
+ "Contact imported successfully" : "El contacto se importó exitosamente",
+ "Error while importing contact" : "Error al importar el contacto",
+ "Import contact" : "Importar contacto",
+ "Reset to default" : "Restablecer a predeterminado",
+ "Import contacts" : "Importar contactos",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Importar un nuevo archivo .vcf eliminará el contacto predeterminado existente y lo reemplazará con el nuevo. ¿Desea continuar?",
+ "Failed to save example event creation setting" : "Fallo al guardar el ajuste de creación de evento de ejemplo",
+ "Failed to upload the example event" : "Fallo al cargar el evento de ejemplo",
+ "Custom example event was saved successfully" : "El evento de ejemplo personalizado se guardó exitosamente",
+ "Failed to delete the custom example event" : "Fallo al guardar el evento de ejemplo personalizado",
+ "Custom example event was deleted successfully" : "El evento de ejemplo personalizado se eliminó exitosamente",
+ "Import calendar event" : "Importar evento del calendario",
+ "Uploading a new event will overwrite the existing one." : "Cargar un evento nuevo sobrescribirá el existente. ",
+ "Upload event" : "Cargar evento",
+ "Availability" : "Disponibilidad",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Si configura sus horas laborales, otras personas verán cuando está fuera de la oficina cuando agenden una reunión.",
+ "Absence" : "Ausencia",
+ "Configure your next absence period." : "Configure el siguiente periodo en que estará ausente",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instale también la {calendarappstoreopen}app de Calendario{linkclose} o {calendardocopen}conecte su escritorio y móvil para sincronizar ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Por favor, asegúrese de configurar correctamente {emailopen}el servidor web{linkclose}",
"Calendar server" : "Servidor de calendario",
"Send invitations to attendees" : "Enviar invitaciones a los asistentes",
"Automatically generate a birthday calendar" : "Generar automáticamente un calendario de cumpleaños",
"Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños se generarán mediante un trabajo en segundo plano.",
"Hence they will not be available immediately after enabling but will show up after some time." : "Por ello, no estarán disponibles inmediatamente tras activarlos, sino que aparecerán después de cierto tiempo.",
"Send notifications for events" : "Enviar notificaciones de los eventos",
- "Notifications are sent via background jobs, so these must occur often enough." : "Las notificaciones son enviadas a través de trabajos en segundo plano, por lo que estos deben ocurrir con la suficiente frecuencia.",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Las notificaciones son enviadas a través de trabajos en segundo plano, por lo que estos deben ocurrir con suficiente frecuencia.",
"Send reminder notifications to calendar sharees as well" : "Enviar recordatorio también a los usuarios con los que se comparte el calendario",
- "Reminders are always sent to organizers and attendees." : "Los recordatorios siempre se envía a los organizadores y asistentes.",
+ "Reminders are always sent to organizers and attendees." : "Los recordatorios siempre se envían a los organizadores y asistentes.",
"Enable notifications for events via push" : "Activar notificaciones push para eventos",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instala también la {calendarappstoreopen}app de Calendario{linkclose} o {calendardocopen}conecta tu escritorio y móvil para sincronizar ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Por favor, asegúrate de configurar correctamente {emailopen}el servidor web{linkclose}",
+ "Example content" : "Contenido de ejemplo",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "El contenido de ejemplo sirve para mostrar las características de Nextcloud. Se proporciona un contenido predeterminado con Nextcloud, y puede ser reemplazado por un contenido personalizado.",
"There was an error updating your attendance status." : "Ha habido un error al actualizar tu estado de asistencia.",
"Please contact the organizer directly." : "Por favor, contacta directamente con el organizador.",
"Are you accepting the invitation?" : "¿Aceptas la invitación?",
"Tentative" : "Provisional",
- "Number of guests" : "Número de invitados",
- "Comment" : "Comentario",
- "Your attendance was updated successfully." : "Tu asistencia se ha actualizado con éxito.",
- "Calendar and tasks" : "Calendario y tareas"
+ "Your attendance was updated successfully." : "Tu asistencia se ha actualizado con éxito."
},
-"nplurals=2; plural=(n != 1);");
+"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;");
diff --git a/apps/dav/l10n/es.json b/apps/dav/l10n/es.json
index ffbdda2ef18..ba95b4a0296 100644
--- a/apps/dav/l10n/es.json
+++ b/apps/dav/l10n/es.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Calendario",
- "Todos" : "Todos",
+ "Tasks" : "Tareas",
"Personal" : "Personal",
"{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
"You created calendar {calendar}" : "Usted creó el calendario {calendar}",
@@ -8,12 +8,12 @@
"You deleted calendar {calendar}" : "Usted eliminó el calendario {calendar}",
"{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
"You updated calendar {calendar}" : "Usted actualizó el calendario {calendar}",
- "{actor} restored calendar {calendar}" : "{actor} ha restablecido el calendario {calendar}",
- "You restored calendar {calendar}" : "Has restablecido el calendario {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} ha restaurado el calendario {calendar}",
+ "You restored calendar {calendar}" : "Ud. ha restaurado el calendario {calendar}",
"You shared calendar {calendar} as public link" : "Has compartido el calendario {calendar} con un enlace público",
"You removed public link for calendar {calendar}" : "Has eliminado el enlace público al calendario {calendar}",
"{actor} shared calendar {calendar} with you" : "{actor} compartió el calendario {calendar} con usted",
- "You shared calendar {calendar} with {user}" : "Usted compartió el calendario {calendar} con {usuario}",
+ "You shared calendar {calendar} with {user}" : "Usted compartió el calendario {calendar} con {user}",
"{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
"{actor} unshared calendar {calendar} from you" : "{actor} dejó de compartir el calendario {calendar} con usted",
"You unshared calendar {calendar} from {user}" : "Usted dejó de compartir el calendario {calendar} de {user}",
@@ -23,41 +23,46 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
"You unshared calendar {calendar} from group {group}" : "Usted dejó de compartir el calendario {calendar} del grupo {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendario {calendar} del grupo {group}",
+ "Untitled event" : "Evento sin título",
"{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
"You created event {event} in calendar {calendar}" : "Usted creó el evento {event} en el calendario {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} eliminó el evento {event} del calendario {calendar}",
"You deleted event {event} from calendar {calendar}" : "Usted eliminó el evento {event} del calendario {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
"You updated event {event} in calendar {calendar}" : "Usted actualizó el evento {event} en el calendario {calendar}",
- "{actor} restored event {event} of calendar {calendar}" : "{actor} ha restablecido el evento {event} del calendario {calendar}",
- "You restored event {event} of calendar {calendar}" : "Has reestablecido el evento {event} del calendario {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} ha movido el evento {event} del calendario {sourceCalendar} al calendario {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Ud. ha movido el evento {event} del calendario {sourceCalendar} al calendario {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} ha restaurado el evento {event} del calendario {calendar}",
+ "You restored event {event} of calendar {calendar}" : "Ud. ha restaurado el evento {event} del calendario {calendar}",
"Busy" : "Ocupado",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó la tarea {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Usted creó la tarea {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} eliminó la tarea {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Usted eliminó la tarea {tod} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó la tarea {todo }en la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Usted actualizó la tarea {todo} en la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} completó la tarea {todo} en la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Usted completó la tarea {todo} en la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió la tarea {todo} en la lista {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Usted reabrió la tarea {todo} en la lista {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} ha creado la tarea {todo} en la lista {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Ud. ha creado la tarea {todo} en la lista {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} ha eliminado la tarea{todo} de la lista {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Ud. ha eliminado la tarea {todo} de la lista {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} ha actualizado la tarea {todo} en la lista {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Ud. ha actualizado la tarea {todo} en la lista {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} ha resuelto la tarea {todo} de la lista {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Ud. ha resuelto la tarea {todo} de la lista {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} ha reabierto la tarea {todo} en la lista {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Ud. ha reabierto la tarea {todo} en la lista {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} ha movido la tarea {todo} de la lista {sourceCalendar} a la lista{targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Ud. ha movido la tarea {todo} de la lista {sourceCalendar} a la lista {targetCalendar}",
"Calendar, contacts and tasks" : "Calendario, contactos y tareas",
"A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado.",
"A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> del calendario fue modificado.",
- "A calendar <strong>todo</strong> was modified" : "Una <strong>lista de tareas</strong> fue modificada",
+ "A calendar <strong>to-do</strong> was modified" : "Se ha modificado una <strong>tarea</strong> de calendario",
"Contact birthdays" : "Cumpleaños del contacto",
"Death of %s" : "Muerte de %s",
+ "Untitled calendar" : "Calendario sin título",
"Calendar:" : "Calendario:",
"Date:" : "Fecha:",
"Where:" : "Dónde:",
"Description:" : "Descripción:",
- "Untitled event" : "Evento sin título",
- "_%n year_::_%n years_" : ["%n año","%n años"],
- "_%n month_::_%n months_" : ["%n mes","%n meses"],
- "_%n day_::_%n days_" : ["%n día","%n días"],
- "_%n hour_::_%n hours_" : ["%n hora","%n horas"],
- "_%n minute_::_%n minutes_" : ["%n minuto","%n minutos"],
+ "_%n year_::_%n years_" : ["%n año","%n años","%n años"],
+ "_%n month_::_%n months_" : ["%n mes","%n meses","%n meses"],
+ "_%n day_::_%n days_" : ["%n día","%n días","%n días"],
+ "_%n hour_::_%n hours_" : ["%n hora","%n horas","%n horas"],
+ "_%n minute_::_%n minutes_" : ["%n minuto","%n minutos","%n minutos"],
"%s (in %s)" : "%s (en %s)",
"%s (%s ago)" : "%s (hace %s)",
"Calendar: %s" : "Calendario: %s",
@@ -65,108 +70,267 @@
"Description: %s" : "Descripción: %s",
"Where: %s" : "Lugar: %s",
"%1$s via %2$s" : "%1$s vía %2$s",
+ "In the past on %1$s for the entire day" : "En el pasado el %1$s durante todo el día",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["En un minuto el %1$s durante todo el día","En %n minutos el %1$s durante todo el día","En %n minutos el %1$s durante todo el día"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["En una hora el %1$s durante todo el día","En %n horas el %1$s durante todo el día","En %n horas el %1$s durante todo el día"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["En un día el %1$s durante todo el día","En %n días el %1$s durante todo el día","En %n días el %1$s durante todo el día"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["En una semana el %1$s durante todo el día","En %n semanas el %1$s durante todo el día","En %n semanas el %1$s durante todo el día"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["En un mes el %1$s durante todo el día","En %n meses el %1$s durante todo el día","En %n meses el %1$s durante todo el día"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["En un año el %1$s durante todo el día","En %n años el %1$s durante todo el día","En %n años el %1$s durante todo el día"],
+ "In the past on %1$s between %2$s - %3$s" : "En el pasado el %1$s entre %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["En un minuto el %1$s entre %2$s - %3$s","En %n minutos el %1$s entre %2$s - %3$s","En %n minutos el %1$s entre %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["En una hora el %1$s entre %2$s - %3$s","En %n horas el %1$s entre %2$s - %3$s","En %n horas el %1$s entre %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["En un día el %1$s entre %2$s - %3$s","En %n días el %1$s entre %2$s - %3$s","En %n días el %1$s entre %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["En una semana el %1$s entre %2$s - %3$s","En %n semanas el %1$s entre %2$s - %3$s","En %n semanas el %1$s entre %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["En un mes el %1$s entre %2$s - %3$s","En %n meses el l%1$s entre %2$s - %3$s ","En %n meses el %1$s entre %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["En un año el %1$s entre %2$s - %3$s","En %n años el %1$s entre %2$s - %3$s","En %n años el %1$s entre %2$s - %3$s"],
+ "Could not generate when statement" : "No se pudo generar la declaración de cuándo",
+ "Every Day for the entire day" : "Todos los días, durante todo el día",
+ "Every Day for the entire day until %1$s" : "Cada día, todo el día hasta el %1$s",
+ "Every Day between %1$s - %2$s" : "Todos los días entre %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Todos los días entre %1$s - %2$s hasta %3$s",
+ "Every %1$d Days for the entire day" : "Cada %1$d días, durante todo el día",
+ "Every %1$d Days for the entire day until %2$s" : "Cada %1$d días, durante todo el día, hasta el %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Cada %1$d días, entre las %2$s y %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Cada %1$d días, entre las %2$s y %3$s hasta el %4$s",
+ "Could not generate event recurrence statement" : "No se ha podido generar la declaración de recurrencia del evento",
+ "Every Week on %1$s for the entire day" : "Cada semana el %1$s, durante todo el día",
+ "Every Week on %1$s for the entire day until %2$s" : "Cada %1$s días, durante todo el día, hasta el %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Cada semana el %1$s entre %2$s y %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Cada semana el %1$s entre %2$s y %3$s hasta el %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Cada %1$d semanas el %2$s todo el día",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Cada %1$d semanas el %2$s durante todo el día hasta %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Cada %1$d semanas el %2$s entre %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Cada %1$d semanas el %2$s entre %3$s - %4$s hasta %5$s",
+ "Every Month on the %1$s for the entire day" : "Cada mes el día %1$s, durante todo el día",
+ "Every Month on the %1$s for the entire day until %2$s" : "Cada mes el día %1$s durante todo el día hasta %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Cada mes el día %1$s entre %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Cada mes el día %1$s entre %2$s - %3$s hasta %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Cada %1$d meses el día %2$s, durante todo el día",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Cada %1$d meses el día %2$s, durante todo el día hasta %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Cada %1$d meses el día %2$s entre %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Cada %1$d meses el día %2$s entre %3$s - %4$s hasta %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Cada año en %1$s el día %2$s, durante todo el día",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Cada año en %1$s el día %2$s, durante todo el día, hasta %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Cada año en %1$s el día %2$s entre %3$s - %4$s.",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Cada año en %1$s el día %2$s entre %3$s - %4$s hasta %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Cada %1$d años en %2$s el día %3$s, durante todo el día",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Cada %1$d años en %2$s el día %3$s, durante todo el día, hasta %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Cada %1$d años en %2$s el día %3$s entre %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Cada %1$d años en %2$s el día %3$s entre %4$s - %5$s hasta %6$s",
+ "On specific dates for the entire day until %1$s" : "En fechas específicas, durante todo el día, hasta %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "En fechas específicas entre %1$s - %2$s hasta %3$s",
+ "In the past on %1$s" : "En el pasado el %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["En un minuto el %1$s","En %n minutos el %1$s","En %n minutos el %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["En una hora el %1$s","En %n horas el %1$s","En %n horas el %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["En un día el %1$s","En %n días el %1$s","En %n días el %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["En una semana el %1$s","En %n semanas el %1$s","En %n semanas el %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["En un mes el %1$s","En %n meses el %1$s","En %n meses el %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["En un año el %1$s","En %n años el %1$s","En %n años el %1$s"],
+ "In the past on %1$s then on %2$s" : "En el pasado el %1$s y luego el %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["En un minuto el %1$s y luego el %2$s","En %n minutos el %1$s y luego el %2$s","En %n minutos el %1$s y luego el %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["En una hora el %1$s y luego el %2$s ","En %n horas el %1$s y luego el %2$s","En %n horas el %1$s y luego el %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["En un día el %1$s y luego el %2$s","En %n días el %1$s y luego el %2$s","En %n días el %1$s y luego el %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["En una semana el %1$s y luego el %2$s","En %n semanas el %1$s y luego el %2$s","En %n semanas el %1$s y luego el %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["En un mes el %1$s y luego el %2$s","En %n meses el %1$s y luego el %2$s","En %n meses el %1$s y luego el %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["En un año el %1$s y luego el %2$s","En %n años el %1$s y luego el %2$s","En %n años el %1$s y luego el %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "En el pasado el %1$s, luego el %2$s y %3$s",
+ "_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_" : ["En un minuto el %1$s y luego el %2$s y %3$s","En %n minutos el %1$s y luego el %2$s y %3$s","En %n minutos el %1$s y luego el %2$s y %3$s"],
+ "_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_" : ["En una hora el %1$s y luego el %2$s y %3$s","En %n horas el %1$s y luego el %2$s y %3$s","En %n horas el %1$s y luego el %2$s y %3$s"],
+ "_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_" : ["En un día el %1$s y luego el %2$s y %3$s","En %n días el %1$s y luego el %2$s y %3$s","En %n días el %1$s y luego el %2$s y %3$s"],
+ "_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_" : ["En una semana el %1$s y luego el %2$s y %3$s","En %n semanas el %1$s y luego el %2$s y %3$s","En %n semanas el %1$s y luego el %2$s y %3$s"],
+ "_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_" : ["En un mes el %1$s y luego el %2$s y %3$s","En %n meses el %1$s y luego el %2$s y %3$s","En %n meses el %1$s y luego el %2$s y %3$s"],
+ "_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_" : ["En un año el %1$s y luego el %2$s y %3$s","En %n años el %1$s y luego el %2$s y %3$s","En %n años el %1$s y luego el %2$s y %3$s"],
+ "Could not generate next recurrence statement" : "No se ha podido generar la declaración de la siguiente recurrencia",
"Cancelled: %1$s" : "Cancelado: %1$s",
- "Invitation canceled" : "Invitación cancelada",
+ "\"%1$s\" has been canceled" : "\"%1$s\" ha sido cancelada",
"Re: %1$s" : "Re: %1$s",
- "Invitation updated" : "Invitación actualizada",
+ "%1$s has accepted your invitation" : "%1$s ha aceptado su invitación",
+ "%1$s has tentatively accepted your invitation" : "%1$s ha aceptado su invitación de forma tentativa",
+ "%1$s has declined your invitation" : "%1$s ha rechazado su invitación",
+ "%1$s has responded to your invitation" : "%1$s ha respondido a su invitación",
+ "Invitation updated: %1$s" : "Invitación actualizada: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s actualizó el evento \"%2$s\"",
"Invitation: %1$s" : "Invitación: %1$s",
- "Invitation" : "Invitación",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s desea invitarle a \"%2$s\"",
+ "Organizer:" : "Organizador:",
+ "Attendees:" : "Asistentes:",
"Title:" : "Título:",
- "Time:" : "Hora:",
+ "When:" : "Cuándo:",
"Location:" : "Ubicación:",
"Link:" : "Enlace:",
- "Organizer:" : "Organizador:",
- "Attendees:" : "Asistentes:",
+ "Occurring:" : "Repetición:",
"Accept" : "Aceptar",
"Decline" : "Rechazar",
"More options …" : "Más opciones...",
"More options at %s" : "Más opciones en %s",
+ "Monday" : "Lunes",
+ "Tuesday" : "Martes",
+ "Wednesday" : "Miércoles",
+ "Thursday" : "Jueves",
+ "Friday" : "Viernes",
+ "Saturday" : "Sábado",
+ "Sunday" : "Domingo",
+ "January" : "Enero",
+ "February" : "Febrero",
+ "March" : "Marzo",
+ "April" : "Abril",
+ "May" : "Mayo",
+ "June" : "Junio",
+ "July" : "Julio",
+ "August" : "Agosto",
+ "September" : "Septiembre",
+ "October" : "Octubre",
+ "November" : "Noviembre",
+ "December" : "Diciembre",
+ "First" : "Primero",
+ "Second" : "Segundo",
+ "Third" : "Tercero",
+ "Fourth" : "Cuarto",
+ "Fifth" : "Quinto",
+ "Last" : "Último",
+ "Second Last" : "Penúltima",
+ "Third Last" : "Antepenúltima",
+ "Fourth Last" : "Ante antepenúltimo",
+ "Fifth Last" : "Quinto último",
"Contacts" : "Contactos",
"{actor} created address book {addressbook}" : "{actor} ha creado la libreta de direcciones {addressbook}",
- "You created address book {addressbook}" : "Has creado la libreta de direcciones {adressbook}",
+ "You created address book {addressbook}" : "Ud. ha creado la libreta de direcciones {addressbook}",
"{actor} deleted address book {addressbook}" : "{actor} ha eliminado la libreta de direcciones {addressbook}",
- "You deleted address book {addressbook}" : "Has eliminado la libreta de direcciones {addressbook}",
+ "You deleted address book {addressbook}" : "Ud. ha eliminado la libreta de direcciones {addressbook}",
"{actor} updated address book {addressbook}" : "{actor} ha actualizado la libreta de direcciones {addressbook}",
- "You updated address book {addressbook}" : "Has actualizado la libreta de direcciones {addressbook}",
+ "You updated address book {addressbook}" : "Ud. ha actualizado la libreta de direcciones {addressbook}",
"{actor} shared address book {addressbook} with you" : "{actor} ha compartido la libreta de direcciones {addressbook} contigo",
- "You shared address book {addressbook} with {user}" : "Has compartido la libreta de direcciones {addressbook} con {user}",
+ "You shared address book {addressbook} with {user}" : "Ud. ha compartido la libreta de direcciones {addressbook} con {user}",
"{actor} shared address book {addressbook} with {user}" : "{actor} ha compartido la libreta de direcciones {addressbook} con {user}",
- "{actor} unshared address book {addressbook} from you" : "{actor} ha descompartido la libreta de direcciones {addressbook} contigo",
- "You unshared address book {addressbook} from {user}" : "Has descompartido la libreta de direcciones {addressbook} con {user}",
- "{actor} unshared address book {addressbook} from {user}" : "{actor} ha descompartido la libreta de direcciones {addressbook} con {user}",
- "{actor} unshared address book {addressbook} from themselves" : "{actor} ha descompartido la libreta de direcciones {addressbook} con ellos mismos",
- "You shared address book {addressbook} with group {group}" : "Has compartido la libreta de direcciones {addressbook} con el grupo {group}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} ha dejado de compartir la libreta de direcciones {addressbook} con Ud.",
+ "You unshared address book {addressbook} from {user}" : "Ud. ha dejado de compartir la libreta de direcciones {addressbook} con {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} ha dejado de compartir la libreta de direcciones {addressbook} con {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} dejó de compartir su propia libreta de direcciones {addressbook}",
+ "You shared address book {addressbook} with group {group}" : "Ud. ha compartido la libreta de direcciones {addressbook} con el grupo {group}",
"{actor} shared address book {addressbook} with group {group}" : "{actor} ha compartido la libreta de direcciones {addressbook} con el grupo {group}",
- "You unshared address book {addressbook} from group {group}" : "Has descompartido la libreta de direcciones {addressbook} con el grupo {group}",
- "{actor} unshared address book {addressbook} from group {group}" : "{actor} ha descompartido la libreta de direcciones {addressbook} con el grupo {group}",
- "{actor} created contact {card} in address book {addressbook}" : "{actor} ha creado el contacto {card} en la libreta de direcciones {addressbook}",
- "You created contact {card} in address book {addressbook}" : "Has creado un contacto {card} en la libreta de direcciones {addressbook}",
+ "You unshared address book {addressbook} from group {group}" : "Ud. ha dejado de compartir la libreta de direcciones {addressbook} con el grupo {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} ha dejado de compartir la libreta de direcciones {addressbook} con el grupo {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} ha creado el contacto {card} en la libreta de direcciones {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Ud. ha creado un contacto {card} en la libreta de direcciones {addressbook}",
"{actor} deleted contact {card} from address book {addressbook}" : "{actor} ha eliminado el contacto {card} de la libreta de direcciones {addressbook}",
- "You deleted contact {card} from address book {addressbook}" : "Has eliminado el contacto {card} de la libreta de direcciones {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Ud. ha eliminado el contacto {card} de la libreta de direcciones {addressbook}",
"{actor} updated contact {card} in address book {addressbook}" : "{actor} ha actualizado el contacto {card} en la libreta de direcciones {addressbook}",
- "You updated contact {card} in address book {addressbook}" : "Has actualizado el contacto {card} en la libreta de direcciones {addressbook}",
- "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Se ha modificado un <strong>contacto</strong> o una <strong>libreta de direcciones</strong> ",
+ "You updated contact {card} in address book {addressbook}" : "Ud. ha actualizado el contacto {card} en la libreta de direcciones {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Se ha modificado un <strong>contacto</strong> o una <strong>libreta de direcciones</strong>",
+ "Accounts" : "Cuentas",
+ "System address book which holds all accounts" : "Libreta de direcciones del sistema que contiene todas las cuentas",
"File is not updatable: %1$s" : "El archivo no se puede actualizar: %1$s",
+ "Failed to get storage for file" : "Error al obtener datos de almacenamiento para el archivo",
+ "Could not write to final file, canceled by hook" : "No se pudo escribir en el archivo final, cancelado por el sistema",
"Could not write file contents" : "No se han podido escribir los contenidos del archivo",
- "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes","%n bytes"],
"Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Error al copiar el archivo al destino (copiado: %1$s, tamaño esperado: %2$s)",
+ "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." : "Se esperaba un tamaño de archivo de %1$s pero se leyó (desde el cliente Nextcloud) y se escribió (en el almacenamiento Nextcloud) %2$s. Podría ser un problema de red en el lado del envío o un problema de escritura en el almacenamiento en el lado del servidor.",
+ "Could not rename part file to final file, canceled by hook" : "No se pudo renombrar del archivo parcial como el archivo final, cancelado por el sistema.",
"Could not rename part file to final file" : "No se ha podido renombrar el archivo parcial como el archivo final",
"Failed to check file size: %1$s" : "Fallo al comprobar el tamaño del archivo: %1$s",
- "Could not open file" : "No se ha podido abrir el archivo",
+ "Could not open file: %1$s, file does seem to exist" : "No se pudo abrir el archivo: %1$s, parece que el archivo existe",
+ "Could not open file: %1$s, file doesn't seem to exist" : "No se pudo abrir el archivo: %1$s, parece que el archivo no existe",
+ "Encryption not ready: %1$s" : "El cifrado no está listo: %1$s",
"Failed to open file: %1$s" : "Fallo al abrir el archivo: %1$s",
"Failed to unlink: %1$s" : "Fallo al desenlazar: %1$s",
- "Could not rename part file assembled from chunks" : "No se ha podido renombrar el archivo parcial formado por los fragmentos",
+ "Failed to write file contents: %1$s" : "Fallo al escribir el contenido del archivo: %1$s",
"File not found: %1$s" : "Archivo no encontrado: %1$s",
+ "Invalid target path" : "Ruta de destino inválida",
"System is in maintenance mode." : "Sistema está en modo de mantenimiento.",
"Upgrade needed" : "Se necesita actualizar",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Tu %s necesita configurarse para usar HTTPS en caso de usar CalDAV y CardDAV con iOS/macOS.",
"Configures a CalDAV account" : "Configura una cuenta CalDAV",
"Configures a CardDAV account" : "Configura una cuenta CardDAV",
"Events" : "Eventos",
- "Tasks" : "Tareas",
"Untitled task" : "Tarea sin título",
"Completed on %s" : "Completado el %s",
"Due on %s by %s" : "Finaliza el %s por %s",
"Due on %s" : "Finaliza el %s",
- "Migrated calendar (%1$s)" : "Calendario migrado (%1$s)",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "¡Bienvenido a Nextcloud Calendar!\n\nEste es un evento de ejemplo - ¡explore la flexibilidad de planear con Nextcloud Calendar editando cuantas veces quiera!\n\nCon Nextcloud Calendar, podrá:\n- Crear, editar, y administrar eventos fácilmente.\n- Crear múltiples calendarios y compartirlos con su equipo de trabajo, amigos, o su familia.\n- Verificar la disponibilidad y mostrar sus tiempos de ocupado a otros.\n- Se integra de manera transparente con otras apps y dispositivos via CalDAV.\n- Personalice su experiencia: Programe eventos recurrentes, ajuste las notificaciones, así como otros ajustes.",
+ "Example event - open me!" : "Evento de ejemplo - ¡ábralo!",
+ "System Address Book" : "Libreta de Direcciones del Sistema",
+ "The system address book contains contact information for all users in your instance." : "La libreta de direcciones del sistema contiene la información de contacto de todos los usuarios de su instancia.",
+ "Enable System Address Book" : "Habilitar la Libreta de Direcciones del Sistema",
+ "DAV system address book" : "Libreta de direcciónes DAV del sistema",
+ "No outstanding DAV system address book sync." : "No hay una sincronización pendiente en la libreta de direcciones DAV del sistema.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "La sincronización DAV de la libreta de direcciones del sistema no se ha ejecutado ya que su instancia tiene más de 1000 usuarios o por que ha ocurrido un error. Por favor, ejecútela manualmente llamando al comando: \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "Endpoint WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "No se pudo verificar si su servidor web está adecuadamente configurado para permitir la sincronización de archivos a través de WebDAV. Por favor, verifique manualmente.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Su servidor web todavía no está configurado correctamente para permitir la sincronización de archivos, porque la interfaz WebDAV parece estar rota.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Su servidor web está adecuadamente configurado para permitir la sincronización de archivos a través de WebDAV.",
+ "Migrated calendar (%1$s)" : "Se migró el calendario (%1$s)",
"Calendars including events, details and attendees" : "Calendarios que incluyen eventos, detalles y asistentes",
"Contacts and groups" : "Contactos y grupos",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "Extremo de WebDAV",
- "Availability" : "Disponibilidad",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Si configura su horario de trabajo, otros usuarios verán cuándo está fuera de la oficina cuando planifiquen una reunión.",
+ "Absence saved" : "Ausencia guardada",
+ "Failed to save your absence settings" : "Error al guardar sus ajustes de ausencia",
+ "Absence cleared" : "Ausencia limpiada",
+ "Failed to clear your absence settings" : "Error al borrar sus ajustes de ausencia",
+ "First day" : "Primer día",
+ "Last day (inclusive)" : "Último día (incluido)",
+ "Out of office replacement (optional)" : "Sustituto durante vacaciones/ausencia (opcional)",
+ "Name of the replacement" : "Nombre del sustituto",
+ "No results." : "Sin resultados.",
+ "Start typing." : "Empiece a escribir.",
+ "Short absence status" : "Estado de ausencia corta",
+ "Long absence Message" : "Mensaje de ausencia larga",
+ "Save" : "Guardar",
+ "Disable absence" : "Deshabilitar ausencia",
+ "Failed to load availability" : "No se ha podido cargar la disponibilidad",
+ "Saved availability" : "Disponibilidad guardada",
+ "Failed to save availability" : "No se ha podido guardar la disponibilidad",
"Time zone:" : "Zona horaria:",
"to" : "para",
- "Delete slot" : "Eliminar espacio",
- "No working hours set" : "No se han establecido horas de funcionamiento",
- "Add slot" : "Añadir espacio",
- "Monday" : "Lunes",
- "Tuesday" : "Martes",
- "Wednesday" : "Miércoles",
- "Thursday" : "Jueves",
- "Friday" : "Viernes",
- "Saturday" : "Sábado",
- "Sunday" : "Domingo",
- "Save" : "Guardar",
+ "Delete slot" : "Eliminar franja de tiempo",
+ "No working hours set" : "No se han establecido horas laborales",
+ "Add slot" : "Añadir franja horaria",
+ "Weekdays" : "Días de semana",
+ "Pick a start time for {dayName}" : "Elija una hora de inicio para {dayName}",
+ "Pick a end time for {dayName}" : "Elija una hora fin para {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Cambiar automáticamente el estado del usuario a \"No molestar\" fuera de las horas de disponibilidad para silenciar todas las notificaciones.",
+ "Cancel" : "Cancelar",
+ "Import" : "Importar",
+ "Error while saving settings" : "Error al guardar los ajustes",
+ "Contact reset successfully" : "El contacto fue restablecido exitosamente",
+ "Error while resetting contact" : "Error al restablecer el contacto",
+ "Contact imported successfully" : "El contacto se importó exitosamente",
+ "Error while importing contact" : "Error al importar el contacto",
+ "Import contact" : "Importar contacto",
+ "Reset to default" : "Restablecer a predeterminado",
+ "Import contacts" : "Importar contactos",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Importar un nuevo archivo .vcf eliminará el contacto predeterminado existente y lo reemplazará con el nuevo. ¿Desea continuar?",
+ "Failed to save example event creation setting" : "Fallo al guardar el ajuste de creación de evento de ejemplo",
+ "Failed to upload the example event" : "Fallo al cargar el evento de ejemplo",
+ "Custom example event was saved successfully" : "El evento de ejemplo personalizado se guardó exitosamente",
+ "Failed to delete the custom example event" : "Fallo al guardar el evento de ejemplo personalizado",
+ "Custom example event was deleted successfully" : "El evento de ejemplo personalizado se eliminó exitosamente",
+ "Import calendar event" : "Importar evento del calendario",
+ "Uploading a new event will overwrite the existing one." : "Cargar un evento nuevo sobrescribirá el existente. ",
+ "Upload event" : "Cargar evento",
+ "Availability" : "Disponibilidad",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Si configura sus horas laborales, otras personas verán cuando está fuera de la oficina cuando agenden una reunión.",
+ "Absence" : "Ausencia",
+ "Configure your next absence period." : "Configure el siguiente periodo en que estará ausente",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instale también la {calendarappstoreopen}app de Calendario{linkclose} o {calendardocopen}conecte su escritorio y móvil para sincronizar ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Por favor, asegúrese de configurar correctamente {emailopen}el servidor web{linkclose}",
"Calendar server" : "Servidor de calendario",
"Send invitations to attendees" : "Enviar invitaciones a los asistentes",
"Automatically generate a birthday calendar" : "Generar automáticamente un calendario de cumpleaños",
"Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños se generarán mediante un trabajo en segundo plano.",
"Hence they will not be available immediately after enabling but will show up after some time." : "Por ello, no estarán disponibles inmediatamente tras activarlos, sino que aparecerán después de cierto tiempo.",
"Send notifications for events" : "Enviar notificaciones de los eventos",
- "Notifications are sent via background jobs, so these must occur often enough." : "Las notificaciones son enviadas a través de trabajos en segundo plano, por lo que estos deben ocurrir con la suficiente frecuencia.",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Las notificaciones son enviadas a través de trabajos en segundo plano, por lo que estos deben ocurrir con suficiente frecuencia.",
"Send reminder notifications to calendar sharees as well" : "Enviar recordatorio también a los usuarios con los que se comparte el calendario",
- "Reminders are always sent to organizers and attendees." : "Los recordatorios siempre se envía a los organizadores y asistentes.",
+ "Reminders are always sent to organizers and attendees." : "Los recordatorios siempre se envían a los organizadores y asistentes.",
"Enable notifications for events via push" : "Activar notificaciones push para eventos",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instala también la {calendarappstoreopen}app de Calendario{linkclose} o {calendardocopen}conecta tu escritorio y móvil para sincronizar ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Por favor, asegúrate de configurar correctamente {emailopen}el servidor web{linkclose}",
+ "Example content" : "Contenido de ejemplo",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "El contenido de ejemplo sirve para mostrar las características de Nextcloud. Se proporciona un contenido predeterminado con Nextcloud, y puede ser reemplazado por un contenido personalizado.",
"There was an error updating your attendance status." : "Ha habido un error al actualizar tu estado de asistencia.",
"Please contact the organizer directly." : "Por favor, contacta directamente con el organizador.",
"Are you accepting the invitation?" : "¿Aceptas la invitación?",
"Tentative" : "Provisional",
- "Number of guests" : "Número de invitados",
- "Comment" : "Comentario",
- "Your attendance was updated successfully." : "Tu asistencia se ha actualizado con éxito.",
- "Calendar and tasks" : "Calendario y tareas"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
+ "Your attendance was updated successfully." : "Tu asistencia se ha actualizado con éxito."
+},"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"
} \ No newline at end of file
diff --git a/apps/dav/l10n/es_419.js b/apps/dav/l10n/es_419.js
deleted file mode 100644
index ba78d8940a8..00000000000
--- a/apps/dav/l10n/es_419.js
+++ /dev/null
@@ -1,66 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/es_419.json b/apps/dav/l10n/es_419.json
deleted file mode 100644
index ec79fad2802..00000000000
--- a/apps/dav/l10n/es_419.json
+++ /dev/null
@@ -1,64 +0,0 @@
-{ "translations": {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/es_AR.js b/apps/dav/l10n/es_AR.js
deleted file mode 100644
index b15ab1b367e..00000000000
--- a/apps/dav/l10n/es_AR.js
+++ /dev/null
@@ -1,56 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Usted creó el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Usted borró el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Usted actualizó el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} con usted",
- "You shared calendar {calendar} with {user}" : "Usted ha compartido el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} con usted",
- "You unshared calendar {calendar} from {user}" : "Usted ha dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Usted ha compartido el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Usted ha dejado de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Usted creó el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Usted borró el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Usted actualizó el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Usted creo el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Usted borró el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Usted actualizó el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Usted resolvió el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Usted reabrió el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Hello %s," : "Hola %s:",
- "Link:" : "Link:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Technical details" : "Detalles técnicos",
- "Remote Address: %s" : "Dirección remota: %s",
- "Request ID: %s" : "ID de solicitud: %s"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/es_AR.json b/apps/dav/l10n/es_AR.json
deleted file mode 100644
index 40f99876c49..00000000000
--- a/apps/dav/l10n/es_AR.json
+++ /dev/null
@@ -1,54 +0,0 @@
-{ "translations": {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Usted creó el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Usted borró el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Usted actualizó el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} con usted",
- "You shared calendar {calendar} with {user}" : "Usted ha compartido el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} con usted",
- "You unshared calendar {calendar} from {user}" : "Usted ha dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Usted ha compartido el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Usted ha dejado de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Usted creó el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Usted borró el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Usted actualizó el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Usted creo el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Usted borró el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Usted actualizó el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Usted resolvió el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Usted reabrió el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Hello %s," : "Hola %s:",
- "Link:" : "Link:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Technical details" : "Detalles técnicos",
- "Remote Address: %s" : "Dirección remota: %s",
- "Request ID: %s" : "ID de solicitud: %s"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/es_CL.js b/apps/dav/l10n/es_CL.js
deleted file mode 100644
index ba78d8940a8..00000000000
--- a/apps/dav/l10n/es_CL.js
+++ /dev/null
@@ -1,66 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/es_CL.json b/apps/dav/l10n/es_CL.json
deleted file mode 100644
index ec79fad2802..00000000000
--- a/apps/dav/l10n/es_CL.json
+++ /dev/null
@@ -1,64 +0,0 @@
-{ "translations": {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/es_CO.js b/apps/dav/l10n/es_CO.js
deleted file mode 100644
index ba78d8940a8..00000000000
--- a/apps/dav/l10n/es_CO.js
+++ /dev/null
@@ -1,66 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/es_CO.json b/apps/dav/l10n/es_CO.json
deleted file mode 100644
index ec79fad2802..00000000000
--- a/apps/dav/l10n/es_CO.json
+++ /dev/null
@@ -1,64 +0,0 @@
-{ "translations": {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/es_CR.js b/apps/dav/l10n/es_CR.js
deleted file mode 100644
index ba78d8940a8..00000000000
--- a/apps/dav/l10n/es_CR.js
+++ /dev/null
@@ -1,66 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/es_CR.json b/apps/dav/l10n/es_CR.json
deleted file mode 100644
index ec79fad2802..00000000000
--- a/apps/dav/l10n/es_CR.json
+++ /dev/null
@@ -1,64 +0,0 @@
-{ "translations": {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/es_DO.js b/apps/dav/l10n/es_DO.js
deleted file mode 100644
index ba78d8940a8..00000000000
--- a/apps/dav/l10n/es_DO.js
+++ /dev/null
@@ -1,66 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/es_DO.json b/apps/dav/l10n/es_DO.json
deleted file mode 100644
index ec79fad2802..00000000000
--- a/apps/dav/l10n/es_DO.json
+++ /dev/null
@@ -1,64 +0,0 @@
-{ "translations": {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/es_EC.js b/apps/dav/l10n/es_EC.js
index ba78d8940a8..bc36002e1e8 100644
--- a/apps/dav/l10n/es_EC.js
+++ b/apps/dav/l10n/es_EC.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Calendario",
- "Todos" : "Pendientes",
+ "Tasks" : "Tareas",
"Personal" : "Personal",
"{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
"You created calendar {calendar}" : "Creaste el calendario {calendar}",
@@ -10,6 +10,8 @@ OC.L10N.register(
"You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
"{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
"You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} restauró el calendario {calendar}",
+ "You restored calendar {calendar}" : "Restauraste el calendario {calendar}",
"You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
"You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
"{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
@@ -18,49 +20,189 @@ OC.L10N.register(
"{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
"You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
"{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {calendar} con él mismo",
"You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
"{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
"You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
+ "Untitled event" : "Evento sin título",
"{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
"You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
"You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
"You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} movió el evento {event} del calendario {targetCalendar} al calendario {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Moviste el evento {event} del calendario {targetCalendar} al calendario {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} restauró el evento {event} del calendario {calendar}",
+ "You restored event {event} of calendar {calendar}" : "Restauraste el evento {event} del calendario {calendar}",
+ "Busy" : "Ocupado",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} creó la tarea {todo} en la lista {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Creaste la tarea {todo} en la lista {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} eliminó la tarea {todo} de la lista {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Eliminaste la tarea {todo} de la lista {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} actualizó la tarea {todo} en la lista {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Actualizaste la tarea {todo} en la lista {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} marcó como resuelta la tarea {todo} en la lista {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Marcaste como resuelta la tarea {todo} en la lista {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} reabrió la tarea {todo} en la lista {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Reabriste la tarea {todo} en la lista {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} movió la tarea {todo} de la lista {targetCalendar} a la lista {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Moviste la tarea {todo} de la lista {targetCalendar} a la lista {targetCalendar}",
+ "Calendar, contacts and tasks" : "Calendario, contactos y tareas",
"A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
"A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
+ "A calendar <strong>to-do</strong> was modified" : "Se modificó una tarea en el calendario",
"Contact birthdays" : "Cumpleaños del contacto",
+ "Death of %s" : "Fallecimiento de %s",
+ "Untitled calendar" : "Calendario sin título",
+ "Calendar:" : "Calendario:",
+ "Date:" : "Fecha:",
"Where:" : "Dónde:",
"Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
+ "_%n year_::_%n years_" : ["%n year","%n years","%n years"],
+ "_%n month_::_%n months_" : ["%n month","%n months","%n months"],
+ "_%n day_::_%n days_" : ["%n day","%n days","%n days"],
+ "_%n hour_::_%n hours_" : ["%n hora","%n horas","%n horas"],
+ "_%n minute_::_%n minutes_" : ["%n minuto","%n minutos","%n minutos"],
+ "%s (in %s)" : "%s (en %s)",
+ "%s (%s ago)" : "%s (%s atrás)",
+ "Calendar: %s" : "Calendario: %s",
+ "Date: %s" : "Fecha: %s",
+ "Description: %s" : "Descripción: %s",
+ "Where: %s" : "Lugar: %s",
+ "%1$s via %2$s" : "%1$s a través de %2$s",
+ "Cancelled: %1$s" : "Cancelado: %1$s",
+ "\"%1$s\" has been canceled" : "\"%1$s\" ha sido cancelado",
+ "Re: %1$s" : "Re: %1$s",
+ "%1$s has accepted your invitation" : "%1$s ha aceptado tu invitación",
+ "%1$s has tentatively accepted your invitation" : "%1$s ha aceptado tu invitación tentativamente",
+ "%1$s has declined your invitation" : "%1$s ha rechazado tu invitación",
+ "%1$s has responded to your invitation" : "%1$s ha respondido a tu invitación",
+ "Invitation updated: %1$s" : "Invitación actualizada: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s actualizó el evento \"%2$s\"",
+ "Invitation: %1$s" : "Invitación: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s te invita a \"%2$s\"",
+ "Organizer:" : "Organizador:",
+ "Attendees:" : "Asistentes:",
+ "Title:" : "Título:",
+ "When:" : "Cuándo:",
"Location:" : "Ubicación:",
"Link:" : "Enlace:",
"Accept" : "Aceptar",
"Decline" : "Declinar",
+ "More options …" : "Más opciones...",
+ "More options at %s" : "Más opciones en %s",
+ "Monday" : "Lunes",
+ "Tuesday" : "Martes",
+ "Wednesday" : "Miércoles",
+ "Thursday" : "Jueves",
+ "Friday" : "Viernes",
+ "Saturday" : "Sábado",
+ "Sunday" : "Domingo",
+ "January" : "Enero",
+ "February" : "Febrero",
+ "March" : "Marzo",
+ "April" : "Abril",
+ "May" : "Mayo",
+ "June" : "Junio",
+ "July" : "Julio",
+ "August" : "Agosto",
+ "September" : "Septiembre",
+ "October" : "Octubre",
+ "November" : "Noviembre",
+ "December" : "Diciembre",
+ "First" : "Primero",
+ "Last" : "Último",
"Contacts" : "Contactos",
- "Tasks" : "Tareas",
+ "{actor} created address book {addressbook}" : "{actor} creó la libreta de direcciones {addressbook}",
+ "You created address book {addressbook}" : "Creaste la libreta de direcciones {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} eliminó la libreta de direcciones {addressbook}",
+ "You deleted address book {addressbook}" : "Eliminaste la libreta de direcciones {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} actualizó la libreta de direcciones {addressbook}",
+ "You updated address book {addressbook}" : "Actualizaste la libreta de direcciones {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} compartió contigo la libreta de direcciones {addressbook}",
+ "You shared address book {addressbook} with {user}" : "Compartiste la libreta de direcciones {addressbook} con {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} compartió la libreta de direcciones {addressbook} con {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} dejó de compartir contigo la libreta de direcciones {addressbook}",
+ "You unshared address book {addressbook} from {user}" : "Dejaste de compartir la libreta de direcciones {addressbook} con {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} dejó de compartir la libreta de direcciones {addressbook} con {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} dejó de compartir la libreta de direcciones {addressbook} consigo mismo",
+ "You shared address book {addressbook} with group {group}" : "Compartiste la libreta de direcciones {addressbook} con el grupo {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} compartió la libreta de direcciones {addressbook} con el grupo {group}",
+ "You unshared address book {addressbook} from group {group}" : "Dejaste de compartir la libreta de direcciones {addressbook} con el grupo {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} dejó de compartir la libreta de direcciones {addressbook} con el grupo {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} creó el contacto {card} en la libreta de direcciones {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Creaste el contacto {card} en la libreta de direcciones {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} eliminó el contacto {card} de la libreta de direcciones {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Eliminaste el contacto {card} de la libreta de direcciones {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} actualizó el contacto {card} en la libreta de direcciones {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Actualizaste el contacto {card} en la libreta de direcciones {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Se modificó un contacto o una libreta de direcciones",
+ "Accounts" : "Cuentas",
+ "System address book which holds all accounts" : "Libreta de direcciones del sistema que contiene todas las cuentas",
+ "File is not updatable: %1$s" : "No se puede actualizar el archivo: %1$s",
+ "Could not write to final file, canceled by hook" : "No se pudo escribir en el archivo final, cancelado por el gancho",
+ "Could not write file contents" : "No se pudo escribir el contenido del archivo",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes","%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Error al copiar el archivo a la ubicación de destino (copiado: %1$s, tamaño de archivo esperado: %2$s)",
+ "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." : "Tamaño de archivo esperado de %1$s, pero se leyó (desde el cliente de Nextcloud) y se escribió (en el almacenamiento de Nextcloud) %2$s. Puede ser un problema de red en el lado del envío o un problema de escritura en el almacenamiento en el lado del servidor.",
+ "Could not rename part file to final file, canceled by hook" : "No se pudo renombrar el archivo parcial al archivo final, cancelado por el gancho",
+ "Could not rename part file to final file" : "No se pudo renombrar el archivo parcial al archivo final",
+ "Failed to check file size: %1$s" : "Error al verificar el tamaño del archivo: %1$s",
+ "Encryption not ready: %1$s" : "La encriptación no está lista: %1$s",
+ "Failed to open file: %1$s" : "Error al abrir el archivo: %1$s",
+ "Failed to unlink: %1$s" : "Error al eliminar: %1$s",
+ "Failed to write file contents: %1$s" : "Error al escribir el contenido del archivo: %1$s",
+ "File not found: %1$s" : "Archivo no encontrado: %1$s",
+ "System is in maintenance mode." : "El sistema está en modo de mantenimiento.",
+ "Upgrade needed" : "Se necesita una actualización",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Tu %s debe configurarse para usar HTTPS para utilizar CalDAV y CardDAV con iOS/macOS.",
+ "Configures a CalDAV account" : "Configura una cuenta CalDAV",
+ "Configures a CardDAV account" : "Configura una cuenta CardDAV",
+ "Events" : "Eventos",
+ "Untitled task" : "Tarea sin título",
+ "Completed on %s" : "Completada el %s",
+ "Due on %s by %s" : "Vence el %s a las %s",
+ "Due on %s" : "Vence el %s",
+ "WebDAV endpoint" : "Extremo WebDAV",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Tu servidor web aún no esta correctamente configurado para permitir la sincronización de archivos porque la interfaz WebDAV parece estar rota. ",
+ "Migrated calendar (%1$s)" : "Calendario migrado (%1$s)",
+ "Calendars including events, details and attendees" : "Calendarios que incluyen eventos, detalles y asistentes",
+ "Contacts and groups" : "Contactos y grupos",
"WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
+ "First day" : "Primer día",
"Save" : "Guardar",
+ "Failed to load availability" : "Error al cargar la disponibilidad",
+ "Saved availability" : "Disponibilidad guardada",
+ "Failed to save availability" : "Error al guardar la disponibilidad",
+ "Time zone:" : "Zona horaria:",
+ "to" : "para",
+ "Delete slot" : "Eliminar ranura",
+ "No working hours set" : "No se han establecido horas de trabajo",
+ "Add slot" : "Agregar ranura",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Configurar automáticamente el estado del usuario como \"No molestar\" fuera de la disponibilidad para silenciar todas las notificaciones.",
+ "Cancel" : "Cancelar",
+ "Import" : "Importar",
+ "Error while saving settings" : "Error al guardar la configuración.",
+ "Reset to default" : "Restablecer al predeterminado",
+ "Availability" : "Disponibilidad",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "También instala la {calendarappstoreopen}aplicación Calendario{linkclose}, o {calendardocopen}conecta tu escritorio y móvil para sincronizar ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Asegúrate de configurar correctamente {emailopen}el servidor de correo electrónico{linkclose}.",
+ "Calendar server" : "Servidor de calendario",
"Send invitations to attendees" : "Enviar invitaciones a los asistentes",
"Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
"Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
"Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
+ "Send notifications for events" : "Enviar notificaciones para eventos",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Las notificaciones se envían a través de trabajos en segundo plano, por lo que deben ocurrir con la suficiente frecuencia.",
+ "Send reminder notifications to calendar sharees as well" : "Enviar recordatorios a los asistentes del calendario también",
+ "Reminders are always sent to organizers and attendees." : "Los recordatorios siempre se envían a los organizadores y asistentes.",
+ "Enable notifications for events via push" : "Habilitar notificaciones para eventos mediante push",
+ "There was an error updating your attendance status." : "Hubo un error al actualizar tu estado de asistencia.",
+ "Please contact the organizer directly." : "Por favor, contacta directamente al organizador.",
+ "Are you accepting the invitation?" : "¿Aceptas la invitación?",
+ "Tentative" : "Tentativo",
+ "Your attendance was updated successfully." : "Tu asistencia se actualizó correctamente."
},
-"nplurals=2; plural=(n != 1);");
+"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;");
diff --git a/apps/dav/l10n/es_EC.json b/apps/dav/l10n/es_EC.json
index ec79fad2802..5e04a22c11b 100644
--- a/apps/dav/l10n/es_EC.json
+++ b/apps/dav/l10n/es_EC.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Calendario",
- "Todos" : "Pendientes",
+ "Tasks" : "Tareas",
"Personal" : "Personal",
"{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
"You created calendar {calendar}" : "Creaste el calendario {calendar}",
@@ -8,6 +8,8 @@
"You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
"{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
"You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} restauró el calendario {calendar}",
+ "You restored calendar {calendar}" : "Restauraste el calendario {calendar}",
"You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
"You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
"{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
@@ -16,49 +18,189 @@
"{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
"You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
"{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {calendar} con él mismo",
"You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
"{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
"You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
+ "Untitled event" : "Evento sin título",
"{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
"You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
"You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
"You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} movió el evento {event} del calendario {targetCalendar} al calendario {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Moviste el evento {event} del calendario {targetCalendar} al calendario {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} restauró el evento {event} del calendario {calendar}",
+ "You restored event {event} of calendar {calendar}" : "Restauraste el evento {event} del calendario {calendar}",
+ "Busy" : "Ocupado",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} creó la tarea {todo} en la lista {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Creaste la tarea {todo} en la lista {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} eliminó la tarea {todo} de la lista {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Eliminaste la tarea {todo} de la lista {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} actualizó la tarea {todo} en la lista {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Actualizaste la tarea {todo} en la lista {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} marcó como resuelta la tarea {todo} en la lista {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Marcaste como resuelta la tarea {todo} en la lista {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} reabrió la tarea {todo} en la lista {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Reabriste la tarea {todo} en la lista {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} movió la tarea {todo} de la lista {targetCalendar} a la lista {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Moviste la tarea {todo} de la lista {targetCalendar} a la lista {targetCalendar}",
+ "Calendar, contacts and tasks" : "Calendario, contactos y tareas",
"A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
"A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
+ "A calendar <strong>to-do</strong> was modified" : "Se modificó una tarea en el calendario",
"Contact birthdays" : "Cumpleaños del contacto",
+ "Death of %s" : "Fallecimiento de %s",
+ "Untitled calendar" : "Calendario sin título",
+ "Calendar:" : "Calendario:",
+ "Date:" : "Fecha:",
"Where:" : "Dónde:",
"Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
+ "_%n year_::_%n years_" : ["%n year","%n years","%n years"],
+ "_%n month_::_%n months_" : ["%n month","%n months","%n months"],
+ "_%n day_::_%n days_" : ["%n day","%n days","%n days"],
+ "_%n hour_::_%n hours_" : ["%n hora","%n horas","%n horas"],
+ "_%n minute_::_%n minutes_" : ["%n minuto","%n minutos","%n minutos"],
+ "%s (in %s)" : "%s (en %s)",
+ "%s (%s ago)" : "%s (%s atrás)",
+ "Calendar: %s" : "Calendario: %s",
+ "Date: %s" : "Fecha: %s",
+ "Description: %s" : "Descripción: %s",
+ "Where: %s" : "Lugar: %s",
+ "%1$s via %2$s" : "%1$s a través de %2$s",
+ "Cancelled: %1$s" : "Cancelado: %1$s",
+ "\"%1$s\" has been canceled" : "\"%1$s\" ha sido cancelado",
+ "Re: %1$s" : "Re: %1$s",
+ "%1$s has accepted your invitation" : "%1$s ha aceptado tu invitación",
+ "%1$s has tentatively accepted your invitation" : "%1$s ha aceptado tu invitación tentativamente",
+ "%1$s has declined your invitation" : "%1$s ha rechazado tu invitación",
+ "%1$s has responded to your invitation" : "%1$s ha respondido a tu invitación",
+ "Invitation updated: %1$s" : "Invitación actualizada: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s actualizó el evento \"%2$s\"",
+ "Invitation: %1$s" : "Invitación: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s te invita a \"%2$s\"",
+ "Organizer:" : "Organizador:",
+ "Attendees:" : "Asistentes:",
+ "Title:" : "Título:",
+ "When:" : "Cuándo:",
"Location:" : "Ubicación:",
"Link:" : "Enlace:",
"Accept" : "Aceptar",
"Decline" : "Declinar",
+ "More options …" : "Más opciones...",
+ "More options at %s" : "Más opciones en %s",
+ "Monday" : "Lunes",
+ "Tuesday" : "Martes",
+ "Wednesday" : "Miércoles",
+ "Thursday" : "Jueves",
+ "Friday" : "Viernes",
+ "Saturday" : "Sábado",
+ "Sunday" : "Domingo",
+ "January" : "Enero",
+ "February" : "Febrero",
+ "March" : "Marzo",
+ "April" : "Abril",
+ "May" : "Mayo",
+ "June" : "Junio",
+ "July" : "Julio",
+ "August" : "Agosto",
+ "September" : "Septiembre",
+ "October" : "Octubre",
+ "November" : "Noviembre",
+ "December" : "Diciembre",
+ "First" : "Primero",
+ "Last" : "Último",
"Contacts" : "Contactos",
- "Tasks" : "Tareas",
+ "{actor} created address book {addressbook}" : "{actor} creó la libreta de direcciones {addressbook}",
+ "You created address book {addressbook}" : "Creaste la libreta de direcciones {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} eliminó la libreta de direcciones {addressbook}",
+ "You deleted address book {addressbook}" : "Eliminaste la libreta de direcciones {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} actualizó la libreta de direcciones {addressbook}",
+ "You updated address book {addressbook}" : "Actualizaste la libreta de direcciones {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} compartió contigo la libreta de direcciones {addressbook}",
+ "You shared address book {addressbook} with {user}" : "Compartiste la libreta de direcciones {addressbook} con {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} compartió la libreta de direcciones {addressbook} con {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} dejó de compartir contigo la libreta de direcciones {addressbook}",
+ "You unshared address book {addressbook} from {user}" : "Dejaste de compartir la libreta de direcciones {addressbook} con {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} dejó de compartir la libreta de direcciones {addressbook} con {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} dejó de compartir la libreta de direcciones {addressbook} consigo mismo",
+ "You shared address book {addressbook} with group {group}" : "Compartiste la libreta de direcciones {addressbook} con el grupo {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} compartió la libreta de direcciones {addressbook} con el grupo {group}",
+ "You unshared address book {addressbook} from group {group}" : "Dejaste de compartir la libreta de direcciones {addressbook} con el grupo {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} dejó de compartir la libreta de direcciones {addressbook} con el grupo {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} creó el contacto {card} en la libreta de direcciones {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Creaste el contacto {card} en la libreta de direcciones {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} eliminó el contacto {card} de la libreta de direcciones {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Eliminaste el contacto {card} de la libreta de direcciones {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} actualizó el contacto {card} en la libreta de direcciones {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Actualizaste el contacto {card} en la libreta de direcciones {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Se modificó un contacto o una libreta de direcciones",
+ "Accounts" : "Cuentas",
+ "System address book which holds all accounts" : "Libreta de direcciones del sistema que contiene todas las cuentas",
+ "File is not updatable: %1$s" : "No se puede actualizar el archivo: %1$s",
+ "Could not write to final file, canceled by hook" : "No se pudo escribir en el archivo final, cancelado por el gancho",
+ "Could not write file contents" : "No se pudo escribir el contenido del archivo",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes","%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Error al copiar el archivo a la ubicación de destino (copiado: %1$s, tamaño de archivo esperado: %2$s)",
+ "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." : "Tamaño de archivo esperado de %1$s, pero se leyó (desde el cliente de Nextcloud) y se escribió (en el almacenamiento de Nextcloud) %2$s. Puede ser un problema de red en el lado del envío o un problema de escritura en el almacenamiento en el lado del servidor.",
+ "Could not rename part file to final file, canceled by hook" : "No se pudo renombrar el archivo parcial al archivo final, cancelado por el gancho",
+ "Could not rename part file to final file" : "No se pudo renombrar el archivo parcial al archivo final",
+ "Failed to check file size: %1$s" : "Error al verificar el tamaño del archivo: %1$s",
+ "Encryption not ready: %1$s" : "La encriptación no está lista: %1$s",
+ "Failed to open file: %1$s" : "Error al abrir el archivo: %1$s",
+ "Failed to unlink: %1$s" : "Error al eliminar: %1$s",
+ "Failed to write file contents: %1$s" : "Error al escribir el contenido del archivo: %1$s",
+ "File not found: %1$s" : "Archivo no encontrado: %1$s",
+ "System is in maintenance mode." : "El sistema está en modo de mantenimiento.",
+ "Upgrade needed" : "Se necesita una actualización",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Tu %s debe configurarse para usar HTTPS para utilizar CalDAV y CardDAV con iOS/macOS.",
+ "Configures a CalDAV account" : "Configura una cuenta CalDAV",
+ "Configures a CardDAV account" : "Configura una cuenta CardDAV",
+ "Events" : "Eventos",
+ "Untitled task" : "Tarea sin título",
+ "Completed on %s" : "Completada el %s",
+ "Due on %s by %s" : "Vence el %s a las %s",
+ "Due on %s" : "Vence el %s",
+ "WebDAV endpoint" : "Extremo WebDAV",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Tu servidor web aún no esta correctamente configurado para permitir la sincronización de archivos porque la interfaz WebDAV parece estar rota. ",
+ "Migrated calendar (%1$s)" : "Calendario migrado (%1$s)",
+ "Calendars including events, details and attendees" : "Calendarios que incluyen eventos, detalles y asistentes",
+ "Contacts and groups" : "Contactos y grupos",
"WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
+ "First day" : "Primer día",
"Save" : "Guardar",
+ "Failed to load availability" : "Error al cargar la disponibilidad",
+ "Saved availability" : "Disponibilidad guardada",
+ "Failed to save availability" : "Error al guardar la disponibilidad",
+ "Time zone:" : "Zona horaria:",
+ "to" : "para",
+ "Delete slot" : "Eliminar ranura",
+ "No working hours set" : "No se han establecido horas de trabajo",
+ "Add slot" : "Agregar ranura",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Configurar automáticamente el estado del usuario como \"No molestar\" fuera de la disponibilidad para silenciar todas las notificaciones.",
+ "Cancel" : "Cancelar",
+ "Import" : "Importar",
+ "Error while saving settings" : "Error al guardar la configuración.",
+ "Reset to default" : "Restablecer al predeterminado",
+ "Availability" : "Disponibilidad",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "También instala la {calendarappstoreopen}aplicación Calendario{linkclose}, o {calendardocopen}conecta tu escritorio y móvil para sincronizar ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Asegúrate de configurar correctamente {emailopen}el servidor de correo electrónico{linkclose}.",
+ "Calendar server" : "Servidor de calendario",
"Send invitations to attendees" : "Enviar invitaciones a los asistentes",
"Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
"Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
"Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
+ "Send notifications for events" : "Enviar notificaciones para eventos",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Las notificaciones se envían a través de trabajos en segundo plano, por lo que deben ocurrir con la suficiente frecuencia.",
+ "Send reminder notifications to calendar sharees as well" : "Enviar recordatorios a los asistentes del calendario también",
+ "Reminders are always sent to organizers and attendees." : "Los recordatorios siempre se envían a los organizadores y asistentes.",
+ "Enable notifications for events via push" : "Habilitar notificaciones para eventos mediante push",
+ "There was an error updating your attendance status." : "Hubo un error al actualizar tu estado de asistencia.",
+ "Please contact the organizer directly." : "Por favor, contacta directamente al organizador.",
+ "Are you accepting the invitation?" : "¿Aceptas la invitación?",
+ "Tentative" : "Tentativo",
+ "Your attendance was updated successfully." : "Tu asistencia se actualizó correctamente."
+},"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"
} \ No newline at end of file
diff --git a/apps/dav/l10n/es_GT.js b/apps/dav/l10n/es_GT.js
deleted file mode 100644
index ba78d8940a8..00000000000
--- a/apps/dav/l10n/es_GT.js
+++ /dev/null
@@ -1,66 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/es_GT.json b/apps/dav/l10n/es_GT.json
deleted file mode 100644
index ec79fad2802..00000000000
--- a/apps/dav/l10n/es_GT.json
+++ /dev/null
@@ -1,64 +0,0 @@
-{ "translations": {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/es_HN.js b/apps/dav/l10n/es_HN.js
deleted file mode 100644
index ba78d8940a8..00000000000
--- a/apps/dav/l10n/es_HN.js
+++ /dev/null
@@ -1,66 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/es_HN.json b/apps/dav/l10n/es_HN.json
deleted file mode 100644
index ec79fad2802..00000000000
--- a/apps/dav/l10n/es_HN.json
+++ /dev/null
@@ -1,64 +0,0 @@
-{ "translations": {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/es_MX.js b/apps/dav/l10n/es_MX.js
index 8e335eca88d..4c58640df1a 100644
--- a/apps/dav/l10n/es_MX.js
+++ b/apps/dav/l10n/es_MX.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Calendario",
- "Todos" : "Pendientes",
+ "Tasks" : "Tareas",
"Personal" : "Personal",
"{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
"You created calendar {calendar}" : "Creaste el calendario {calendar}",
@@ -10,6 +10,8 @@ OC.L10N.register(
"You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
"{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
"You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} restauró el calendario {calendar}",
+ "You restored calendar {calendar}" : "Ha restaurado el calendario {calendar}",
"You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
"You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
"{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
@@ -18,50 +20,230 @@ OC.L10N.register(
"{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
"You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
"{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {calendar} con él mismo",
"You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
"{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
"You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
+ "Untitled event" : "Evento sin título",
"{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
"You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
"You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
"You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} movió el evento {event} del calendario {sourceCalendar} al calendario {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Ha movido el evento {event} del calendario {sourceCalendar} al calendario {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} restauró el evento {event} del calendario {calendar}",
+ "You restored event {event} of calendar {calendar}" : "Has restaurado el evento {event} del calendario {calendar}",
+ "Busy" : "Ocupado",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} creó la tarea {todo} en la lista {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Ha creado la tarea {todo} en la lista {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} eliminó la tarea {todo} de la lista {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Ha eliminado la tarea {todo} de la lista {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} actualizó la tarea {todo} en la lista {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Ha actualizado la tarea {todo} en la lista {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} resolvió la tarea {todo} en la lista {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Ha resuelto la tarea {todo} de la lista {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} reabrió la tarea {todo} en la lista {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Ha reabierto la tarea {todo} en la lista {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} movió la tarea {todo} de la lista {sourceCalendar} a la lista {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Ha movido la tarea {todo} de la lista {sourceCalendar} a la lista {targetCalendar}",
+ "Calendar, contacts and tasks" : "Calendario, contactos y tareas",
"A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
"A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
+ "A calendar <strong>to-do</strong> was modified" : "Se modificó una <strong>tarea</strong> del calendario",
"Contact birthdays" : "Cumpleaños del contacto",
+ "Death of %s" : "Fallecimiento de %s",
+ "Untitled calendar" : "Calendario sin título",
+ "Calendar:" : "Calendario:",
+ "Date:" : "Fecha:",
"Where:" : "Dónde:",
"Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
+ "_%n year_::_%n years_" : ["%n año","%n años","%n años"],
+ "_%n month_::_%n months_" : ["%n mes","%n meses","%n meses"],
+ "_%n day_::_%n days_" : ["%n día","%n días","%n días"],
+ "_%n hour_::_%n hours_" : ["%n hora","%n horas","%n horas"],
+ "_%n minute_::_%n minutes_" : ["%n minuto","%n minutos","%n minutos"],
+ "%s (in %s)" : "%s (en %s)",
+ "%s (%s ago)" : "%s (hace %s)",
+ "Calendar: %s" : "Calendario: %s",
+ "Date: %s" : "Fecha: %s",
+ "Description: %s" : "Descripción: %s",
+ "Where: %s" : "Lugar: %s",
+ "%1$s via %2$s" : "%1$s vía %2$s",
+ "Could not generate when statement" : "No se pudo generar la declaración de cuándo",
+ "Every Day for the entire day" : "Cada día todo el día",
+ "Every Day for the entire day until %1$s" : "Cada día todo el día hasta el %1$s",
+ "Every Day between %1$s - %2$s" : "Cada día entre las %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Cada día entre las %1$s - %2$s hasta el %3$s",
+ "Every %1$d Days for the entire day" : "Cada %1$d días todo el día",
+ "Every %1$d Days for the entire day until %2$s" : "Cada %1$d días todo el día hasta el %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Cada %1$d días entre las %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Cada %1$d días entre las %2$s - %3$s hasta el %4$s",
+ "Could not generate event recurrence statement" : "No se pudo generar la declaración de recurrencia del evento",
+ "Every Week on %1$s for the entire day" : "Cada semana el %1$s todo el día",
+ "Cancelled: %1$s" : "Cancelado: %1$s",
+ "\"%1$s\" has been canceled" : "\"%1$s\" ha sido cancelado",
+ "Re: %1$s" : "Respecto a: %1$s",
+ "%1$s has accepted your invitation" : "%1$s ha aceptado su invitación",
+ "%1$s has tentatively accepted your invitation" : "%1$s ha aceptado tentativamente su invitación",
+ "%1$s has declined your invitation" : "%1$s ha declinado su invitación",
+ "%1$s has responded to your invitation" : "%1$s ha respondido a su invitación",
+ "Invitation updated: %1$s" : "Invitación actualizada: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s actualizó el evento \"%2$s\"",
+ "Invitation: %1$s" : "Invitación: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s desea invitarlo a \"%2$s\"",
+ "Organizer:" : "Organizador:",
+ "Attendees:" : "Asistentes:",
+ "Title:" : "Título:",
+ "When:" : "Cuándo:",
"Location:" : "Ubicación:",
"Link:" : "Enlace:",
+ "Occurring:" : "Ocurriendo:",
"Accept" : "Aceptar",
"Decline" : "Rechazar",
+ "More options …" : "Más opciones ...",
+ "More options at %s" : "Más opciones en %s",
+ "Monday" : "Lunes",
+ "Tuesday" : "Martes",
+ "Wednesday" : "Miércoles",
+ "Thursday" : "Jueves",
+ "Friday" : "Viernes",
+ "Saturday" : "Sábado",
+ "Sunday" : "Domingo",
+ "January" : "Enero",
+ "February" : "Febrero",
+ "March" : "Marzo",
+ "April" : "Abril",
+ "May" : "Mayo",
+ "June" : "Junio",
+ "July" : "Julio",
+ "August" : "Agosto",
+ "September" : "Septiembre",
+ "October" : "Octubre",
+ "November" : "Noviembre",
+ "December" : "Diciembre",
+ "First" : "Primero",
+ "Second" : "Segundo",
+ "Third" : "Tercero",
+ "Fourth" : "Cuarto",
+ "Last" : "Último",
+ "Second Last" : "Penúltimo",
+ "Third Last" : "Antepenúltimo",
+ "Fourth Last" : "Ante antepenúltimo",
"Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
+ "{actor} created address book {addressbook}" : "{actor} creó la libreta de direcciones {addressbook}",
+ "You created address book {addressbook}" : "Ha creado la libreta de direcciones {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} eliminó la libreta de direcciones {addressbook}",
+ "You deleted address book {addressbook}" : "Ha eliminado la libreta de direcciones {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} actualizó la libreta de direcciones {addressbook}",
+ "You updated address book {addressbook}" : "Ha actualizado la libreta de direcciones {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} le compartió la libreta de direcciones {addressbook}",
+ "You shared address book {addressbook} with {user}" : "Ha compartido la libreta de direcciones {addressbook} con {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} compartió la libreta de direcciones {addressbook} con {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} le ha dejado de compartir la libreta de direcciones {addressbook}",
+ "You unshared address book {addressbook} from {user}" : "Has dejado de compartir la libreta de direcciones {addressbook} con {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} dejó de compartir la libreta de direcciones {addressbook} con {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} dejó de compartir la libreta de direcciones {addressbook} consigo mismo",
+ "You shared address book {addressbook} with group {group}" : "Has compartido la libreta de direcciones {addressbook} con el grupo {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} compartió la libreta de direcciones {addressbook} con el grupo {group}",
+ "You unshared address book {addressbook} from group {group}" : "Ha dejado de compartir la libreta de direcciones {addressbook} con el grupo {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} dejó de compartir la libreta de direcciones {addressbook} con el grupo {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} creó el contacto {card} en la libreta de direcciones {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Ha creado el contacto {card} en la libreta de direcciones {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} eliminó el contacto {card} de la libreta de direcciones {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Ha eliminado el contacto {card} de la libreta de direcciones {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} actualizó el contacto {card} en la libreta de direcciones {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Ha actualizado el contacto {card} en la libreta de direcciones {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Se modificó un <strong>contacto</strong> o una <strong>libreta de direcciones</strong> ",
+ "Accounts" : "Cuentas",
+ "System address book which holds all accounts" : "Libreta de direcciones del sistema que contiene todas las cuentas",
+ "File is not updatable: %1$s" : "No se puede actualizar el archivo: %1$s",
+ "Could not write to final file, canceled by hook" : "No se pudo escribir en el archivo final, cancelado por el sistema",
+ "Could not write file contents" : "No se pudo escribir el contenido del archivo",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes","%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Error al copiar el archivo a la ubicación destino (copiado: %1$s, tamaño de archivo esperado: %2$s)",
+ "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." : "Se esperaba un tamaño de archivo de %1$s pero se leyó (desde el cliente de Nextcloud) y se escribió (en el almacenamiento de Nextcloud) %2$s. Puede ser un problema de red del lado del envío o un problema de escritura en el almacenamiento del lado del servidor.",
+ "Could not rename part file to final file, canceled by hook" : "No se pudo renombrar el archivo parcial al archivo final, cancelado por el sistema",
+ "Could not rename part file to final file" : "No se pudo renombrar el archivo parcial al archivo final",
+ "Failed to check file size: %1$s" : "No se pudo verificar el tamaño del archivo: %1$s",
+ "Encryption not ready: %1$s" : "El cifrado no está listo: %1$s",
+ "Failed to open file: %1$s" : "No se pudo abrir el archivo: %1$s",
+ "Failed to unlink: %1$s" : "No se pudo desenlazar: %1$s",
+ "Failed to write file contents: %1$s" : "No se pudo escribir el contenido del archivo: %1$s",
+ "File not found: %1$s" : "No se encontró el archivo: %1$s",
+ "System is in maintenance mode." : "El sistema está en modo de mantenimiento.",
+ "Upgrade needed" : "Se necesita actualizar",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Su %s debe configurarse para usar HTTPS para utilizar CalDAV y CardDAV con iOS/macOS.",
+ "Configures a CalDAV account" : "Configura una cuenta CalDAV",
+ "Configures a CardDAV account" : "Configura una cuenta CardDAV",
+ "Events" : "Eventos",
+ "Untitled task" : "Tarea sin título",
+ "Completed on %s" : "Completado en %s",
+ "Due on %s by %s" : "Vence el %s por %s",
+ "Due on %s" : "Vence el %s",
+ "DAV system address book" : "Libreta de direcciones DAV del sistema",
+ "No outstanding DAV system address book sync." : "No hay una sincronización pendiente en la libreta de direcciones DAV del sistema.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "La sincronización de la libreta de direcciones DAV del sistema aún no se ha ejecutado, porque su instancia tiene más de 1000 usuarios o porque ocurrió un error. Por favor, ejecútelo manualmente con el comando \"occ dav:sync-system-addressbook\".",
"WebDAV endpoint" : "Endpoint WebDAV",
- "Tentative" : "Tentativo",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "No se pudo verificar si su servidor web está adecuadamente configurado para permitir la sincronización mediante WebDAV. Por favor, revíselo manualmente.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Tu servidor web aún no esta correctamente configurado para permitir la sincronización de archivos porque la interfaz WebDAV parece estar rota. ",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Su servidor web está adecuadamente configurado para permitir la sincronización mediante WebDAV.",
+ "Migrated calendar (%1$s)" : "Calendario migrado (%1$s)",
+ "Calendars including events, details and attendees" : "Calendarios que incluyen eventos, detalles y asistentes",
+ "Contacts and groups" : "Contactos y grupos",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "Ausencia guardada",
+ "Failed to save your absence settings" : "No se pudieron guardar sus ajustes de ausencia",
+ "Absence cleared" : "Ausencia limpiada",
+ "Failed to clear your absence settings" : "No se pudieron limpiar sus ajustes de ausencia",
+ "First day" : "Primer día",
+ "Last day (inclusive)" : "Último día (inclusivo)",
+ "Out of office replacement (optional)" : "Reemplazo para cuando fuera de la oficina (opcional)",
+ "Name of the replacement" : "Nombre del reemplazo",
+ "No results." : "Sin resultados.",
+ "Start typing." : "Empezar a escribir.",
+ "Short absence status" : "Estado de ausencia corta",
+ "Long absence Message" : "Mensaje de ausencia larga",
"Save" : "Guardar",
+ "Disable absence" : "Deshabilitar ausencia",
+ "Failed to load availability" : "No se pudo cargar la disponibilidad",
+ "Saved availability" : "Disponibilidad guardada",
+ "Failed to save availability" : "No se pudo guardar la disponibilidad",
+ "Time zone:" : "Zona horaria:",
+ "to" : "para",
+ "Delete slot" : "Eliminar ranura",
+ "No working hours set" : "No se han establecido las horas de trabajo",
+ "Add slot" : "Añadir ranura",
+ "Weekdays" : "Días laborales",
+ "Pick a start time for {dayName}" : "Elija una hora de inicio para {dayName}",
+ "Pick a end time for {dayName}" : "Elija una hora fin para {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Establecer automáticamente el estado de usuario como \"No molestar\" fuera de la disponibilidad para silenciar todas las notificaciones.",
+ "Cancel" : "Cancelar",
+ "Import" : "Importar",
+ "Error while saving settings" : "Error al guardar la configuración",
+ "Reset to default" : "Restablecer al predeterminado",
+ "Availability" : "Disponibilidad",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Si configura sus horas laborales, otras personas verán cuándo está fuera de la oficina cuando agenden una reunión.",
+ "Absence" : "Ausencia",
+ "Configure your next absence period." : "Configure su siguiente periodo de ausencia.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instale también la {calendarappstoreopen}aplicación de calendario{linkclose} o {calendardocopen}conecte su escritorio y móvil para sincronizar ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Por favor, asegúrese de configurar correctamente {emailopen}el servidor de correo electrónico{linkclose}",
+ "Calendar server" : "Servidor del calendario",
"Send invitations to attendees" : "Enviar invitaciones a los asistentes",
"Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
"Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
"Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
+ "Send notifications for events" : "Enviar notificaciones para eventos",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Las notificaciones son enviadas a través de trabajos en segundo plano, por lo que estos deben ocurrir con suficiente frecuencia.",
+ "Send reminder notifications to calendar sharees as well" : "Enviar recordatorio también a los usuarios con los que se comparte el calendario",
+ "Reminders are always sent to organizers and attendees." : "Los recordatorios se envían siempre a los organizadores y asistentes.",
+ "Enable notifications for events via push" : "Habilitar notificaciones para eventos mediante push",
+ "There was an error updating your attendance status." : "Ocurrió un error al actualizar su estado de asistencia.",
+ "Please contact the organizer directly." : "Por favor, contacte al organizador directamente.",
+ "Are you accepting the invitation?" : "¿Acepta la invitación?",
+ "Tentative" : "Tentativo",
+ "Your attendance was updated successfully." : "Su asistencia se actualizó correctamente."
},
-"nplurals=2; plural=(n != 1);");
+"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;");
diff --git a/apps/dav/l10n/es_MX.json b/apps/dav/l10n/es_MX.json
index 671f6916560..646747cfc68 100644
--- a/apps/dav/l10n/es_MX.json
+++ b/apps/dav/l10n/es_MX.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Calendario",
- "Todos" : "Pendientes",
+ "Tasks" : "Tareas",
"Personal" : "Personal",
"{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
"You created calendar {calendar}" : "Creaste el calendario {calendar}",
@@ -8,6 +8,8 @@
"You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
"{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
"You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} restauró el calendario {calendar}",
+ "You restored calendar {calendar}" : "Ha restaurado el calendario {calendar}",
"You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
"You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
"{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
@@ -16,50 +18,230 @@
"{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
"You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
"{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {calendar} con él mismo",
"You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
"{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
"You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
+ "Untitled event" : "Evento sin título",
"{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
"You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
"You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
"You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} movió el evento {event} del calendario {sourceCalendar} al calendario {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Ha movido el evento {event} del calendario {sourceCalendar} al calendario {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} restauró el evento {event} del calendario {calendar}",
+ "You restored event {event} of calendar {calendar}" : "Has restaurado el evento {event} del calendario {calendar}",
+ "Busy" : "Ocupado",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} creó la tarea {todo} en la lista {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Ha creado la tarea {todo} en la lista {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} eliminó la tarea {todo} de la lista {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Ha eliminado la tarea {todo} de la lista {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} actualizó la tarea {todo} en la lista {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Ha actualizado la tarea {todo} en la lista {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} resolvió la tarea {todo} en la lista {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Ha resuelto la tarea {todo} de la lista {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} reabrió la tarea {todo} en la lista {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Ha reabierto la tarea {todo} en la lista {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} movió la tarea {todo} de la lista {sourceCalendar} a la lista {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Ha movido la tarea {todo} de la lista {sourceCalendar} a la lista {targetCalendar}",
+ "Calendar, contacts and tasks" : "Calendario, contactos y tareas",
"A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
"A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
+ "A calendar <strong>to-do</strong> was modified" : "Se modificó una <strong>tarea</strong> del calendario",
"Contact birthdays" : "Cumpleaños del contacto",
+ "Death of %s" : "Fallecimiento de %s",
+ "Untitled calendar" : "Calendario sin título",
+ "Calendar:" : "Calendario:",
+ "Date:" : "Fecha:",
"Where:" : "Dónde:",
"Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
+ "_%n year_::_%n years_" : ["%n año","%n años","%n años"],
+ "_%n month_::_%n months_" : ["%n mes","%n meses","%n meses"],
+ "_%n day_::_%n days_" : ["%n día","%n días","%n días"],
+ "_%n hour_::_%n hours_" : ["%n hora","%n horas","%n horas"],
+ "_%n minute_::_%n minutes_" : ["%n minuto","%n minutos","%n minutos"],
+ "%s (in %s)" : "%s (en %s)",
+ "%s (%s ago)" : "%s (hace %s)",
+ "Calendar: %s" : "Calendario: %s",
+ "Date: %s" : "Fecha: %s",
+ "Description: %s" : "Descripción: %s",
+ "Where: %s" : "Lugar: %s",
+ "%1$s via %2$s" : "%1$s vía %2$s",
+ "Could not generate when statement" : "No se pudo generar la declaración de cuándo",
+ "Every Day for the entire day" : "Cada día todo el día",
+ "Every Day for the entire day until %1$s" : "Cada día todo el día hasta el %1$s",
+ "Every Day between %1$s - %2$s" : "Cada día entre las %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Cada día entre las %1$s - %2$s hasta el %3$s",
+ "Every %1$d Days for the entire day" : "Cada %1$d días todo el día",
+ "Every %1$d Days for the entire day until %2$s" : "Cada %1$d días todo el día hasta el %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Cada %1$d días entre las %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Cada %1$d días entre las %2$s - %3$s hasta el %4$s",
+ "Could not generate event recurrence statement" : "No se pudo generar la declaración de recurrencia del evento",
+ "Every Week on %1$s for the entire day" : "Cada semana el %1$s todo el día",
+ "Cancelled: %1$s" : "Cancelado: %1$s",
+ "\"%1$s\" has been canceled" : "\"%1$s\" ha sido cancelado",
+ "Re: %1$s" : "Respecto a: %1$s",
+ "%1$s has accepted your invitation" : "%1$s ha aceptado su invitación",
+ "%1$s has tentatively accepted your invitation" : "%1$s ha aceptado tentativamente su invitación",
+ "%1$s has declined your invitation" : "%1$s ha declinado su invitación",
+ "%1$s has responded to your invitation" : "%1$s ha respondido a su invitación",
+ "Invitation updated: %1$s" : "Invitación actualizada: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s actualizó el evento \"%2$s\"",
+ "Invitation: %1$s" : "Invitación: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s desea invitarlo a \"%2$s\"",
+ "Organizer:" : "Organizador:",
+ "Attendees:" : "Asistentes:",
+ "Title:" : "Título:",
+ "When:" : "Cuándo:",
"Location:" : "Ubicación:",
"Link:" : "Enlace:",
+ "Occurring:" : "Ocurriendo:",
"Accept" : "Aceptar",
"Decline" : "Rechazar",
+ "More options …" : "Más opciones ...",
+ "More options at %s" : "Más opciones en %s",
+ "Monday" : "Lunes",
+ "Tuesday" : "Martes",
+ "Wednesday" : "Miércoles",
+ "Thursday" : "Jueves",
+ "Friday" : "Viernes",
+ "Saturday" : "Sábado",
+ "Sunday" : "Domingo",
+ "January" : "Enero",
+ "February" : "Febrero",
+ "March" : "Marzo",
+ "April" : "Abril",
+ "May" : "Mayo",
+ "June" : "Junio",
+ "July" : "Julio",
+ "August" : "Agosto",
+ "September" : "Septiembre",
+ "October" : "Octubre",
+ "November" : "Noviembre",
+ "December" : "Diciembre",
+ "First" : "Primero",
+ "Second" : "Segundo",
+ "Third" : "Tercero",
+ "Fourth" : "Cuarto",
+ "Last" : "Último",
+ "Second Last" : "Penúltimo",
+ "Third Last" : "Antepenúltimo",
+ "Fourth Last" : "Ante antepenúltimo",
"Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
+ "{actor} created address book {addressbook}" : "{actor} creó la libreta de direcciones {addressbook}",
+ "You created address book {addressbook}" : "Ha creado la libreta de direcciones {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} eliminó la libreta de direcciones {addressbook}",
+ "You deleted address book {addressbook}" : "Ha eliminado la libreta de direcciones {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} actualizó la libreta de direcciones {addressbook}",
+ "You updated address book {addressbook}" : "Ha actualizado la libreta de direcciones {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} le compartió la libreta de direcciones {addressbook}",
+ "You shared address book {addressbook} with {user}" : "Ha compartido la libreta de direcciones {addressbook} con {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} compartió la libreta de direcciones {addressbook} con {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} le ha dejado de compartir la libreta de direcciones {addressbook}",
+ "You unshared address book {addressbook} from {user}" : "Has dejado de compartir la libreta de direcciones {addressbook} con {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} dejó de compartir la libreta de direcciones {addressbook} con {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} dejó de compartir la libreta de direcciones {addressbook} consigo mismo",
+ "You shared address book {addressbook} with group {group}" : "Has compartido la libreta de direcciones {addressbook} con el grupo {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} compartió la libreta de direcciones {addressbook} con el grupo {group}",
+ "You unshared address book {addressbook} from group {group}" : "Ha dejado de compartir la libreta de direcciones {addressbook} con el grupo {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} dejó de compartir la libreta de direcciones {addressbook} con el grupo {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} creó el contacto {card} en la libreta de direcciones {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Ha creado el contacto {card} en la libreta de direcciones {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} eliminó el contacto {card} de la libreta de direcciones {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Ha eliminado el contacto {card} de la libreta de direcciones {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} actualizó el contacto {card} en la libreta de direcciones {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Ha actualizado el contacto {card} en la libreta de direcciones {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Se modificó un <strong>contacto</strong> o una <strong>libreta de direcciones</strong> ",
+ "Accounts" : "Cuentas",
+ "System address book which holds all accounts" : "Libreta de direcciones del sistema que contiene todas las cuentas",
+ "File is not updatable: %1$s" : "No se puede actualizar el archivo: %1$s",
+ "Could not write to final file, canceled by hook" : "No se pudo escribir en el archivo final, cancelado por el sistema",
+ "Could not write file contents" : "No se pudo escribir el contenido del archivo",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes","%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Error al copiar el archivo a la ubicación destino (copiado: %1$s, tamaño de archivo esperado: %2$s)",
+ "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." : "Se esperaba un tamaño de archivo de %1$s pero se leyó (desde el cliente de Nextcloud) y se escribió (en el almacenamiento de Nextcloud) %2$s. Puede ser un problema de red del lado del envío o un problema de escritura en el almacenamiento del lado del servidor.",
+ "Could not rename part file to final file, canceled by hook" : "No se pudo renombrar el archivo parcial al archivo final, cancelado por el sistema",
+ "Could not rename part file to final file" : "No se pudo renombrar el archivo parcial al archivo final",
+ "Failed to check file size: %1$s" : "No se pudo verificar el tamaño del archivo: %1$s",
+ "Encryption not ready: %1$s" : "El cifrado no está listo: %1$s",
+ "Failed to open file: %1$s" : "No se pudo abrir el archivo: %1$s",
+ "Failed to unlink: %1$s" : "No se pudo desenlazar: %1$s",
+ "Failed to write file contents: %1$s" : "No se pudo escribir el contenido del archivo: %1$s",
+ "File not found: %1$s" : "No se encontró el archivo: %1$s",
+ "System is in maintenance mode." : "El sistema está en modo de mantenimiento.",
+ "Upgrade needed" : "Se necesita actualizar",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Su %s debe configurarse para usar HTTPS para utilizar CalDAV y CardDAV con iOS/macOS.",
+ "Configures a CalDAV account" : "Configura una cuenta CalDAV",
+ "Configures a CardDAV account" : "Configura una cuenta CardDAV",
+ "Events" : "Eventos",
+ "Untitled task" : "Tarea sin título",
+ "Completed on %s" : "Completado en %s",
+ "Due on %s by %s" : "Vence el %s por %s",
+ "Due on %s" : "Vence el %s",
+ "DAV system address book" : "Libreta de direcciones DAV del sistema",
+ "No outstanding DAV system address book sync." : "No hay una sincronización pendiente en la libreta de direcciones DAV del sistema.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "La sincronización de la libreta de direcciones DAV del sistema aún no se ha ejecutado, porque su instancia tiene más de 1000 usuarios o porque ocurrió un error. Por favor, ejecútelo manualmente con el comando \"occ dav:sync-system-addressbook\".",
"WebDAV endpoint" : "Endpoint WebDAV",
- "Tentative" : "Tentativo",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "No se pudo verificar si su servidor web está adecuadamente configurado para permitir la sincronización mediante WebDAV. Por favor, revíselo manualmente.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Tu servidor web aún no esta correctamente configurado para permitir la sincronización de archivos porque la interfaz WebDAV parece estar rota. ",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Su servidor web está adecuadamente configurado para permitir la sincronización mediante WebDAV.",
+ "Migrated calendar (%1$s)" : "Calendario migrado (%1$s)",
+ "Calendars including events, details and attendees" : "Calendarios que incluyen eventos, detalles y asistentes",
+ "Contacts and groups" : "Contactos y grupos",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "Ausencia guardada",
+ "Failed to save your absence settings" : "No se pudieron guardar sus ajustes de ausencia",
+ "Absence cleared" : "Ausencia limpiada",
+ "Failed to clear your absence settings" : "No se pudieron limpiar sus ajustes de ausencia",
+ "First day" : "Primer día",
+ "Last day (inclusive)" : "Último día (inclusivo)",
+ "Out of office replacement (optional)" : "Reemplazo para cuando fuera de la oficina (opcional)",
+ "Name of the replacement" : "Nombre del reemplazo",
+ "No results." : "Sin resultados.",
+ "Start typing." : "Empezar a escribir.",
+ "Short absence status" : "Estado de ausencia corta",
+ "Long absence Message" : "Mensaje de ausencia larga",
"Save" : "Guardar",
+ "Disable absence" : "Deshabilitar ausencia",
+ "Failed to load availability" : "No se pudo cargar la disponibilidad",
+ "Saved availability" : "Disponibilidad guardada",
+ "Failed to save availability" : "No se pudo guardar la disponibilidad",
+ "Time zone:" : "Zona horaria:",
+ "to" : "para",
+ "Delete slot" : "Eliminar ranura",
+ "No working hours set" : "No se han establecido las horas de trabajo",
+ "Add slot" : "Añadir ranura",
+ "Weekdays" : "Días laborales",
+ "Pick a start time for {dayName}" : "Elija una hora de inicio para {dayName}",
+ "Pick a end time for {dayName}" : "Elija una hora fin para {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Establecer automáticamente el estado de usuario como \"No molestar\" fuera de la disponibilidad para silenciar todas las notificaciones.",
+ "Cancel" : "Cancelar",
+ "Import" : "Importar",
+ "Error while saving settings" : "Error al guardar la configuración",
+ "Reset to default" : "Restablecer al predeterminado",
+ "Availability" : "Disponibilidad",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Si configura sus horas laborales, otras personas verán cuándo está fuera de la oficina cuando agenden una reunión.",
+ "Absence" : "Ausencia",
+ "Configure your next absence period." : "Configure su siguiente periodo de ausencia.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instale también la {calendarappstoreopen}aplicación de calendario{linkclose} o {calendardocopen}conecte su escritorio y móvil para sincronizar ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Por favor, asegúrese de configurar correctamente {emailopen}el servidor de correo electrónico{linkclose}",
+ "Calendar server" : "Servidor del calendario",
"Send invitations to attendees" : "Enviar invitaciones a los asistentes",
"Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
"Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
"Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
+ "Send notifications for events" : "Enviar notificaciones para eventos",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Las notificaciones son enviadas a través de trabajos en segundo plano, por lo que estos deben ocurrir con suficiente frecuencia.",
+ "Send reminder notifications to calendar sharees as well" : "Enviar recordatorio también a los usuarios con los que se comparte el calendario",
+ "Reminders are always sent to organizers and attendees." : "Los recordatorios se envían siempre a los organizadores y asistentes.",
+ "Enable notifications for events via push" : "Habilitar notificaciones para eventos mediante push",
+ "There was an error updating your attendance status." : "Ocurrió un error al actualizar su estado de asistencia.",
+ "Please contact the organizer directly." : "Por favor, contacte al organizador directamente.",
+ "Are you accepting the invitation?" : "¿Acepta la invitación?",
+ "Tentative" : "Tentativo",
+ "Your attendance was updated successfully." : "Su asistencia se actualizó correctamente."
+},"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"
} \ No newline at end of file
diff --git a/apps/dav/l10n/es_NI.js b/apps/dav/l10n/es_NI.js
deleted file mode 100644
index ba78d8940a8..00000000000
--- a/apps/dav/l10n/es_NI.js
+++ /dev/null
@@ -1,66 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/es_NI.json b/apps/dav/l10n/es_NI.json
deleted file mode 100644
index ec79fad2802..00000000000
--- a/apps/dav/l10n/es_NI.json
+++ /dev/null
@@ -1,64 +0,0 @@
-{ "translations": {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/es_PA.js b/apps/dav/l10n/es_PA.js
deleted file mode 100644
index ba78d8940a8..00000000000
--- a/apps/dav/l10n/es_PA.js
+++ /dev/null
@@ -1,66 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/es_PA.json b/apps/dav/l10n/es_PA.json
deleted file mode 100644
index ec79fad2802..00000000000
--- a/apps/dav/l10n/es_PA.json
+++ /dev/null
@@ -1,64 +0,0 @@
-{ "translations": {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/es_PE.js b/apps/dav/l10n/es_PE.js
deleted file mode 100644
index ba78d8940a8..00000000000
--- a/apps/dav/l10n/es_PE.js
+++ /dev/null
@@ -1,66 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/es_PE.json b/apps/dav/l10n/es_PE.json
deleted file mode 100644
index ec79fad2802..00000000000
--- a/apps/dav/l10n/es_PE.json
+++ /dev/null
@@ -1,64 +0,0 @@
-{ "translations": {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/es_PR.js b/apps/dav/l10n/es_PR.js
deleted file mode 100644
index ba78d8940a8..00000000000
--- a/apps/dav/l10n/es_PR.js
+++ /dev/null
@@ -1,66 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/es_PR.json b/apps/dav/l10n/es_PR.json
deleted file mode 100644
index ec79fad2802..00000000000
--- a/apps/dav/l10n/es_PR.json
+++ /dev/null
@@ -1,64 +0,0 @@
-{ "translations": {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/es_PY.js b/apps/dav/l10n/es_PY.js
deleted file mode 100644
index ba78d8940a8..00000000000
--- a/apps/dav/l10n/es_PY.js
+++ /dev/null
@@ -1,66 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/es_PY.json b/apps/dav/l10n/es_PY.json
deleted file mode 100644
index ec79fad2802..00000000000
--- a/apps/dav/l10n/es_PY.json
+++ /dev/null
@@ -1,64 +0,0 @@
-{ "translations": {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/es_SV.js b/apps/dav/l10n/es_SV.js
deleted file mode 100644
index ba78d8940a8..00000000000
--- a/apps/dav/l10n/es_SV.js
+++ /dev/null
@@ -1,66 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/es_SV.json b/apps/dav/l10n/es_SV.json
deleted file mode 100644
index ec79fad2802..00000000000
--- a/apps/dav/l10n/es_SV.json
+++ /dev/null
@@ -1,64 +0,0 @@
-{ "translations": {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/es_UY.js b/apps/dav/l10n/es_UY.js
deleted file mode 100644
index ba78d8940a8..00000000000
--- a/apps/dav/l10n/es_UY.js
+++ /dev/null
@@ -1,66 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/es_UY.json b/apps/dav/l10n/es_UY.json
deleted file mode 100644
index ec79fad2802..00000000000
--- a/apps/dav/l10n/es_UY.json
+++ /dev/null
@@ -1,64 +0,0 @@
-{ "translations": {
- "Calendar" : "Calendario",
- "Todos" : "Pendientes",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} creó el calendario {calendar}",
- "You created calendar {calendar}" : "Creaste el calendario {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} borró el calendario {calendar}",
- "You deleted calendar {calendar}" : "Borraste el calendario {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} actualizó el calendario {calendar}",
- "You updated calendar {calendar}" : "Actualizaste el calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Compartiste el calendario {calendar} como una liga pública",
- "You removed public link for calendar {calendar}" : "Eliminaste la liga pública para el calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} ha compartido el calendario {calendar} contigo",
- "You shared calendar {calendar} with {user}" : "Compartiste el calendario {calendar} con {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} compartió el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ha dejado de compartir el calendario {calendar} contigo",
- "You unshared calendar {calendar} from {user}" : "Has dejado de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} dejó de compartir el calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} dejó de compartir {el calendario calendar} con él mismo",
- "You shared calendar {calendar} with group {group}" : "Compartiste el calendario {calendar} con el grupo {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió el calendario {calendar} con el grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Dejaste de compartir el calendario {calendar} con el grupo {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} dejó de compartir el calendrio {calendar} con el grupo {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} creó el evento {event} en el calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Creaste el evento {event} en el calendario {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} borró el eventó {event} del calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Borraste el evento {event} del calendario {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizó el evento {event} en el calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Actualizaste el evento {event} en el calendario {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creó el pendiente {todo} en la lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Creaste el pendiente {todo} en la lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} borró el pendiente {todo} de la lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Borraste el pendiente {todo} de la lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizó el pendiente {todo} de la lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Actualizaste el pendiente {todo} de la lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolvió el pendiente {todo} de la lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Resolviste el pendiente {todo} de la lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabrió el pendiente {todo} de la lista{calendar}",
- "You reopened todo {todo} in list {calendar}" : "Reabriste el pendiente {todo} de la lista {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> fue modificado",
- "A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> de un calendario fue modificado",
- "A calendar <strong>todo</strong> was modified" : "Un <strong>pendiente</strong> de un calendario fue modificado",
- "Contact birthdays" : "Cumpleaños del contacto",
- "Where:" : "Dónde:",
- "Description:" : "Descripción:",
- "Invitation canceled" : "Invitación cancelada",
- "Invitation updated" : "Invitación actualizada",
- "Location:" : "Ubicación:",
- "Link:" : "Enlace:",
- "Accept" : "Aceptar",
- "Decline" : "Declinar",
- "Contacts" : "Contactos",
- "Tasks" : "Tareas",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativo",
- "Save" : "Guardar",
- "Send invitations to attendees" : "Enviar invitaciones a los asistentes",
- "Automatically generate a birthday calendar" : "Generar automaticamente un calendario para cumpleaños",
- "Birthday calendars will be generated by a background job." : "Los calendarios de cumpleaños serán generados por un trabajo de segundo plano",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Por lo tanto no estarán disponbiles inmediatamente después de habilitarlos pero se mostrarán después de un tiempo.",
- "Hello %s," : "Hola %s,",
- "When:" : "Cuándo:"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/et_EE.js b/apps/dav/l10n/et_EE.js
index 0539dc6979a..9935e3357df 100644
--- a/apps/dav/l10n/et_EE.js
+++ b/apps/dav/l10n/et_EE.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Kalender",
- "Todos" : "Ülesanded",
+ "Tasks" : "Ülesanded",
"Personal" : "Isiklik",
"{actor} created calendar {calendar}" : "{actor} lõi kalendri {calendar}",
"You created calendar {calendar}" : "Sa lõid kalendri {calendar}",
@@ -10,6 +10,10 @@ OC.L10N.register(
"You deleted calendar {calendar}" : "Sa kustutasid kalendri {calendar}",
"{actor} updated calendar {calendar}" : "{actor} uuendas kalendrit {calendar}",
"You updated calendar {calendar}" : "Sa uuendasid kalendrit {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} taastas kalendri {calendar}",
+ "You restored calendar {calendar}" : "Sina taastasid kalendri {calendar}",
+ "You shared calendar {calendar} as public link" : "Sina jagasid „{calendar}“ kalendrit avaliku lingina",
+ "You removed public link for calendar {calendar}" : "Sa eemaldasid „{calendar}“ kalendri avaliku lingi",
"{actor} shared calendar {calendar} with you" : "{actor} jagas kalendrit {calendar} sinuga",
"You shared calendar {calendar} with {user}" : "Sa jagasid kalendrit {calendar} kasutajaga {user}",
"{actor} shared calendar {calendar} with {user}" : "{actor} jagas kalendrit {calendar} kasutajaga {user}",
@@ -21,41 +25,314 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} jagas kalendrit {calendar} grupiga {group}",
"You unshared calendar {calendar} from group {group}" : "Sa lõpetasid kalendri {calendar} jagamise grupiga {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} lõpetas kalendri {calendar} jagamise grupiga {group}",
+ "Untitled event" : "Ilma nimeta sündmus",
"{actor} created event {event} in calendar {calendar}" : "{actor} lõi sündmuse {event} kalendrisse {calendar}",
"You created event {event} in calendar {calendar}" : "Sa lõid sündmuse {event} kalendrisse {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} kustutas sündmuse {event} kalendrist {calendar}",
"You deleted event {event} from calendar {calendar}" : "Sa kustutasid sündmuse {event} kalendrist {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} uuendas sündmust {event} kalendris {calendar}",
- "You updated event {event} in calendar {calendar}" : "Sa uuendasid sündmust {event} kalendris {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} lõi ülesande {todo} nimekirjas {calendar}",
- "You created todo {todo} in list {calendar}" : "Sa lõid ülesande {todo} nimekirjas {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} kustutas ülesande {todo} nimekirjast {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Sa kustutasid ülesande {todo} nimekirjast {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} uuendas ülesande {todo} nimekirjas {calendar}",
- "You updated todo {todo} in list {calendar}" : "Sa uuendasid ülesande {todo} nimekirjas {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} lõpetas ülesande {todo} nimekirjas {calendar}",
- "You solved todo {todo} in list {calendar}" : "Sa lõpetasid ülesande {todo} nimekirjas {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} taasavas ülesande {todo} nimekirjas {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Sa taasavasid ülesande {todo} nimekirjas {calendar}",
+ "You updated event {event} in calendar {calendar}" : "Sa uuendasid „{event}“ sündmust „{calendar}“ kalendris",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} teisaldas „{event}“ sündmuse „{sourceCalendar}“ kalendrist „{targetCalendar}“ kalendrisse",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Sina teisaldasid „{event}“ sündmuse „{sourceCalendar}“ kalendrist „{targetCalendar}“ kalendrisse",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} taastas „{event}“ sündmuse „{calendar}“ kalendris",
+ "You restored event {event} of calendar {calendar}" : "Sina taastasid „{event}“ sündmuse „{calendar}“ kalendris",
+ "Busy" : "Hõivatud",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} lisas {todo} ülesande {calendar} loendissse",
+ "You created to-do {todo} in list {calendar}" : "Sina lisasid {todo} ülesande {calendar} loendisse",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} kustutas {todo} ülesande {calendar} loendist",
+ "You deleted to-do {todo} from list {calendar}" : "Sina kustutasid {todo} ülesande {calendar} loendist",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} uuendas {todo} ülesannnet {calendar} loendist",
+ "You updated to-do {todo} in list {calendar}" : "Sina uuendasid {todo} ülesannet {calendar} loendist",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} lahendas {todo} ülesande {calendar} loendist",
+ "You solved to-do {todo} in list {calendar}" : "Sina lahendasid {todo} ülesande {calendar} loendist",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} avas uuesti {todo} ülesande {calendar} loendist",
+ "You reopened to-do {todo} in list {calendar}" : "Sina avasid uuesti {todo} ülesande {calendar} loendist",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} teisaldas {todo} ülesande {sourceCalendar} loendist {targetCalendar} loendisse",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Sina teisaldasid {todo} ülesande {sourceCalendar} loendist {targetCalendar} loendisse",
+ "Calendar, contacts and tasks" : "Kalender, kontaktid ja ülesanded",
"A <strong>calendar</strong> was modified" : " <strong>Kalendrit</strong> muudeti",
"A calendar <strong>event</strong> was modified" : "Kalendri <strong>sündmust </strong> muudeti",
- "A calendar <strong>todo</strong> was modified" : "Kalendri <strong>ülesannet</strong> muudeti",
+ "A calendar <strong>to-do</strong> was modified" : "Kalendri <strong>ülesannet</strong> muudeti",
"Contact birthdays" : "Kontaktide sünnipäevad",
+ "Death of %s" : "%s surm",
+ "Untitled calendar" : "Nimetu kalender",
+ "Calendar:" : "Kalender:",
+ "Date:" : "Kuupäev:",
"Where:" : "Kus:",
"Description:" : "Kirjeldus:",
- "Invitation canceled" : "Kutse on tühistatud",
- "Invitation updated" : "Kutse uuendatud",
+ "_%n year_::_%n years_" : ["%n aasta","%n aastat"],
+ "_%n month_::_%n months_" : ["%n kuu","%n kuud"],
+ "_%n day_::_%n days_" : ["%n päev","%n päeva"],
+ "_%n hour_::_%n hours_" : ["%n tund","%n tundi"],
+ "_%n minute_::_%n minutes_" : ["%n minut","%n minutit"],
+ "%s (in %s)" : "%s (%s pärast)",
+ "%s (%s ago)" : "%s (%s eest)",
+ "Calendar: %s" : "Kalender: %s",
+ "Date: %s" : "Kuupäev: %s",
+ "Description: %s" : "Kirjeldus: %s",
+ "Where: %s" : "Kus: %s",
+ "%1$s via %2$s" : "%1$s %2$s kaudu",
+ "In the past on %1$s for the entire day" : "Minevikus kogu päeva: %1$s",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["%n minuti pärast kogu päeva: %1$s","%n minuti pärast kogu päeva: %1$s"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["%n tunni pärast kogu päeva: %1$s","%n tunni pärast kogu päeva: %1$s"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["%n päeva pärast kogu päeva: %1$s","%n päeva pärast kogu päeva: %1$s"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["%n nädala pärast kogu päeva: %1$s","%n nädala pärast kogu päeva: %1$s"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["%n kuu pärast kogu päeva: %1$s","%n kuu pärast kogu päeva: %1$s"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["%n aasta pärast kogu päeva: %1$s","%n aasta pärast kogu päeva: %1$s"],
+ "In the past on %1$s between %2$s - %3$s" : "Minevikus: %1$s ajavahemikus %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["%n minuti pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s","%n minuti pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["%n tunni pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s","%n tunni pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["%n päeva pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s","%n päeva pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["%n nädala pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s","%n nädala pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["%n kuu pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s","%n kuu pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["%n aasta pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s","%n aasta pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s"],
+ "Could not generate when statement" : "Ei õnnestunud koostada tingimuslikku „when“ lausendit",
+ "Every Day for the entire day" : "Iga päev kogu päeva jooksul",
+ "Every Day for the entire day until %1$s" : "Iga päev kogu päeva jooksul kuni %1$s",
+ "Every Day between %1$s - %2$s" : "Iga päev ajavahemikus %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Iga päev ajavahemikus %1$s - %2$s kuni %3$s",
+ "Every %1$d Days for the entire day" : "Iga %1$d. päev kogu päeva jooksul",
+ "Every %1$d Days for the entire day until %2$s" : "Iga %1$d. päev kogu päeva jooksul kuni %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Iga %1$d. päev kogu päeva jooksul ajavahemikus %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Iga %1$d. päev kogu päeva jooksul ajavahemikus %2$s - %3$s, kuni %4$s",
+ "Could not generate event recurrence statement" : "Ei õnnestunud koostada ürituse kordumise lausendit",
+ "Every Week on %1$s for the entire day" : "Kogu päeva kestel igal nädalal: %1$s",
+ "Every Week on %1$s for the entire day until %2$s" : "Kogu päeva kestel igal nädalal kuni %2$s: %1$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Igal nädalal: %1$s ajavahemikus %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Igal nädalal kuni %2$s: %1$s ajavahemikus %3$s - %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Igal %1$d-l nädalal kogu päeva kestel: %2$s",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Igal %1$d-l nädalal kuni %3$s kogu päeva kestel: %2$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Igal %1$d-l nädalal ajavahemikus %3$s - %4$s: %2$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Igal %1$d-l nädalal kuni %5$s ajavahemikus %3$s - %4$s: %2$s",
+ "Every Month on the %1$s for the entire day" : "Kogu päeva kestel igal kuul: %1$s",
+ "Every Month on the %1$s for the entire day until %2$s" : "Kogu päeva kestel igal kuul kuni %2$s: %1$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Igal kuul: %1$s ajavahemikus %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Igal kuul kuni %2$s: %1$s ajavahemikus %3$s - %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Igal %1$d-l kuul kogu päeva kestel: %2$s",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Igal %1$d-l kuul kuni %3$s kogu päeva kestel: %2$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Igal %1$d-l kuul ajavahemikus %3$s - %4$s: %2$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Igal %1$d-l kuul kuni %5$s ajavahemikus %3$s - %4$s: %2$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Kogu päeva kestel igal aastal: %1$s, %2$s",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Kogu päeva kestel igal aastal kuni %3$s: %1$s, %2$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Igal aastal ajavahemikus %3$s - %4$s: %1$s, %2$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Igal aastal kuni %5$sajavahemikus %3$s - %4$s: %1$s, %2$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Igal %1$d-l aastal kogu päeva kestel: %2$s, %3$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Kogu päeva kestel igal %1$d-l aastal kuni %4$s: %2$s, %3$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Igal %1$d-l aastal ajavahemikus %4$s - %5$s: %2$s, %3$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Igal %1$d-l aastal kuni %6$s ajavahemikus %4$s - %5$s: %2$s, %3$s",
+ "On specific dates for the entire day until %1$s" : "Kindlatel päevadel kogu päeva jooksul kuni %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "Kindlatel päevadel ajavahemikus %1$s - %2$s kuni %3$s",
+ "In the past on %1$s" : "Minevikus %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Ühe minuti pärast: %1$s","%n minuti pärast: %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Tunni pärast: %1$s","%n tunni pärast: %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Päeva möödudes: %1$s","%n päeva möödudes: %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Nädala möödudes: %1$s","%n nädala möödudes: %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Kuu möödudes: %1$s","%n kuu möödudes: %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Aasta möödudes: %1$s","%n aasta möödudes: %1$s"],
+ "In the past on %1$s then on %2$s" : "Minevikus: %1$s ja siis %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Minuti pärast: %1$s ja siis %2$s","%n minuti pärast: %1$s ja siis %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Tunni pärast: %1$s ja siis %2$s","%n tunni pärast: %1$s ja siis %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Päeva möödudes: %1$s ja siis %2$s","%n päeva möödudes: %1$s ja siis %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Nädala möödudes: %1$s ja siis %2$s","%n nädala möödudes: %1$s ja siis %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Kuu möödudes: %1$s ja siis %2$s","%n kuu möödudes: %1$s ja siis %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Aasta möödudes: %1$s ja siis %2$s","%n aasta möödudes: %1$s ja siis %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "Minevikus: %1$s ja siis %2$s ning %3$s",
+ "_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_" : ["Minuti pärast: %1$s ja siis %2$s ning %3$s","%n minuti pärast: %1$s ja siis %2$s ning %3$s"],
+ "_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_" : ["Tunni pärast: %1$s ja siis %2$s ning %3$s","%n tunni pärast: %1$s ja siis %2$s ning %3$s"],
+ "_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_" : ["Päeva möödudes: %1$s ja siis %2$s ning %3$s","%n päeva möödudes: %1$s ja siis %2$s ning %3$s"],
+ "_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_" : ["Nädala möödudes: %1$s ja siis %2$s ning %3$s","%n nädala möödudes: %1$s ja siis %2$s ning %3$s"],
+ "_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_" : ["Kuu möödudes: %1$s ja siis %2$s ning %3$s","%n kuu möödudes: %1$s ja siis %2$s ning %3$s"],
+ "_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_" : ["Aasta möödudes: %1$s ja siis %2$s ning %3$s","%n aasta möödudes: %1$s ja siis %2$s ning %3$s"],
+ "Could not generate next recurrence statement" : "Ei õnnestunud koostada korduva ürituse järgmise toimumise lausendit",
+ "Cancelled: %1$s" : "Tühistatud: %1$s",
+ "\"%1$s\" has been canceled" : "„%1$s“ on tühistatud",
+ "Re: %1$s" : "Re: %1$s",
+ "%1$s has accepted your invitation" : "„%1$s“ on nõustunud sinu kutsega",
+ "%1$s has tentatively accepted your invitation" : "„%1$s“ on esialgselt nõustunud sinu kutsega",
+ "%1$s has declined your invitation" : "„%1$s“ on sinu kutsest keeldunud",
+ "%1$s has responded to your invitation" : "„%1$s“ on vastanud sinu kutsele",
+ "Invitation updated: %1$s" : "Kutse on uuendatud: %1$s",
+ "%1$s updated the event \"%2$s\"" : "„%1$s“ uuendas sündmust „%2$s“",
+ "Invitation: %1$s" : "Kutse: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "„%1$s“ soovib saata sulle „%2$s“ kutset",
+ "Organizer:" : "Korraldaja:",
+ "Attendees:" : "Osalejad:",
+ "Title:" : "Pealkiri:",
+ "When:" : "Millal:",
"Location:" : "Asukoht:",
"Link:" : "Link:",
+ "Occurring:" : "Toimub:",
"Accept" : "Nõustu",
"Decline" : "Keeldu",
+ "More options …" : "Täiendavad valikud…",
+ "More options at %s" : "Lisavalikud: %s",
+ "Monday" : "Esmaspäev",
+ "Tuesday" : "Teisipäev",
+ "Wednesday" : "Kolmapäev",
+ "Thursday" : "Neljapäev",
+ "Friday" : "Reede",
+ "Saturday" : "Laupäev",
+ "Sunday" : "Pühapäev",
+ "January" : "Jaanuar",
+ "February" : "Veebruar",
+ "March" : "Märts",
+ "April" : "Aprill",
+ "May" : "Mai",
+ "June" : "Juuni",
+ "July" : "Juuli",
+ "August" : "August",
+ "September" : "September",
+ "October" : "Oktoober",
+ "November" : "November",
+ "December" : "Detsember",
+ "First" : "Esimene",
+ "Second" : "Teine",
+ "Third" : "Kolmas",
+ "Fourth" : "Neljas",
+ "Fifth" : "Viies",
+ "Last" : "Viimane",
+ "Second Last" : "Teiseks viimane",
+ "Third Last" : "Kolmandks viimane",
+ "Fourth Last" : "Neljandaks viimane",
+ "Fifth Last" : "Viiendaks viimane",
"Contacts" : "Kontaktid",
- "Tasks" : "Ülesanded",
+ "{actor} created address book {addressbook}" : "{actor} lõi aadressiraamatu „{addressbook}“",
+ "You created address book {addressbook}" : "Sa lõid aadressiraamatu „{addressbook}“",
+ "{actor} deleted address book {addressbook}" : "„{actor}“ kustutas „{addressbook}“ aadressiraamatu",
+ "You deleted address book {addressbook}" : "Sa kustutasid aadressiraamatu „{addressbook}“",
+ "{actor} updated address book {addressbook}" : "{actor} uuendasaadressiraamatut „{addressbook}“",
+ "You updated address book {addressbook}" : "Sa uuendasid aadressiraamatut „{addressbook}“",
+ "{actor} shared address book {addressbook} with you" : "{actor} jagas „{addressbook}“ aadressiraamatut sinuga",
+ "You shared address book {addressbook} with {user}" : "Sa jagasid „{addressbook}“ aadressiraamatut kasutajaga „{user}“",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} jagas „{addressbook}“ aadressiraamatut kasutajaga „{user}“",
+ "{actor} unshared address book {addressbook} from you" : "{actor} lõpetas „{addressbook}“ aadressiraamatu jagamise sinult",
+ "You unshared address book {addressbook} from {user}" : "Sina lõpetasid „{addressbook}“ aadressiraamatu jagamise kasutajalt {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} lõpetas „{addressbook}“ aadressiraamatu kasutajalt {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} lõpetas „{addressbook}“ aadressiraamatu jagamise iseendalt",
+ "You shared address book {addressbook} with group {group}" : "Sina jagasid „{addressbook}“ aadressiraamatut grupiga {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} jagas „{addressbook}“ aadressiraamatut grupiga {group}",
+ "You unshared address book {addressbook} from group {group}" : "Sina lõpetasid „{addressbook}“ aadressiraamatu jagamise grupiga {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} lõpetas „{addressbook}“ aadressiraamatu jagamise grupiga {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} lisas „{card}“ kontakti „{addressbook}“ aadressiraamatusse",
+ "You created contact {card} in address book {addressbook}" : "Sina lisasid „{card}“ kontakti „{addressbook}“ aadressiraamatusse",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} kustutas „{card}“ kontakti „{addressbook}“ aadressiraamatus",
+ "You deleted contact {card} from address book {addressbook}" : "Sa kustutasid „{card}“ kontakti „{addressbook}“ aadressiraamatus",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} uuendas „{card}“ kontakti „{addressbook}“ aadressiraamatus",
+ "You updated contact {card} in address book {addressbook}" : "Sa uuendasid „{card}“ kontakti „{addressbook}“ aadressiraamatus",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>Kontakti</strong> või <strong>aadressiraamatut</strong> muudeti",
+ "Accounts" : "Kasutajakontod",
+ "System address book which holds all accounts" : "Süsteemne aadressiraamat, kus leiduvad kõik kasutajakontod",
+ "File is not updatable: %1$s" : "Fail pole uuendatav: %1$s",
+ "Failed to get storage for file" : "Failile ei õnnestunud eraldada andmeruumi",
+ "Could not write to final file, canceled by hook" : "Lõppfaili kirjutamine ei õnnestunud, selle katkestas programmi haak",
+ "Could not write file contents" : "Ei õnnestunud kirjutada faili sisu",
+ "_%n byte_::_%n bytes_" : ["%n bait","%n baiti"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Viga faili kopeerimisel sihtkausta (kopeerisin %1$s, aga suuruseks eeldasin %2$s)",
+ "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." : "Eeldasin faili suuruseks %1$s, aga lugesin (Nextcloudi kliendilt) ja kirjutasin (Nextcloudi andmeruumi) %2$s. Tegemist võib olla võrguühenduse veaga saatja poolel või andmeruumi kirjutamise veaga serveri poolel.",
+ "Could not rename part file to final file, canceled by hook" : "Osalise faili nime muutmine lõplikuks nimeks ei õnnestunud, selle katkestas programmi haak",
+ "Could not rename part file to final file" : "Osalise faili nime muutmine lõplikuks nimeks ei õnnestunud",
+ "Failed to check file size: %1$s" : "Faili suuruse kontrollimine ei õnnestunud: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "„%1$s“ faili avamine ei õnnestunud - aga tundub, et ta on olemas",
+ "Could not open file: %1$s, file doesn't seem to exist" : "„%1$s“ faili avamine ei õnnestunud - tundub, et teda pole olemas",
+ "Encryption not ready: %1$s" : "Krüptimine pole veel kasutatav: %1$s",
+ "Failed to open file: %1$s" : "Faili avamine ei õnnestunud: %1$s",
+ "Failed to unlink: %1$s" : "Lingi eemaldamine ei õnnestunud: %1$s",
+ "Failed to write file contents: %1$s" : "Faili sisu salvestamine ei õnnestunud: %1$s",
+ "File not found: %1$s" : "Faili ei leidu: %1$s",
+ "Invalid target path" : "Vigane sihtasukoht",
+ "System is in maintenance mode." : "Server on hooldusrežiimis.",
+ "Upgrade needed" : "Uuendus on vajalik",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Selleks, et sinu %s toimiks iOS-is/macOS-is CalDAV-i ja CardDAV-iga peab https olema seadistatud.",
+ "Configures a CalDAV account" : "Seadistab CalDAV-i kasutajakonto",
+ "Configures a CardDAV account" : "Seadistab CardDAV-i kasutajakonto",
+ "Events" : "Sündmused",
+ "Untitled task" : "Ilma nimeta pealkiri",
+ "Completed on %s" : "Lõpetatud %s",
+ "Due on %s by %s" : "Tähtaeg: %s, täitjaks %s",
+ "Due on %s" : "Tähtaeg: %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Tere tulemast Nextcloudi Kalendrisse!\n\nSee näidissündmus võimaldab sul tutvuda Nextcloudi Kalendri paindlikkusega oma aja plaanimisel - proovi teha igasuguseid muudatusi!\n\nNextcloudi Kalendriga saad sa:\n- vaevata luua, muuta ja hallata sündmusi,\n- koostada mitmeid kalendreid ning neid jagada tiimikaaslaste, sõprade ja perega,\n- kontrollida teiste vabu aega ja enda omi näidata teistele,\n- kasutada sujuvat CalDAV-i põhist lõimingut teiste rakenduste ja seadmetega,\n- kohendada kõike oma vajadustele: ajastades korduvaid sündmusi ning sättida teavitusi ja muid seadistusi.",
+ "Example event - open me!" : "Näidissündmus - klõpsi mind!",
+ "System Address Book" : "Süsteemne aadressiraamat",
+ "The system address book contains contact information for all users in your instance." : "Süsteemses aadressiraamatus leiduvad kõikde selle serveri kasutajate kontaktteave.",
+ "Enable System Address Book" : "Kasuta süsteemset aadressiraamatut",
+ "DAV system address book" : "DAV-i süsteemne aadressiraamat",
+ "No outstanding DAV system address book sync." : "Pole DAV-i süsteemse aadressiraamatu sünkroniseerimist.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Kuna selles serveris on üle 1000 kasutaja, siis DAV-i süsteemse aadressiraamatu sünkroonomist poel veel toimunud. Aga võis ka juhtuda viga. Palun käivita ta käsurealt ise käsuga „occ dav:sync-system-addressbook“.",
+ "WebDAV endpoint" : "WebDAV-i teenuse otspunkt",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Ei õnnestunud kontrollida, kas sinu veebiserver on korrektselt seadistatud ja võimaldab kasutada failide sünkroniseerimist WebDAV-i vahendusel. Palun kontrolli seda käsitsi.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Sinu veebiserver pole veel failide sünkroniseerimiseks vajalikult seadistatud, kuna WebDAV liides paistab olevat katki.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Sinu veebiserver on korrektselt seadistatud ja võimaldab kasutada failide sünkroniseerimist WebDAV-i vahendusel.",
+ "Migrated calendar (%1$s)" : "Ümberkolitud kalender (%1$s)",
+ "Calendars including events, details and attendees" : "Kalendrid, sealhulgas sündmused, üksikasjad ja osalejad",
+ "Contacts and groups" : "Kontaktid ja grupid",
"WebDAV" : "WebDAV",
- "Tentative" : "Esialgne",
+ "Absence saved" : "Äraoleku teave on salvestatud",
+ "Failed to save your absence settings" : "Sinu äraoleku seadistuste salvestamine ei õnnestunud",
+ "Absence cleared" : "Äraolek on eemaldatud",
+ "Failed to clear your absence settings" : "Sinu äraoleku seadistuste eemaldamine ei õnnestunud",
+ "First day" : "Esimene päev",
+ "Last day (inclusive)" : "Viimane päev (kaasaarvatud)",
+ "Out of office replacement (optional)" : "Asendaja äraoleku ajaks (valikuline)",
+ "Name of the replacement" : "Asendaja nimi",
+ "No results." : "Vasteid ei leitud.",
+ "Start typing." : "Alusta kirjutamist.",
+ "Short absence status" : "Äraoleku lühinimi",
+ "Long absence Message" : "Äraoleku pikk sõnum",
"Save" : "Salvesta",
+ "Disable absence" : "Lülita äraolek välja",
+ "Failed to load availability" : "Saadavuse laadimine ei õnnestunud",
+ "Saved availability" : "Saadavus on salvestatud",
+ "Failed to save availability" : "Saadavuse salvestamine ei õnnestunud",
+ "Time zone:" : "Ajavöönd:",
+ "to" : "saaja",
+ "Delete slot" : "Kustuta ajavahemik",
+ "No working hours set" : "Tööajad on sisestamata",
+ "Add slot" : "Lisa ajavahemik",
+ "Weekdays" : "Nädalapäevad",
+ "Pick a start time for {dayName}" : "Vali algusaeg: {dayName}",
+ "Pick a end time for {dayName}" : "Vali lõpuaeg: {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Kõikide teavituste summutamiseks määra automaatselt kasutajale olek „Ära sega“ nendele aegadele, kus ta vaba ei ole.",
+ "Cancel" : "Tühista",
+ "Import" : "Impordi",
+ "Error while saving settings" : "Viga seadistuste salvestamisel",
+ "Contact reset successfully" : "Kontakti lähtestamine õnnestus",
+ "Error while resetting contact" : "Viga kontakti lähtestamisel",
+ "Contact imported successfully" : "Kontakti importimine õnnestus",
+ "Error while importing contact" : "Viga kontakti importimisel",
+ "Import contact" : "Impordi kontakt",
+ "Reset to default" : "Taasta vaikeseadistused",
+ "Import contacts" : "Impordi kontaktid",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Uue .vcf faili importimisel olemasolev vaikimisi kontakt kustutatakse ja asendatakse uuega. Kas sa soovid jätkata?",
+ "Failed to save example event creation setting" : "Näidissündmuse loomise seadistuste salvestamine ei õnnestunud",
+ "Failed to upload the example event" : "Näidissündmuse üleslaadimine ei õnnestunud",
+ "Custom example event was saved successfully" : "Kohandatud näidissündmuse salvestamine õnnestus",
+ "Failed to delete the custom example event" : "Kohandatud näidissündmuse kustutamine ei õnnestunud",
+ "Custom example event was deleted successfully" : "Kohandatud näidissündmuse kustutamine õnnestus",
+ "Import calendar event" : "Impordi kalendrisündmus",
+ "Uploading a new event will overwrite the existing one." : "Uue sündmuse üleslaadimisel asendatakse olemasolevad.",
+ "Upload event" : "Laadi sündmus üles",
+ "Availability" : "Saadavus",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Kui seadistad oma tööajad, siis teised saavad kohtumise broneerimisel arvestada sellega, millal sind kohal pole.",
+ "Absence" : "Äraolek",
+ "Configure your next absence period." : "Seadista järgmise äraoleku ajavahemik.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Palun paigalda ka {calendarappstoreopen}Kalendrirakendus{linkclose} või {calendardocopen}lisa sünkroniseerimine oma töölaule ja nutiseadmesse ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Palun kontrolli, et {emailopen}e-posti server{linkclose} on seadistatud korrektselt.",
+ "Calendar server" : "Kalendriserver",
"Send invitations to attendees" : "Saada osalejatele kutsed",
- "Hello %s," : "Tere %s,",
- "When:" : "Millal:"
+ "Automatically generate a birthday calendar" : "Koosta sünnipäevade kalender automaatselt",
+ "Birthday calendars will be generated by a background job." : "Sünnipäevade kalender luuakse automaatselt taustateenuse poolt.",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "Seega pole need andmed kohe saadaval, vaid ilmuvad mingi aja pärast.",
+ "Send notifications for events" : "Saada sündmuste teavitusi",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Teavitused saadetakse taustateenuste poolt. See eeldab, et vastavaid skripte käivitatakse piisavalt tihti.",
+ "Send reminder notifications to calendar sharees as well" : "Saada meeldetuletused ka neile, kes ürituse on lisanud oma kalendrisse",
+ "Reminders are always sent to organizers and attendees." : "Meeldetuletused on alati saadetud korraldajatele ja osalejatele.",
+ "Enable notifications for events via push" : "Võta kasutusele tõuketeenustepõhised teavitused",
+ "Example content" : "Sisunäidis",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Sisunäidis annab ülevaate Nexctcloudi võimalustest. Vaikimisi sisu tuleb Nexctcloudi paigaldusega kaasa ja seda saab alati oma sisuga asendada.",
+ "There was an error updating your attendance status." : "Sinu osalemise oleku muutmisel tekkis viga.",
+ "Please contact the organizer directly." : "Palun võta ühendust korraldajaga otse.",
+ "Are you accepting the invitation?" : "Kas sa nõustud kutsega?",
+ "Tentative" : "Esialgne",
+ "Your attendance was updated successfully." : "Sinu osalemise oleku muutmine õnnestus."
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/et_EE.json b/apps/dav/l10n/et_EE.json
index 494ec4d25b4..3da2c28f3d9 100644
--- a/apps/dav/l10n/et_EE.json
+++ b/apps/dav/l10n/et_EE.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Kalender",
- "Todos" : "Ülesanded",
+ "Tasks" : "Ülesanded",
"Personal" : "Isiklik",
"{actor} created calendar {calendar}" : "{actor} lõi kalendri {calendar}",
"You created calendar {calendar}" : "Sa lõid kalendri {calendar}",
@@ -8,6 +8,10 @@
"You deleted calendar {calendar}" : "Sa kustutasid kalendri {calendar}",
"{actor} updated calendar {calendar}" : "{actor} uuendas kalendrit {calendar}",
"You updated calendar {calendar}" : "Sa uuendasid kalendrit {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} taastas kalendri {calendar}",
+ "You restored calendar {calendar}" : "Sina taastasid kalendri {calendar}",
+ "You shared calendar {calendar} as public link" : "Sina jagasid „{calendar}“ kalendrit avaliku lingina",
+ "You removed public link for calendar {calendar}" : "Sa eemaldasid „{calendar}“ kalendri avaliku lingi",
"{actor} shared calendar {calendar} with you" : "{actor} jagas kalendrit {calendar} sinuga",
"You shared calendar {calendar} with {user}" : "Sa jagasid kalendrit {calendar} kasutajaga {user}",
"{actor} shared calendar {calendar} with {user}" : "{actor} jagas kalendrit {calendar} kasutajaga {user}",
@@ -19,41 +23,314 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} jagas kalendrit {calendar} grupiga {group}",
"You unshared calendar {calendar} from group {group}" : "Sa lõpetasid kalendri {calendar} jagamise grupiga {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} lõpetas kalendri {calendar} jagamise grupiga {group}",
+ "Untitled event" : "Ilma nimeta sündmus",
"{actor} created event {event} in calendar {calendar}" : "{actor} lõi sündmuse {event} kalendrisse {calendar}",
"You created event {event} in calendar {calendar}" : "Sa lõid sündmuse {event} kalendrisse {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} kustutas sündmuse {event} kalendrist {calendar}",
"You deleted event {event} from calendar {calendar}" : "Sa kustutasid sündmuse {event} kalendrist {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} uuendas sündmust {event} kalendris {calendar}",
- "You updated event {event} in calendar {calendar}" : "Sa uuendasid sündmust {event} kalendris {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} lõi ülesande {todo} nimekirjas {calendar}",
- "You created todo {todo} in list {calendar}" : "Sa lõid ülesande {todo} nimekirjas {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} kustutas ülesande {todo} nimekirjast {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Sa kustutasid ülesande {todo} nimekirjast {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} uuendas ülesande {todo} nimekirjas {calendar}",
- "You updated todo {todo} in list {calendar}" : "Sa uuendasid ülesande {todo} nimekirjas {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} lõpetas ülesande {todo} nimekirjas {calendar}",
- "You solved todo {todo} in list {calendar}" : "Sa lõpetasid ülesande {todo} nimekirjas {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} taasavas ülesande {todo} nimekirjas {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Sa taasavasid ülesande {todo} nimekirjas {calendar}",
+ "You updated event {event} in calendar {calendar}" : "Sa uuendasid „{event}“ sündmust „{calendar}“ kalendris",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} teisaldas „{event}“ sündmuse „{sourceCalendar}“ kalendrist „{targetCalendar}“ kalendrisse",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Sina teisaldasid „{event}“ sündmuse „{sourceCalendar}“ kalendrist „{targetCalendar}“ kalendrisse",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} taastas „{event}“ sündmuse „{calendar}“ kalendris",
+ "You restored event {event} of calendar {calendar}" : "Sina taastasid „{event}“ sündmuse „{calendar}“ kalendris",
+ "Busy" : "Hõivatud",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} lisas {todo} ülesande {calendar} loendissse",
+ "You created to-do {todo} in list {calendar}" : "Sina lisasid {todo} ülesande {calendar} loendisse",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} kustutas {todo} ülesande {calendar} loendist",
+ "You deleted to-do {todo} from list {calendar}" : "Sina kustutasid {todo} ülesande {calendar} loendist",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} uuendas {todo} ülesannnet {calendar} loendist",
+ "You updated to-do {todo} in list {calendar}" : "Sina uuendasid {todo} ülesannet {calendar} loendist",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} lahendas {todo} ülesande {calendar} loendist",
+ "You solved to-do {todo} in list {calendar}" : "Sina lahendasid {todo} ülesande {calendar} loendist",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} avas uuesti {todo} ülesande {calendar} loendist",
+ "You reopened to-do {todo} in list {calendar}" : "Sina avasid uuesti {todo} ülesande {calendar} loendist",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} teisaldas {todo} ülesande {sourceCalendar} loendist {targetCalendar} loendisse",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Sina teisaldasid {todo} ülesande {sourceCalendar} loendist {targetCalendar} loendisse",
+ "Calendar, contacts and tasks" : "Kalender, kontaktid ja ülesanded",
"A <strong>calendar</strong> was modified" : " <strong>Kalendrit</strong> muudeti",
"A calendar <strong>event</strong> was modified" : "Kalendri <strong>sündmust </strong> muudeti",
- "A calendar <strong>todo</strong> was modified" : "Kalendri <strong>ülesannet</strong> muudeti",
+ "A calendar <strong>to-do</strong> was modified" : "Kalendri <strong>ülesannet</strong> muudeti",
"Contact birthdays" : "Kontaktide sünnipäevad",
+ "Death of %s" : "%s surm",
+ "Untitled calendar" : "Nimetu kalender",
+ "Calendar:" : "Kalender:",
+ "Date:" : "Kuupäev:",
"Where:" : "Kus:",
"Description:" : "Kirjeldus:",
- "Invitation canceled" : "Kutse on tühistatud",
- "Invitation updated" : "Kutse uuendatud",
+ "_%n year_::_%n years_" : ["%n aasta","%n aastat"],
+ "_%n month_::_%n months_" : ["%n kuu","%n kuud"],
+ "_%n day_::_%n days_" : ["%n päev","%n päeva"],
+ "_%n hour_::_%n hours_" : ["%n tund","%n tundi"],
+ "_%n minute_::_%n minutes_" : ["%n minut","%n minutit"],
+ "%s (in %s)" : "%s (%s pärast)",
+ "%s (%s ago)" : "%s (%s eest)",
+ "Calendar: %s" : "Kalender: %s",
+ "Date: %s" : "Kuupäev: %s",
+ "Description: %s" : "Kirjeldus: %s",
+ "Where: %s" : "Kus: %s",
+ "%1$s via %2$s" : "%1$s %2$s kaudu",
+ "In the past on %1$s for the entire day" : "Minevikus kogu päeva: %1$s",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["%n minuti pärast kogu päeva: %1$s","%n minuti pärast kogu päeva: %1$s"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["%n tunni pärast kogu päeva: %1$s","%n tunni pärast kogu päeva: %1$s"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["%n päeva pärast kogu päeva: %1$s","%n päeva pärast kogu päeva: %1$s"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["%n nädala pärast kogu päeva: %1$s","%n nädala pärast kogu päeva: %1$s"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["%n kuu pärast kogu päeva: %1$s","%n kuu pärast kogu päeva: %1$s"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["%n aasta pärast kogu päeva: %1$s","%n aasta pärast kogu päeva: %1$s"],
+ "In the past on %1$s between %2$s - %3$s" : "Minevikus: %1$s ajavahemikus %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["%n minuti pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s","%n minuti pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["%n tunni pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s","%n tunni pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["%n päeva pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s","%n päeva pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["%n nädala pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s","%n nädala pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["%n kuu pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s","%n kuu pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["%n aasta pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s","%n aasta pärast ajavahemikus %2$s -%3$s kogu päeva: %1$s"],
+ "Could not generate when statement" : "Ei õnnestunud koostada tingimuslikku „when“ lausendit",
+ "Every Day for the entire day" : "Iga päev kogu päeva jooksul",
+ "Every Day for the entire day until %1$s" : "Iga päev kogu päeva jooksul kuni %1$s",
+ "Every Day between %1$s - %2$s" : "Iga päev ajavahemikus %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Iga päev ajavahemikus %1$s - %2$s kuni %3$s",
+ "Every %1$d Days for the entire day" : "Iga %1$d. päev kogu päeva jooksul",
+ "Every %1$d Days for the entire day until %2$s" : "Iga %1$d. päev kogu päeva jooksul kuni %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Iga %1$d. päev kogu päeva jooksul ajavahemikus %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Iga %1$d. päev kogu päeva jooksul ajavahemikus %2$s - %3$s, kuni %4$s",
+ "Could not generate event recurrence statement" : "Ei õnnestunud koostada ürituse kordumise lausendit",
+ "Every Week on %1$s for the entire day" : "Kogu päeva kestel igal nädalal: %1$s",
+ "Every Week on %1$s for the entire day until %2$s" : "Kogu päeva kestel igal nädalal kuni %2$s: %1$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Igal nädalal: %1$s ajavahemikus %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Igal nädalal kuni %2$s: %1$s ajavahemikus %3$s - %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Igal %1$d-l nädalal kogu päeva kestel: %2$s",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Igal %1$d-l nädalal kuni %3$s kogu päeva kestel: %2$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Igal %1$d-l nädalal ajavahemikus %3$s - %4$s: %2$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Igal %1$d-l nädalal kuni %5$s ajavahemikus %3$s - %4$s: %2$s",
+ "Every Month on the %1$s for the entire day" : "Kogu päeva kestel igal kuul: %1$s",
+ "Every Month on the %1$s for the entire day until %2$s" : "Kogu päeva kestel igal kuul kuni %2$s: %1$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Igal kuul: %1$s ajavahemikus %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Igal kuul kuni %2$s: %1$s ajavahemikus %3$s - %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Igal %1$d-l kuul kogu päeva kestel: %2$s",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Igal %1$d-l kuul kuni %3$s kogu päeva kestel: %2$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Igal %1$d-l kuul ajavahemikus %3$s - %4$s: %2$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Igal %1$d-l kuul kuni %5$s ajavahemikus %3$s - %4$s: %2$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Kogu päeva kestel igal aastal: %1$s, %2$s",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Kogu päeva kestel igal aastal kuni %3$s: %1$s, %2$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Igal aastal ajavahemikus %3$s - %4$s: %1$s, %2$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Igal aastal kuni %5$sajavahemikus %3$s - %4$s: %1$s, %2$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Igal %1$d-l aastal kogu päeva kestel: %2$s, %3$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Kogu päeva kestel igal %1$d-l aastal kuni %4$s: %2$s, %3$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Igal %1$d-l aastal ajavahemikus %4$s - %5$s: %2$s, %3$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Igal %1$d-l aastal kuni %6$s ajavahemikus %4$s - %5$s: %2$s, %3$s",
+ "On specific dates for the entire day until %1$s" : "Kindlatel päevadel kogu päeva jooksul kuni %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "Kindlatel päevadel ajavahemikus %1$s - %2$s kuni %3$s",
+ "In the past on %1$s" : "Minevikus %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Ühe minuti pärast: %1$s","%n minuti pärast: %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Tunni pärast: %1$s","%n tunni pärast: %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Päeva möödudes: %1$s","%n päeva möödudes: %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Nädala möödudes: %1$s","%n nädala möödudes: %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Kuu möödudes: %1$s","%n kuu möödudes: %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Aasta möödudes: %1$s","%n aasta möödudes: %1$s"],
+ "In the past on %1$s then on %2$s" : "Minevikus: %1$s ja siis %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Minuti pärast: %1$s ja siis %2$s","%n minuti pärast: %1$s ja siis %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Tunni pärast: %1$s ja siis %2$s","%n tunni pärast: %1$s ja siis %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Päeva möödudes: %1$s ja siis %2$s","%n päeva möödudes: %1$s ja siis %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Nädala möödudes: %1$s ja siis %2$s","%n nädala möödudes: %1$s ja siis %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Kuu möödudes: %1$s ja siis %2$s","%n kuu möödudes: %1$s ja siis %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Aasta möödudes: %1$s ja siis %2$s","%n aasta möödudes: %1$s ja siis %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "Minevikus: %1$s ja siis %2$s ning %3$s",
+ "_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_" : ["Minuti pärast: %1$s ja siis %2$s ning %3$s","%n minuti pärast: %1$s ja siis %2$s ning %3$s"],
+ "_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_" : ["Tunni pärast: %1$s ja siis %2$s ning %3$s","%n tunni pärast: %1$s ja siis %2$s ning %3$s"],
+ "_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_" : ["Päeva möödudes: %1$s ja siis %2$s ning %3$s","%n päeva möödudes: %1$s ja siis %2$s ning %3$s"],
+ "_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_" : ["Nädala möödudes: %1$s ja siis %2$s ning %3$s","%n nädala möödudes: %1$s ja siis %2$s ning %3$s"],
+ "_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_" : ["Kuu möödudes: %1$s ja siis %2$s ning %3$s","%n kuu möödudes: %1$s ja siis %2$s ning %3$s"],
+ "_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_" : ["Aasta möödudes: %1$s ja siis %2$s ning %3$s","%n aasta möödudes: %1$s ja siis %2$s ning %3$s"],
+ "Could not generate next recurrence statement" : "Ei õnnestunud koostada korduva ürituse järgmise toimumise lausendit",
+ "Cancelled: %1$s" : "Tühistatud: %1$s",
+ "\"%1$s\" has been canceled" : "„%1$s“ on tühistatud",
+ "Re: %1$s" : "Re: %1$s",
+ "%1$s has accepted your invitation" : "„%1$s“ on nõustunud sinu kutsega",
+ "%1$s has tentatively accepted your invitation" : "„%1$s“ on esialgselt nõustunud sinu kutsega",
+ "%1$s has declined your invitation" : "„%1$s“ on sinu kutsest keeldunud",
+ "%1$s has responded to your invitation" : "„%1$s“ on vastanud sinu kutsele",
+ "Invitation updated: %1$s" : "Kutse on uuendatud: %1$s",
+ "%1$s updated the event \"%2$s\"" : "„%1$s“ uuendas sündmust „%2$s“",
+ "Invitation: %1$s" : "Kutse: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "„%1$s“ soovib saata sulle „%2$s“ kutset",
+ "Organizer:" : "Korraldaja:",
+ "Attendees:" : "Osalejad:",
+ "Title:" : "Pealkiri:",
+ "When:" : "Millal:",
"Location:" : "Asukoht:",
"Link:" : "Link:",
+ "Occurring:" : "Toimub:",
"Accept" : "Nõustu",
"Decline" : "Keeldu",
+ "More options …" : "Täiendavad valikud…",
+ "More options at %s" : "Lisavalikud: %s",
+ "Monday" : "Esmaspäev",
+ "Tuesday" : "Teisipäev",
+ "Wednesday" : "Kolmapäev",
+ "Thursday" : "Neljapäev",
+ "Friday" : "Reede",
+ "Saturday" : "Laupäev",
+ "Sunday" : "Pühapäev",
+ "January" : "Jaanuar",
+ "February" : "Veebruar",
+ "March" : "Märts",
+ "April" : "Aprill",
+ "May" : "Mai",
+ "June" : "Juuni",
+ "July" : "Juuli",
+ "August" : "August",
+ "September" : "September",
+ "October" : "Oktoober",
+ "November" : "November",
+ "December" : "Detsember",
+ "First" : "Esimene",
+ "Second" : "Teine",
+ "Third" : "Kolmas",
+ "Fourth" : "Neljas",
+ "Fifth" : "Viies",
+ "Last" : "Viimane",
+ "Second Last" : "Teiseks viimane",
+ "Third Last" : "Kolmandks viimane",
+ "Fourth Last" : "Neljandaks viimane",
+ "Fifth Last" : "Viiendaks viimane",
"Contacts" : "Kontaktid",
- "Tasks" : "Ülesanded",
+ "{actor} created address book {addressbook}" : "{actor} lõi aadressiraamatu „{addressbook}“",
+ "You created address book {addressbook}" : "Sa lõid aadressiraamatu „{addressbook}“",
+ "{actor} deleted address book {addressbook}" : "„{actor}“ kustutas „{addressbook}“ aadressiraamatu",
+ "You deleted address book {addressbook}" : "Sa kustutasid aadressiraamatu „{addressbook}“",
+ "{actor} updated address book {addressbook}" : "{actor} uuendasaadressiraamatut „{addressbook}“",
+ "You updated address book {addressbook}" : "Sa uuendasid aadressiraamatut „{addressbook}“",
+ "{actor} shared address book {addressbook} with you" : "{actor} jagas „{addressbook}“ aadressiraamatut sinuga",
+ "You shared address book {addressbook} with {user}" : "Sa jagasid „{addressbook}“ aadressiraamatut kasutajaga „{user}“",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} jagas „{addressbook}“ aadressiraamatut kasutajaga „{user}“",
+ "{actor} unshared address book {addressbook} from you" : "{actor} lõpetas „{addressbook}“ aadressiraamatu jagamise sinult",
+ "You unshared address book {addressbook} from {user}" : "Sina lõpetasid „{addressbook}“ aadressiraamatu jagamise kasutajalt {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} lõpetas „{addressbook}“ aadressiraamatu kasutajalt {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} lõpetas „{addressbook}“ aadressiraamatu jagamise iseendalt",
+ "You shared address book {addressbook} with group {group}" : "Sina jagasid „{addressbook}“ aadressiraamatut grupiga {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} jagas „{addressbook}“ aadressiraamatut grupiga {group}",
+ "You unshared address book {addressbook} from group {group}" : "Sina lõpetasid „{addressbook}“ aadressiraamatu jagamise grupiga {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} lõpetas „{addressbook}“ aadressiraamatu jagamise grupiga {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} lisas „{card}“ kontakti „{addressbook}“ aadressiraamatusse",
+ "You created contact {card} in address book {addressbook}" : "Sina lisasid „{card}“ kontakti „{addressbook}“ aadressiraamatusse",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} kustutas „{card}“ kontakti „{addressbook}“ aadressiraamatus",
+ "You deleted contact {card} from address book {addressbook}" : "Sa kustutasid „{card}“ kontakti „{addressbook}“ aadressiraamatus",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} uuendas „{card}“ kontakti „{addressbook}“ aadressiraamatus",
+ "You updated contact {card} in address book {addressbook}" : "Sa uuendasid „{card}“ kontakti „{addressbook}“ aadressiraamatus",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>Kontakti</strong> või <strong>aadressiraamatut</strong> muudeti",
+ "Accounts" : "Kasutajakontod",
+ "System address book which holds all accounts" : "Süsteemne aadressiraamat, kus leiduvad kõik kasutajakontod",
+ "File is not updatable: %1$s" : "Fail pole uuendatav: %1$s",
+ "Failed to get storage for file" : "Failile ei õnnestunud eraldada andmeruumi",
+ "Could not write to final file, canceled by hook" : "Lõppfaili kirjutamine ei õnnestunud, selle katkestas programmi haak",
+ "Could not write file contents" : "Ei õnnestunud kirjutada faili sisu",
+ "_%n byte_::_%n bytes_" : ["%n bait","%n baiti"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Viga faili kopeerimisel sihtkausta (kopeerisin %1$s, aga suuruseks eeldasin %2$s)",
+ "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." : "Eeldasin faili suuruseks %1$s, aga lugesin (Nextcloudi kliendilt) ja kirjutasin (Nextcloudi andmeruumi) %2$s. Tegemist võib olla võrguühenduse veaga saatja poolel või andmeruumi kirjutamise veaga serveri poolel.",
+ "Could not rename part file to final file, canceled by hook" : "Osalise faili nime muutmine lõplikuks nimeks ei õnnestunud, selle katkestas programmi haak",
+ "Could not rename part file to final file" : "Osalise faili nime muutmine lõplikuks nimeks ei õnnestunud",
+ "Failed to check file size: %1$s" : "Faili suuruse kontrollimine ei õnnestunud: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "„%1$s“ faili avamine ei õnnestunud - aga tundub, et ta on olemas",
+ "Could not open file: %1$s, file doesn't seem to exist" : "„%1$s“ faili avamine ei õnnestunud - tundub, et teda pole olemas",
+ "Encryption not ready: %1$s" : "Krüptimine pole veel kasutatav: %1$s",
+ "Failed to open file: %1$s" : "Faili avamine ei õnnestunud: %1$s",
+ "Failed to unlink: %1$s" : "Lingi eemaldamine ei õnnestunud: %1$s",
+ "Failed to write file contents: %1$s" : "Faili sisu salvestamine ei õnnestunud: %1$s",
+ "File not found: %1$s" : "Faili ei leidu: %1$s",
+ "Invalid target path" : "Vigane sihtasukoht",
+ "System is in maintenance mode." : "Server on hooldusrežiimis.",
+ "Upgrade needed" : "Uuendus on vajalik",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Selleks, et sinu %s toimiks iOS-is/macOS-is CalDAV-i ja CardDAV-iga peab https olema seadistatud.",
+ "Configures a CalDAV account" : "Seadistab CalDAV-i kasutajakonto",
+ "Configures a CardDAV account" : "Seadistab CardDAV-i kasutajakonto",
+ "Events" : "Sündmused",
+ "Untitled task" : "Ilma nimeta pealkiri",
+ "Completed on %s" : "Lõpetatud %s",
+ "Due on %s by %s" : "Tähtaeg: %s, täitjaks %s",
+ "Due on %s" : "Tähtaeg: %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Tere tulemast Nextcloudi Kalendrisse!\n\nSee näidissündmus võimaldab sul tutvuda Nextcloudi Kalendri paindlikkusega oma aja plaanimisel - proovi teha igasuguseid muudatusi!\n\nNextcloudi Kalendriga saad sa:\n- vaevata luua, muuta ja hallata sündmusi,\n- koostada mitmeid kalendreid ning neid jagada tiimikaaslaste, sõprade ja perega,\n- kontrollida teiste vabu aega ja enda omi näidata teistele,\n- kasutada sujuvat CalDAV-i põhist lõimingut teiste rakenduste ja seadmetega,\n- kohendada kõike oma vajadustele: ajastades korduvaid sündmusi ning sättida teavitusi ja muid seadistusi.",
+ "Example event - open me!" : "Näidissündmus - klõpsi mind!",
+ "System Address Book" : "Süsteemne aadressiraamat",
+ "The system address book contains contact information for all users in your instance." : "Süsteemses aadressiraamatus leiduvad kõikde selle serveri kasutajate kontaktteave.",
+ "Enable System Address Book" : "Kasuta süsteemset aadressiraamatut",
+ "DAV system address book" : "DAV-i süsteemne aadressiraamat",
+ "No outstanding DAV system address book sync." : "Pole DAV-i süsteemse aadressiraamatu sünkroniseerimist.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Kuna selles serveris on üle 1000 kasutaja, siis DAV-i süsteemse aadressiraamatu sünkroonomist poel veel toimunud. Aga võis ka juhtuda viga. Palun käivita ta käsurealt ise käsuga „occ dav:sync-system-addressbook“.",
+ "WebDAV endpoint" : "WebDAV-i teenuse otspunkt",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Ei õnnestunud kontrollida, kas sinu veebiserver on korrektselt seadistatud ja võimaldab kasutada failide sünkroniseerimist WebDAV-i vahendusel. Palun kontrolli seda käsitsi.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Sinu veebiserver pole veel failide sünkroniseerimiseks vajalikult seadistatud, kuna WebDAV liides paistab olevat katki.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Sinu veebiserver on korrektselt seadistatud ja võimaldab kasutada failide sünkroniseerimist WebDAV-i vahendusel.",
+ "Migrated calendar (%1$s)" : "Ümberkolitud kalender (%1$s)",
+ "Calendars including events, details and attendees" : "Kalendrid, sealhulgas sündmused, üksikasjad ja osalejad",
+ "Contacts and groups" : "Kontaktid ja grupid",
"WebDAV" : "WebDAV",
- "Tentative" : "Esialgne",
+ "Absence saved" : "Äraoleku teave on salvestatud",
+ "Failed to save your absence settings" : "Sinu äraoleku seadistuste salvestamine ei õnnestunud",
+ "Absence cleared" : "Äraolek on eemaldatud",
+ "Failed to clear your absence settings" : "Sinu äraoleku seadistuste eemaldamine ei õnnestunud",
+ "First day" : "Esimene päev",
+ "Last day (inclusive)" : "Viimane päev (kaasaarvatud)",
+ "Out of office replacement (optional)" : "Asendaja äraoleku ajaks (valikuline)",
+ "Name of the replacement" : "Asendaja nimi",
+ "No results." : "Vasteid ei leitud.",
+ "Start typing." : "Alusta kirjutamist.",
+ "Short absence status" : "Äraoleku lühinimi",
+ "Long absence Message" : "Äraoleku pikk sõnum",
"Save" : "Salvesta",
+ "Disable absence" : "Lülita äraolek välja",
+ "Failed to load availability" : "Saadavuse laadimine ei õnnestunud",
+ "Saved availability" : "Saadavus on salvestatud",
+ "Failed to save availability" : "Saadavuse salvestamine ei õnnestunud",
+ "Time zone:" : "Ajavöönd:",
+ "to" : "saaja",
+ "Delete slot" : "Kustuta ajavahemik",
+ "No working hours set" : "Tööajad on sisestamata",
+ "Add slot" : "Lisa ajavahemik",
+ "Weekdays" : "Nädalapäevad",
+ "Pick a start time for {dayName}" : "Vali algusaeg: {dayName}",
+ "Pick a end time for {dayName}" : "Vali lõpuaeg: {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Kõikide teavituste summutamiseks määra automaatselt kasutajale olek „Ära sega“ nendele aegadele, kus ta vaba ei ole.",
+ "Cancel" : "Tühista",
+ "Import" : "Impordi",
+ "Error while saving settings" : "Viga seadistuste salvestamisel",
+ "Contact reset successfully" : "Kontakti lähtestamine õnnestus",
+ "Error while resetting contact" : "Viga kontakti lähtestamisel",
+ "Contact imported successfully" : "Kontakti importimine õnnestus",
+ "Error while importing contact" : "Viga kontakti importimisel",
+ "Import contact" : "Impordi kontakt",
+ "Reset to default" : "Taasta vaikeseadistused",
+ "Import contacts" : "Impordi kontaktid",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Uue .vcf faili importimisel olemasolev vaikimisi kontakt kustutatakse ja asendatakse uuega. Kas sa soovid jätkata?",
+ "Failed to save example event creation setting" : "Näidissündmuse loomise seadistuste salvestamine ei õnnestunud",
+ "Failed to upload the example event" : "Näidissündmuse üleslaadimine ei õnnestunud",
+ "Custom example event was saved successfully" : "Kohandatud näidissündmuse salvestamine õnnestus",
+ "Failed to delete the custom example event" : "Kohandatud näidissündmuse kustutamine ei õnnestunud",
+ "Custom example event was deleted successfully" : "Kohandatud näidissündmuse kustutamine õnnestus",
+ "Import calendar event" : "Impordi kalendrisündmus",
+ "Uploading a new event will overwrite the existing one." : "Uue sündmuse üleslaadimisel asendatakse olemasolevad.",
+ "Upload event" : "Laadi sündmus üles",
+ "Availability" : "Saadavus",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Kui seadistad oma tööajad, siis teised saavad kohtumise broneerimisel arvestada sellega, millal sind kohal pole.",
+ "Absence" : "Äraolek",
+ "Configure your next absence period." : "Seadista järgmise äraoleku ajavahemik.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Palun paigalda ka {calendarappstoreopen}Kalendrirakendus{linkclose} või {calendardocopen}lisa sünkroniseerimine oma töölaule ja nutiseadmesse ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Palun kontrolli, et {emailopen}e-posti server{linkclose} on seadistatud korrektselt.",
+ "Calendar server" : "Kalendriserver",
"Send invitations to attendees" : "Saada osalejatele kutsed",
- "Hello %s," : "Tere %s,",
- "When:" : "Millal:"
+ "Automatically generate a birthday calendar" : "Koosta sünnipäevade kalender automaatselt",
+ "Birthday calendars will be generated by a background job." : "Sünnipäevade kalender luuakse automaatselt taustateenuse poolt.",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "Seega pole need andmed kohe saadaval, vaid ilmuvad mingi aja pärast.",
+ "Send notifications for events" : "Saada sündmuste teavitusi",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Teavitused saadetakse taustateenuste poolt. See eeldab, et vastavaid skripte käivitatakse piisavalt tihti.",
+ "Send reminder notifications to calendar sharees as well" : "Saada meeldetuletused ka neile, kes ürituse on lisanud oma kalendrisse",
+ "Reminders are always sent to organizers and attendees." : "Meeldetuletused on alati saadetud korraldajatele ja osalejatele.",
+ "Enable notifications for events via push" : "Võta kasutusele tõuketeenustepõhised teavitused",
+ "Example content" : "Sisunäidis",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Sisunäidis annab ülevaate Nexctcloudi võimalustest. Vaikimisi sisu tuleb Nexctcloudi paigaldusega kaasa ja seda saab alati oma sisuga asendada.",
+ "There was an error updating your attendance status." : "Sinu osalemise oleku muutmisel tekkis viga.",
+ "Please contact the organizer directly." : "Palun võta ühendust korraldajaga otse.",
+ "Are you accepting the invitation?" : "Kas sa nõustud kutsega?",
+ "Tentative" : "Esialgne",
+ "Your attendance was updated successfully." : "Sinu osalemise oleku muutmine õnnestus."
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/dav/l10n/eu.js b/apps/dav/l10n/eu.js
index adc5994178f..df586e1958e 100644
--- a/apps/dav/l10n/eu.js
+++ b/apps/dav/l10n/eu.js
@@ -2,160 +2,336 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Egutegia",
- "Todos" : "Egitekoak",
+ "Tasks" : "Zereginak",
"Personal" : "Pertsonala",
- "{actor} created calendar {calendar}" : "{actor}-k sortutako egutegia: {calendar}",
+ "{actor} created calendar {calendar}" : "{actor}-(e)k sortutako egutegia: {calendar}",
"You created calendar {calendar}" : "{calendar} egutegia sortu duzu",
- "{actor} deleted calendar {calendar}" : "{actor}-k {calendar} egutegia borratu du",
- "You deleted calendar {calendar}" : "{calendar} egutegia borratu duzu",
- "{actor} updated calendar {calendar}" : "{actor} -k {calendar} egutegia eguneratu du",
+ "{actor} deleted calendar {calendar}" : "{actor}-(e)k {calendar} egutegia ezabatu du",
+ "You deleted calendar {calendar}" : "{calendar} egutegia ezabatu duzu",
+ "{actor} updated calendar {calendar}" : "{actor}-(e)k {calendar} egutegia eguneratu du",
"You updated calendar {calendar}" : "{calendar} egutegia eguneratu duzu ",
- "{actor} restored calendar {calendar}" : "{actor}-(e)k berrezarritako {calendar} egutegia",
- "You restored calendar {calendar}" : "Berrezarri duzu {calendar} egutegia",
+ "{actor} restored calendar {calendar}" : "{actor}-(e)k {calendar} egutegia leheneratu du",
+ "You restored calendar {calendar}" : "{calendar} egutegia leheneratu duzu",
"You shared calendar {calendar} as public link" : " {calendar} partekatu duzu esteka publiko gisa",
"You removed public link for calendar {calendar}" : "{calendar} egutegiaren esteka publikoa kendu duzu",
- "{actor} shared calendar {calendar} with you" : "{actor} -k zurekin {calendar} egutegia partekatu du",
+ "{actor} shared calendar {calendar} with you" : "{actor}-(e)k zurekin {calendar} egutegia partekatu du",
"You shared calendar {calendar} with {user}" : "{calendar} egutegia {user} erabiltzailearekin partekatu duzu",
- "{actor} shared calendar {calendar} with {user}" : "{actor} -k {calendar} egutegia {user}-rekin partekatu du",
- "{actor} unshared calendar {calendar} from you" : "{actor} zurekin partekatzen zuen {calendar} egutegia partekatzeari utzi dio",
- "You unshared calendar {calendar} from {user}" : "Partekatzen zenuen {calendar} egutegia {user} -rekin partekatzeari utzi diozu ",
- "{actor} unshared calendar {calendar} from {user}" : "{actor}-ek {user} erabiltzailearekin partekatzen zuen {calendar} egutegia partekatzeari utzi dio",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} {calendar} egutegia partekatzeari utzi dio bere buruarekin",
+ "{actor} shared calendar {calendar} with {user}" : "{actor}-(e)k {calendar} egutegia {user}-(r)ekin partekatu du",
+ "{actor} unshared calendar {calendar} from you" : "{actor}-(e)k zurekin {calendar} egutegia partekatzeari utzi dio",
+ "You unshared calendar {calendar} from {user}" : "{calendar} egutegia {user}-(r)ekin partekatzeari utzi diozu ",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor}-(e)k {user} erabiltzailearekin {calendar} egutegia partekatzeari utzi dio",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor}-(e)k {calendar} egutegiaren partekatzea eten du beretzat.",
"You shared calendar {calendar} with group {group}" : "{calendar} egutegia {group} taldearekin partekatu duzu",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} {group} taldearekin {calendar} egutegia partekatu du",
- "You unshared calendar {calendar} from group {group}" : "{group} taldearekin {calendar} egutegia partekatzeari utzi dio.",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor}-rk {group} taldearekin {calendar} egutegia partekatzeari utzi dio.",
+ "{actor} shared calendar {calendar} with group {group}" : "{actor}-(e)k {group} taldearekin {calendar} egutegia partekatu du",
+ "You unshared calendar {calendar} from group {group}" : "{group} taldearekin {calendar} egutegia partekatzeari utzi diozu.",
+ "{actor} unshared calendar {calendar} from group {group}" : "{actor}-(e)k {group} taldearekin {calendar} egutegia partekatzeari utzi dio.",
+ "Untitled event" : "Izenik gabeko gertaera",
"{actor} created event {event} in calendar {calendar}" : "{actor} erabiltzaileak {event} gertaera sortu du {calendar} egutegian.",
"You created event {event} in calendar {calendar}" : "{calendar} egutegian {event} gertaera sortu duzu",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} erabiltzaileak {event} gertaera ezabatu du {calendar} egutegitik",
"You deleted event {event} from calendar {calendar}" : "{event} gertaera ezabatu duzu {calendar} egutegitik ",
- "{actor} updated event {event} in calendar {calendar}" : "{actor}-(r)ek {event} gertaera eguneratu du {calendar} egutegian",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} erabiltzaileak {event} gertaera eguneratu du {calendar} egutegian",
"You updated event {event} in calendar {calendar}" : "{event} gertaera eguneratu duzu {calendar} egutegian ",
- "{actor} restored event {event} of calendar {calendar}" : "{actor}-(e)k berrezarri du {calendar} egutegiko {event} gertaera ",
- "You restored event {event} of calendar {calendar}" : "Berrezarri duzu {calendar} egutegiko {event} gertaera ",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} erabiltzaileak {event} gertaera {sourceCalendar} egutegitik {targetCalendar} egutegira mugitu du",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{event} gertaera {sourceCalendar} egutegitik {targetCalendar} egutegira mugitu duzu",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} erabiltzaileak {calendar} egutegiko {event} gertaera leheneratu du",
+ "You restored event {event} of calendar {calendar}" : "{calendar} egutegiko {event} gertaera leheneratu duzu",
"Busy" : "Lanpetua",
- "{actor} created todo {todo} in list {calendar}" : "{actor}-ek {calendar} zerrendan {todo} zeregina sortu du.",
- "You created todo {todo} in list {calendar}" : "{calendar} zerrendan {todo} ekintza sortu duzu.",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor}-rek {calendar} zerrendan {todo} zeregina borratu du.",
- "You deleted todo {todo} from list {calendar}" : " {calendar} zerrendan {todo} zeregina borratu duzu.",
- "{actor} updated todo {todo} in list {calendar}" : "{actor}-rek {calendar} zerrendan {todo} zeregina eguneratu du.",
- "You updated todo {todo} in list {calendar}" : "{calendar} zerrendan {todo} zeregina eguneratu duzu.",
- "{actor} solved todo {todo} in list {calendar}" : "{actor}-rek {calendar} zerrendan {todo} zeregina bukatu du.",
- "You solved todo {todo} in list {calendar}" : " {calendar} zerrendan {todo} zeregina bukatu duzu.",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} erabiltzaileak {calendar} zerrendan {todo} zeregina berrireki du.",
- "You reopened todo {todo} in list {calendar}" : "{calendar} egutegian {todo} zeregina berrireki duzu. ",
- "Calendar, contacts and tasks" : "Egutegia, kontaktuak eta atazak",
- "A <strong>calendar</strong> was modified" : "Egutegia aldatu da",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} erabiltzaileak {calendar} zerrendan {todo} egitekoa sortu du",
+ "You created to-do {todo} in list {calendar}" : "{calendar} zerrendan {todo} egitekoa sortu duzu",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor}-(e)k {calendar} zerrendan {todo} egitekoa ezabatu du",
+ "You deleted to-do {todo} from list {calendar}" : " {calendar} zerrendan {todo} egitekoa ezabatu duzu",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor}-(e)k {calendar} zerrendan {todo} egitekoa eguneratu du",
+ "You updated to-do {todo} in list {calendar}" : "{calendar} zerrendan {todo} egitekoa eguneratu duzu",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor}-(e)k {calendar} zerrendan {todo} egitekoa burutu du.",
+ "You solved to-do {todo} in list {calendar}" : " {calendar} zerrendan {todo} egitekoa burutu duzu",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor}-(e)k {calendar} zerrendan {todo} egitekoa berrireki du",
+ "You reopened to-do {todo} in list {calendar}" : "{calendar} zerrendan {todo} egitekoa berrireki duzu",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor}-(e)k {todo} egitekoa {sourceCalendar} zerrendatik {targetCalendar} zerrendara mugitu du",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{todo} egitekoa {sourceCalendar} zerrendatik {targetCalendar} zerrendara mugitu duzu",
+ "Calendar, contacts and tasks" : "Egutegia, kontaktuak eta zereginak",
+ "A <strong>calendar</strong> was modified" : "<strong>Egutegi</strong> bat aldatu da",
"A calendar <strong>event</strong> was modified" : "Egutegiaren <strong>gertaera</strong> bat aldatu da",
- "A calendar <strong>todo</strong> was modified" : "Egutegiaren zeregin bat aldatu da",
+ "A calendar <strong>to-do</strong> was modified" : "Egutegi baten <strong>egiteko</strong> bat aldatu da",
"Contact birthdays" : "Kontaktuen urtebetetzeak",
"Death of %s" : "%s(r)en heriotza",
+ "Untitled calendar" : "Izenik gabeko egutegia",
"Calendar:" : "Egutegia:",
"Date:" : "Data:",
"Where:" : "Non:",
"Description:" : "Deskribapena:",
- "Untitled event" : "Izenik gabeko gertaera",
"_%n year_::_%n years_" : ["Urte %n","%n urte"],
"_%n month_::_%n months_" : ["Hilabete %n","%n hilabete"],
"_%n day_::_%n days_" : ["Egun %n","%n egun"],
"_%n hour_::_%n hours_" : ["Ordu %n","%n ordu"],
"_%n minute_::_%n minutes_" : ["Minutu %n","%n minutu"],
- "%s (in %s)" : "%s (%s(r)etik)",
+ "%s (in %s)" : "%s (%s(e)tik)",
"%s (%s ago)" : "%s (orain dela %s)",
"Calendar: %s" : "Egutegia: %s",
"Date: %s" : "Data: %s",
"Description: %s" : "Deskripzioa: %s",
"Where: %s" : "Non: %s",
"%1$s via %2$s" : "%2$s bidez, %1$s",
+ "In the past on %1$s for the entire day" : "Iraganean %1$s(e)an egun osorako",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Minutu batean %1$s(e)an egun osorako","%n minututan %1$s(e)an egun osorako"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Ordu batean %1$s(e)an egun osorako","%n ordutan %1$s(e)an egun osorako"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Egun batean %1$s(e)an egun osorako","%n egunetan %1$s(e)an egun osorako"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Aste batean %1$s(e)an egun osorako","%n astetan %1$s(e)an egun osorako"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Hilabete batean %1$s(e)an egun osorako","%n hilabetetan %1$s(e)an egun osorako"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Urte batean %1$s(e)an egun osorako","%n urtetan %1$s(e)an egun osorako"],
+ "In the past on %1$s between %2$s - %3$s" : "Iraganean %1$s(e)an %2$s eta %3$s artean",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Minutu batean %1$s(e)an %2$s eta %3$s artean","%n minututan %1$s(e)an %2$s eta %3$s artean"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Ordu batean %1$s(e)an %2$s eta %3$s artean","%n ordutan %1$s(e)an %2$s eta %3$s artean"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Egun batean %1$s(e)an %2$s eta %3$s artean","%n egunetan %1$s(e)an %2$s eta %3$s artean"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Aste batean %1$s(e)an %2$s eta %3$s artean","%n astetan %1$s(e)an %2$s eta %3$s artean"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Hilabete batean %1$s(e)an %2$s eta %3$s artean","%n hilabetetan %1$s(e)an %2$s eta %3$s artean"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Urte batean %1$s(e)an %2$s eta %3$s artean","%n urtetan %1$s(e)an %2$s eta %3$s artean"],
+ "Could not generate when statement" : "Ezin izan da noiz adierazpena sortu",
+ "Every Day for the entire day" : "Egunero egun osoan",
+ "Every Day for the entire day until %1$s" : "Egunero egun osoan %1$s arte",
+ "Every Day between %1$s - %2$s" : "Egunero %1$s(e)tatik %2$s(e)tara",
+ "Every Day between %1$s - %2$s until %3$s" : "Egunero %1$s eta %2$s artean %3$s arte",
+ "Every %1$d Days for the entire day" : "%1$d egunero egun osorako",
+ "Every %1$d Days for the entire day until %2$s" : "%1$d egunero egun osorako %2$s arte",
+ "Every %1$d Days between %2$s - %3$s" : "%1$d egunero %2$s eta %3$s artean ",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "%1$d egunero %2$s eta %3$s artean %4$s arte",
+ "Could not generate event recurrence statement" : "Ezin izan da gertaeraren errepikapen adierazpena sortu",
+ "Every Week on %1$s for the entire day" : "Astero %1$s(e)an egun osoan",
+ "Every Week on %1$s for the entire day until %2$s" : "Astero %1$s(e)an egun osorako %2$s arte",
+ "Every Week on %1$s between %2$s - %3$s" : "Astero %1$s(e)an %2$s eta %3$s artean",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Astero %1$s(e)an %2$s eta %3$s artean %4$s arte ",
+ "Every %1$d Weeks on %2$s for the entire day" : "%1$d astero %2$s(e)an egun osorako",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "%1$d astero %2$s(e)an egun osorako %3$s arte",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "%1$d astero %2$s(e)an %3$s eta %4$s artean",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "%1$d astero %2$s(e)an %3$s eta %4$s artean %5$s arte",
+ "Every Month on the %1$s for the entire day" : "Hilabetero %1$s(e)an egun osoan",
+ "Every Month on the %1$s for the entire day until %2$s" : "Hilabetero %1$s(e)an egun osorako %2$s arte",
+ "Every Month on the %1$s between %2$s - %3$s" : "Hilabetero %1$s(e)an %2$s eta %3$s artean",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Hilabetero %1$s(e)an %2$s eta %3$s artean %4$s arte ",
+ "Every %1$d Months on the %2$s for the entire day" : "%1$d hilabetero %2$s(e)an egun osorako",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "%1$d hilabetero %2$s(e)an egun osorako %3$s arte",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "%1$d hilabetero %2$s(e)an %3$s eta %4$s artean",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "%1$d hilabetero %2$s(e)an %3$s eta %4$s artean %5$s arte",
+ "Every Year in %1$s on the %2$s for the entire day" : "Urtero %1$s(e)n %2$s(e)an egun osorako",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Urtero %1$s(e)n %2$s(e)an egun osorako %3$s arte",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Urtero %1$s(e)n %2$s(e)an %3$s eta %4$s artean",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Urtero %1$s(e)n %2$s(e)an %3$s eta %4$s artean %5$s arte",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "%1$d urtero %2$s(e)n %3$s(e)an egun osorako",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "%1$d urtero %2$s(e)n %3$s(e)an egun osorako %4$s arte",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "%1$d urtero %2$s(e)n %3$s(e)an %4$s eta %5$s artean",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "%1$d urtero %2$s(e)n %3$s(e)an %4$s eta %5$s artean %6$s arte",
+ "On specific dates for the entire day until %1$s" : "Egun osoan data zehatzetan %1$s arte",
+ "On specific dates between %1$s - %2$s until %3$s" : "Data zehatzetan %1$s eta %2$s artean %3$s arte",
+ "In the past on %1$s" : "Iraganean %1$s(e)an",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Minutu batean %1$s(e)an","%n minututan %1$s(e)an"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Ordu batean %1$s(e)an","%n ordutan %1$s(e)an"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Egun batean %1$s(e)an","%n egunetan %1$s(e)an"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Aste batean %1$s(e)an","%n astetan %1$s(e)an"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Hilabete batean %1$s(e)an","%n hilabetetan %1$s(e)an"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Urte batean %1$s(e)an","%n urtetan %1$s(e)an"],
+ "In the past on %1$s then on %2$s" : "Iraganean %1$s(e)an eta %2$s(e)an",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Minutu batean %1$s(e)an eta %2$s(e)an","%n minututan %1$s(e)an eta %2$s(e)an"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Ordu batean %1$s(e)an eta %2$s(e)an","%n ordutan %1$s(e)an eta %2$s(e)an"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Egun batean %1$s(e)an eta %2$s(e)an","%n egunetan %1$s(e)an eta %2$s(e)an"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Aste batean %1$s(e)an eta %2$s(e)an","%n astetan %1$s(e)an eta %2$s(e)an"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Hilabete batean %1$s(e)an eta %2$s(e)an","%n hilabetetan %1$s(e)an eta %2$s(e)an"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Urte batean %1$s(e)an eta %2$s(e)an","%n urtetan %1$s(e)an eta %2$s(e)an"],
+ "In the past on %1$s then on %2$s and %3$s" : "Iraganean %1$s(e)an, %2$s(e)an eta %3$s(e)an",
+ "_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_" : ["Minutu batean %1$s(e)an, %2$s(e)an eta %3$s(e)an","%n minututan %1$s(e)an, %2$s(e)an eta %3$s(e)an"],
+ "_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_" : ["Ordu batean %1$s(e)an, %2$s(e)an eta %3$s(e)an","%n ordutan %1$s(e)an, %2$s(e)an eta %3$s(e)an"],
+ "_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_" : ["Egun batean %1$s(e)an, %2$s(e)an eta %3$s(e)an","%n egunetan %1$s(e)an, %2$s(e)an eta %3$s(e)an"],
+ "_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_" : ["Aste batean %1$s(e)an, %2$s(e)an eta %3$s(e)an","%n astetan %1$s(e)an, %2$s(e)an eta %3$s(e)an"],
+ "_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_" : ["Hilabete batean %1$s(e)an, %2$s(e)an eta %3$s(e)an","%n hilabetetan %1$s(e)an, %2$s(e)an eta %3$s(e)an"],
+ "_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_" : ["Urte batean %1$s(e)an, %2$s(e)an eta %3$s(e)an","%n urtetan %1$s(e)an, %2$s(e)an eta %3$s(e)an"],
+ "Could not generate next recurrence statement" : "Ezin izan da sortu hurrengo errepikapen-adierazpena",
"Cancelled: %1$s" : "Utzita: %1$s",
- "Invitation canceled" : "Gonbidapena ezeztatua",
+ "\"%1$s\" has been canceled" : "\"%1$s\" bertan behera utzi da",
"Re: %1$s" : "Er: %1$s",
- "Invitation updated" : "Gonbidapena eguneratu da",
+ "%1$s has accepted your invitation" : "%1$s zure gonbidapena onartu du",
+ "%1$s has tentatively accepted your invitation" : "%1$s zure gonbidapena behin-behinean onartu du",
+ "%1$s has declined your invitation" : "%1$s zure gonbidapenari uko egin dio",
+ "%1$s has responded to your invitation" : "%1$s -(e)k zure gonbidapena erantzun du",
+ "Invitation updated: %1$s" : "Gonbidapena eguneratuta: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s-k \"%2$s\" ekitaldia eguneratu du",
"Invitation: %1$s" : "Gonbidapena: %1$s",
- "Invitation" : "Gonbidapena",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s-(e)k \"%2$s\"(e)ra gonbidatu nahi zaitu",
+ "Organizer:" : "Antolatzailea:",
+ "Attendees:" : "Parte-hartzaileak:",
"Title:" : "Izenburua:",
- "Time:" : "Noiz:",
+ "When:" : "Noiz:",
"Location:" : "Kokapena:",
"Link:" : "Esteka:",
- "Organizer:" : "Antolatzailea:",
- "Attendees:" : "Partaideak:",
+ "Occurring:" : "Gertatzen:",
"Accept" : "Onartu",
"Decline" : "Uko egin",
"More options …" : "Aukera gehiago …",
- "More options at %s" : "Aukera gehiago %s(e)n ",
+ "More options at %s" : "Aukera gehiago hemen %s ",
+ "Monday" : "Astelehena",
+ "Tuesday" : "Asteartea",
+ "Wednesday" : "Asteazkena",
+ "Thursday" : "Osteguna",
+ "Friday" : "Ostirala",
+ "Saturday" : "Larunbata",
+ "Sunday" : "Igandea",
+ "January" : "Urtarrila",
+ "February" : "Otsaila",
+ "March" : "Martxoa",
+ "April" : "Apirila",
+ "May" : "Maiatza",
+ "June" : "Ekaina",
+ "July" : "Uztaila",
+ "August" : "Abuztua",
+ "September" : "Iraila",
+ "October" : "Urria",
+ "November" : "Azaroa",
+ "December" : "Abendua",
+ "First" : "Lehenengoa",
+ "Second" : "Bigarrena",
+ "Third" : "Hirugarrena",
+ "Fourth" : "Laugarrena",
+ "Fifth" : "Bosgarrena",
+ "Last" : "Azkena",
+ "Second Last" : "Azken aurrekoa",
+ "Third Last" : "Hirugarren azkena",
+ "Fourth Last" : "Laugarren azkena",
"Contacts" : "Kontaktuak",
"{actor} created address book {addressbook}" : "{actor}-(e)k {addressbook} helbide-liburua sortu du ",
- "You created address book {addressbook}" : "Sortu duzu {addressbook} helbide-liburua ",
- "{actor} deleted address book {addressbook}" : "{actor}-(e)k ezabatu du {addressbook} helbide liburua",
- "You deleted address book {addressbook}" : "Ezabatu duzu {addressbook} helbide-liburua",
- "{actor} updated address book {addressbook}" : "{actor}-(e)k eguneratu du {addressbook} helbide-liburua ",
- "You updated address book {addressbook}" : "Eguneratu duzu {addressbook} helbide-liburua ",
- "{actor} shared address book {addressbook} with you" : "{actor}-(e)k partekatu du {addressbook} helbide-liburua",
- "You shared address book {addressbook} with {user}" : "Partekatu duzu {addressbook} helbide-liburua",
- "{actor} shared address book {addressbook} with {user}" : "{actor}-(e)k partekatu du {addressbook} helbide-liburua {user}-(r)ekin",
+ "You created address book {addressbook}" : "{addressbook} helbide-liburua sortu duzu",
+ "{actor} deleted address book {addressbook}" : "{actor}-(e)k {addressbook} helbide liburua ezabatu du",
+ "You deleted address book {addressbook}" : "{addressbook} helbide-liburua ezabatu duzu",
+ "{actor} updated address book {addressbook}" : "{actor}-(e)k {addressbook} helbide-liburua eguneratu du",
+ "You updated address book {addressbook}" : "{addressbook} helbide-liburua eguneratu duzu",
+ "{actor} shared address book {addressbook} with you" : "{actor}-(e)k {addressbook} helbide-liburua partekatu du",
+ "You shared address book {addressbook} with {user}" : "{addressbook} helbide-liburua partekatu duzu",
+ "{actor} shared address book {addressbook} with {user}" : "{actor}-(e)k {addressbook} helbide-liburua partekatu du {user}-(r)ekin",
"{actor} unshared address book {addressbook} from you" : "{actor}-(e)k {addressbook} helbide-liburua zurekin partekatzeari utzi dio",
"You unshared address book {addressbook} from {user}" : "{addressbook} helbide-liburua {user}-(r)ekin partekatzeari utzi diozu",
"{actor} unshared address book {addressbook} from {user}" : "{actor}-(e)k {addressbook} helbide-liburua {user}-(r)ekin partekatzeari utzi dio",
- "{actor} unshared address book {addressbook} from themselves" : "{actor}-(e)k {addressbook} helbide-liburua beraiekin partekatzeari utzi dio",
- "You shared address book {addressbook} with group {group}" : "Partekatu duzu {addressbook} helbide-liburua {group} taldearekin",
- "{actor} shared address book {addressbook} with group {group}" : "{actor}-(e)k partekatu du {addressbook} helbide-liburua {group} taldearekin",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor}-(e)k {addressbook} helbide-liburuaren partekatzea eten du beretzat.",
+ "You shared address book {addressbook} with group {group}" : "{addressbook} helbide-liburua partekatu duzu {group} taldearekin",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor}-(e)k {addressbook} helbide-liburua partekatu du {group} taldearekin",
"You unshared address book {addressbook} from group {group}" : "{addressbook} helbide-liburua {group} taldearekin partekatzeari utzi diozu",
"{actor} unshared address book {addressbook} from group {group}" : "{actor}-(e)k {addressbook} helbide-liburua {group} taldearekin partekatzeari utzi dio",
- "{actor} created contact {card} in address book {addressbook}" : "{actor}-(e)k sortu du {card} kontaktua {addressbook} helbide-liburuan",
- "You created contact {card} in address book {addressbook}" : "Sortu duzu {card} kontaktua {addressbook} helbide-liburuan",
- "{actor} deleted contact {card} from address book {addressbook}" : "{actor}-(e)k ezabatu du {card} kontaktua {addressbook} helbide-liburutik",
- "You deleted contact {card} from address book {addressbook}" : "Ezabatu duzu {card} kontaktua {addressbook} helbide-liburutik",
- "{actor} updated contact {card} in address book {addressbook}" : "{actor}-(e)k eguneratu du {card} kontaktua {addressbook} helbide-liburuan",
- "You updated contact {card} in address book {addressbook}" : "Eguneratu duzu {card} kontaktua {addressbook} helbide-liburuan",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor}-(e)k {card} kontaktua sortu du {addressbook} helbide-liburuan",
+ "You created contact {card} in address book {addressbook}" : "{card} kontaktua sortu duzu {addressbook} helbide-liburuan",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor}-(e)k {card} kontaktua ezabatu du {addressbook} helbide-liburutik",
+ "You deleted contact {card} from address book {addressbook}" : "{card} kontaktua ezabatu duzu {addressbook} helbide-liburutik",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor}-(e)k {card} kontaktua eguneratu du {addressbook} helbide-liburuan",
+ "You updated contact {card} in address book {addressbook}" : "{card} kontaktua eguneratu duzu {addressbook} helbide-liburuan",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>kontaktu</strong> edo <strong>helbide-liburu</strong>bat aldatu da",
+ "Accounts" : "Kontuak",
+ "System address book which holds all accounts" : "Kontu guztiak dituen sistemaren helbide-liburua",
+ "File is not updatable: %1$s" : "Fitxategia ez da eguneragarria: %1$s",
+ "Failed to get storage for file" : "Fitxategirako biltegia lortzeak huts egin du",
+ "Could not write to final file, canceled by hook" : "Ezin izan da azken fitxategian idatzi, kakoak bertan behera utzi du",
+ "Could not write file contents" : "Ezin izan dira fitxategiaren edukiak idatzi",
"_%n byte_::_%n bytes_" : ["Byte %n","%n byte"],
- "Could not open file" : "Ezin izan da fitxategia ireki",
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Errore bat gertatu da fitxategia helburuko kokapenera kopiatzean (kopiatua: %1$s, espero zen fitxategi-tamaina: %2$s)",
+ "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." : "%1$sfitxategi-tamaina espero zen baina irakurria (Nextcloud bezerotik) eta idatzia (Nextcloud biltegian) %2$sizan da. Bidaltzailearen aldean sareko arazo bat izan daiteke edo zerbitzariaren arazo bat biltegian idazteko.",
+ "Could not rename part file to final file, canceled by hook" : "Ezin izan da zati-fitxategiaren izena aldatu azken fitxategira, kakoak bertan behera utzi du",
+ "Could not rename part file to final file" : "Ezin izan da zati-fitxategia azken fitxategira aldatu",
+ "Failed to check file size: %1$s" : "Ezin izan da egiaztatu fitxategiaren tamaina:%1$s",
+ "Could not open file: %1$s, file does seem to exist" : "Ezin da fitxategi hau ireki: %1$s, badirudi fitxategia existitzen dela",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Ezin da fitxategi hau ireki: %1$s, badirudi fitxategia ez dela existitzen",
+ "Encryption not ready: %1$s" : "Enkriptatzea ez dago prest:%1$s",
+ "Failed to open file: %1$s" : "Ezin izan da fitxategia ireki:%1$s",
+ "Failed to unlink: %1$s" : "Ezin izan da deskonektatu:%1$s",
+ "Failed to write file contents: %1$s" : "Ezin izan dira fitxategiaren edukiak idatzi:%1$s",
+ "File not found: %1$s" : "Ez da fitxategirik aurkitu:%1$s",
+ "Invalid target path" : "Helburu bide-izen baliogabea",
"System is in maintenance mode." : "Sistema mantentze moduan dago.",
"Upgrade needed" : "Bertsio-berritzea beharrezkoa",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Zure %s HTTPS erabiltzeko konfiguratu behar da CalDAV eta CardDAV erabiltzeko iOS eta macOSrekin.",
"Configures a CalDAV account" : "CalDAV kontu bat konfiguratzen du",
"Configures a CardDAV account" : "CardDAV kontu bat konfiguratzen du",
"Events" : "Gertaerak",
- "Tasks" : "Zereginak",
"Untitled task" : "Izenik gabeko zeregina",
"Completed on %s" : "%s-an osatua",
- "Due on %s by %s" : "%s-(e)tik %s-(e)an epemuga",
+ "Due on %s by %s" : "%s-(e)an epemuga %s-(e)k",
"Due on %s" : "%s-(e)an epemuga",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Ongi etorri Nextcloud Egutegira!\n\nHau gertaera erakusgarria da - aztertu plangintzaren malgutasuna Nextcloud Egutegiarekin nahi dituzun edizioak eginez!\n\nNextcloud Egutegia aukerarekin, hau egin dezakezu:\n- Sortu, editatu eta kudeatu gertaerak esfortzurik gabe.\n- Egutegi ugari sortu eta taldekideekin, lagunekin edo familiarekin partekatu.\n- Egiaztatu libre egotea eta bistaratu zure laneko orduak beste batzuei.\n- Aplikazio eta gailuekin arazorik gabe integratzea CalDAV bidez.\n- Zure esperientzia pertsonalizatu: gertaera errepikariak programatu, jakinarazpenak doitu eta bestelako ezarpenak.",
+ "Example event - open me!" : "Gertaera adibidea - ireki nazazu!",
+ "System Address Book" : "Sistemaren helbide-liburua",
+ "The system address book contains contact information for all users in your instance." : "Sistemaren helbide-liburuak zure instantziako erabiltzaile guztien kontaktu-informazioa dauka.",
+ "Enable System Address Book" : "Gaitu sistemaren helbide-liburua",
+ "DAV system address book" : "DAV sistemaren helbide-liburua",
+ "No outstanding DAV system address book sync." : "Ez dago DAV sistema helbide-liburuaren sinkronizazio arrarorik.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV sistemaren helbide-liburuaren sinkronizazioa oraindik ez da martxan jarri zure instantziak 1000 erabiltzaile baino gehiago dituelako edo akats bat gertatu delako. Mesedez, exekutatu eskuz \"occ dav:sync-system-addressbook\" deituz.",
+ "WebDAV endpoint" : "WebDAV amaiera-puntua",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Ezin izan da egiaztatu zure zerbitzaria WebDAV bidezko fitxategi-sinkronizazioa onartzeko ondo konfiguratuta badagoen. Mesedez, egiaztatu ezazu eskuz.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Zure web zerbitzaria ez dago behar bezala konfiguratuta fitxategien sinkronizazioa baimentzeko, WebDAV interfazea puskatuta dagoela dirudi.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Zure web zerbitzaria WebDAV bidezko fitxategien sinkronizazioa onartzeko ondo konfiguratuta dago.",
+ "Migrated calendar (%1$s)" : "Migratutako egutegia (%1$s)",
+ "Calendars including events, details and attendees" : "Egutegiak, gertaerak, xehetasunak eta parte-hartzaileak barne",
"Contacts and groups" : "Kontaktuak eta taldeak",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV amaiera-puntua",
- "Availability" : "Eskuragarritasuna",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Zure lan orduak konfiguratzen badituzu, beste erabiltzaileak bulegotik kanpo zaudela ikusiko dute bilera bat erreserbatzen dutenean.",
+ "Absence saved" : "Absentzia gordeta",
+ "Failed to save your absence settings" : "Ezin izan dira zure absentzia ezarpenak gorde",
+ "Absence cleared" : "Abszentzia ezabatuta",
+ "Failed to clear your absence settings" : "Ezin izan dira zure absentzia ezarpenak garbitu",
+ "First day" : "Lehen eguna",
+ "Last day (inclusive)" : "Azken eguna (barne)",
+ "Out of office replacement (optional)" : "Bulegotik kanpo ordezkatzea (aukerakoa)",
+ "Name of the replacement" : "Ordezkoaren izena",
+ "No results." : "Ez dago emaitzarik.",
+ "Start typing." : "Hasi idazten.",
+ "Short absence status" : "Absentzia-egoera laburra",
+ "Long absence Message" : "Absentzia-mezu luzea",
+ "Save" : "Gorde",
+ "Disable absence" : "Desgaitu absentzia",
+ "Failed to load availability" : "Ezin izan da eskuragarritasuna kargatu",
+ "Saved availability" : "Eskuragarritasuna gorde da",
+ "Failed to save availability" : "Ezin izan da eskuragarritasuna gorde",
"Time zone:" : "Ordu-zona:",
"to" : "honi",
"Delete slot" : "Ezabatu tartea",
"No working hours set" : "Ez dira laneko orduak ezarri",
"Add slot" : "Gehitu tartea",
- "Monday" : "Astelehena",
- "Tuesday" : "Asteartea",
- "Wednesday" : "Asteazkena",
- "Thursday" : "Osteguna",
- "Friday" : "Ostirala",
- "Saturday" : "Larunbata",
- "Sunday" : "Igandea",
- "Save" : "Gorde",
+ "Weekdays" : "Astegunak",
+ "Pick a start time for {dayName}" : "Hautatu hasiera ordu bat {dayName}(e)rako",
+ "Pick a end time for {dayName}" : "Hautatu bukaera ordu bat {dayName}(e)rako",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Eskuragarri ez egotean, ezarri automatikoki erabiltzailearen egoera \"Ez molestatu\" moduan jakinarazpen guztiak isilarazteko.",
+ "Cancel" : "Utzi",
+ "Import" : "Inportatu",
+ "Error while saving settings" : "Errorea ezarpenak gordetzean",
+ "Contact reset successfully" : "Kontaktua behar bezala berrezarri da",
+ "Error while resetting contact" : "Errorea kontaktua berrezartzean",
+ "Contact imported successfully" : "Kontaktua behar bezala inportatu da",
+ "Error while importing contact" : "Errorea kontaktua inportatzean",
+ "Import contact" : "Inportatu kontaktua",
+ "Reset to default" : "Berezarri balio lehenetsira",
+ "Import contacts" : "Inportatu kontaktuak",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : ".vcf fitxategi berri bat inportatzean, lehendik dagoen kontaktu lehenetsia ezabatu eta berriarekin ordeztuko da. Jarraitu nahi duzu?",
+ "Failed to save example event creation setting" : "Adibide gertaeraren sortze ezarpenak gordetzeak huts egin du",
+ "Failed to upload the example event" : "Adibide gertaera igotzeak huts egin du",
+ "Custom example event was saved successfully" : "Adibide gertaera pertsonalizatua behar bezala gorde da",
+ "Failed to delete the custom example event" : "Adibide gertaera pertsonalizatua ezabatzeak huts egin du",
+ "Custom example event was deleted successfully" : "Adibide gertaera pertsonalizatua behar bezala ezabatu da.",
+ "Import calendar event" : "Inportatu egutegiko gertaera",
+ "Uploading a new event will overwrite the existing one." : "Gertaera berri bat igotzeak dagoena gainidatz dezake",
+ "Upload event" : "Igo gertaera",
+ "Availability" : "Eskuragarritasuna",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Zure lan orduak konfiguratzen badituzu, beste pertsonek bulegotik kanpo zaudela ikusiko dute bilera bat erreserbatzen dutenean.",
+ "Absence" : "Absentzia",
+ "Configure your next absence period." : "Konfiguratu zure hurrengo absentzia aldia.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instalatu ezazu {calendarappstoreopen}Egutegi aplikazioa{linkclose} ere, edo {calendardocopen}konektatu zure ordenagailua eta mugikorra sinkronizatzeko ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Egiaztatu {emailopen}posta elektroniko zerbitzaria{linkclose} ondo konfiguratuta dagoela.",
"Calendar server" : "Egutegi-zerbitzaria",
- "Send invitations to attendees" : "Gonbidatutakoei gonbidapenak bidali",
+ "Send invitations to attendees" : "Parte-hartzaileei gonbidapenak bidali",
"Automatically generate a birthday calendar" : "Automatikoki sortu urtebetetzeen egutegia",
"Birthday calendars will be generated by a background job." : "Urtebetetze egutegiak atzealdeko lan batek sortuko ditu.",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Beraz ez dira gaitu ostean agertuko baina denbora pasa ahala agertuko dira.",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "Beraz ez dira gaitu bezain laster prest egongo, baina denbora bat pasatzean agertuko dira.",
"Send notifications for events" : "Bidali jakinarazpenak gertaerentzako",
"Notifications are sent via background jobs, so these must occur often enough." : "Jakinarazpenak atzealdeko lanen bidez bidaliko dira, beraz sarri gertatu behar dira.",
+ "Send reminder notifications to calendar sharees as well" : "Bidali gogorarazpen jakinarazpenak egutegi partekatzea dutenei ere",
+ "Reminders are always sent to organizers and attendees." : "Gogorarazpenak beti bidaltzen zaizkie antolatzaileei eta parte-hartzaileei.",
"Enable notifications for events via push" : "Gaitu push bidezko jakinarazpenak gertaerentzat",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instalatu ezazu {calendarappstoreopen}Egutegi aplikazioa{linkclose} ere, edo {calendardocopen}konektatu zure ordenagailua eta mugikorra sinkronizatzeko ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Mesedez egiaztatu {emailopen}posta elektroniko zerbitzaria{linkclose} ondo konfiguratuta dagoela.",
+ "Example content" : "Adibideko edukia",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Adibideko edukiak Nextcloud-en ezaugarriak erakusteko balio du. Eduki lehenetsia Nextcloud-ekin bidaltzen da, eta eduki pertsonalizatuarekin ordezka daiteke.",
"There was an error updating your attendance status." : "Errore bat gertatu da zure parte-hartze egoera eguneratzerakoan.",
"Please contact the organizer directly." : "Mesedez jarri harremanetan antolatzailearekin zuzenean.",
"Are you accepting the invitation?" : "Gonbidapena onartzen duzu?",
"Tentative" : "Behin behinekoa",
- "Number of guests" : "Gonbidatu kopurua",
- "Comment" : "Iruzkindu",
- "Your attendance was updated successfully." : "Zure parte-hartzea ondo eguneratu da.",
- "Calendar and tasks" : "Egutegia eta atazak"
+ "Your attendance was updated successfully." : "Zure parte-hartzea ondo eguneratu da."
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/eu.json b/apps/dav/l10n/eu.json
index 27d6c6faedc..5d312d36973 100644
--- a/apps/dav/l10n/eu.json
+++ b/apps/dav/l10n/eu.json
@@ -1,159 +1,335 @@
{ "translations": {
"Calendar" : "Egutegia",
- "Todos" : "Egitekoak",
+ "Tasks" : "Zereginak",
"Personal" : "Pertsonala",
- "{actor} created calendar {calendar}" : "{actor}-k sortutako egutegia: {calendar}",
+ "{actor} created calendar {calendar}" : "{actor}-(e)k sortutako egutegia: {calendar}",
"You created calendar {calendar}" : "{calendar} egutegia sortu duzu",
- "{actor} deleted calendar {calendar}" : "{actor}-k {calendar} egutegia borratu du",
- "You deleted calendar {calendar}" : "{calendar} egutegia borratu duzu",
- "{actor} updated calendar {calendar}" : "{actor} -k {calendar} egutegia eguneratu du",
+ "{actor} deleted calendar {calendar}" : "{actor}-(e)k {calendar} egutegia ezabatu du",
+ "You deleted calendar {calendar}" : "{calendar} egutegia ezabatu duzu",
+ "{actor} updated calendar {calendar}" : "{actor}-(e)k {calendar} egutegia eguneratu du",
"You updated calendar {calendar}" : "{calendar} egutegia eguneratu duzu ",
- "{actor} restored calendar {calendar}" : "{actor}-(e)k berrezarritako {calendar} egutegia",
- "You restored calendar {calendar}" : "Berrezarri duzu {calendar} egutegia",
+ "{actor} restored calendar {calendar}" : "{actor}-(e)k {calendar} egutegia leheneratu du",
+ "You restored calendar {calendar}" : "{calendar} egutegia leheneratu duzu",
"You shared calendar {calendar} as public link" : " {calendar} partekatu duzu esteka publiko gisa",
"You removed public link for calendar {calendar}" : "{calendar} egutegiaren esteka publikoa kendu duzu",
- "{actor} shared calendar {calendar} with you" : "{actor} -k zurekin {calendar} egutegia partekatu du",
+ "{actor} shared calendar {calendar} with you" : "{actor}-(e)k zurekin {calendar} egutegia partekatu du",
"You shared calendar {calendar} with {user}" : "{calendar} egutegia {user} erabiltzailearekin partekatu duzu",
- "{actor} shared calendar {calendar} with {user}" : "{actor} -k {calendar} egutegia {user}-rekin partekatu du",
- "{actor} unshared calendar {calendar} from you" : "{actor} zurekin partekatzen zuen {calendar} egutegia partekatzeari utzi dio",
- "You unshared calendar {calendar} from {user}" : "Partekatzen zenuen {calendar} egutegia {user} -rekin partekatzeari utzi diozu ",
- "{actor} unshared calendar {calendar} from {user}" : "{actor}-ek {user} erabiltzailearekin partekatzen zuen {calendar} egutegia partekatzeari utzi dio",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} {calendar} egutegia partekatzeari utzi dio bere buruarekin",
+ "{actor} shared calendar {calendar} with {user}" : "{actor}-(e)k {calendar} egutegia {user}-(r)ekin partekatu du",
+ "{actor} unshared calendar {calendar} from you" : "{actor}-(e)k zurekin {calendar} egutegia partekatzeari utzi dio",
+ "You unshared calendar {calendar} from {user}" : "{calendar} egutegia {user}-(r)ekin partekatzeari utzi diozu ",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor}-(e)k {user} erabiltzailearekin {calendar} egutegia partekatzeari utzi dio",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor}-(e)k {calendar} egutegiaren partekatzea eten du beretzat.",
"You shared calendar {calendar} with group {group}" : "{calendar} egutegia {group} taldearekin partekatu duzu",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} {group} taldearekin {calendar} egutegia partekatu du",
- "You unshared calendar {calendar} from group {group}" : "{group} taldearekin {calendar} egutegia partekatzeari utzi dio.",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor}-rk {group} taldearekin {calendar} egutegia partekatzeari utzi dio.",
+ "{actor} shared calendar {calendar} with group {group}" : "{actor}-(e)k {group} taldearekin {calendar} egutegia partekatu du",
+ "You unshared calendar {calendar} from group {group}" : "{group} taldearekin {calendar} egutegia partekatzeari utzi diozu.",
+ "{actor} unshared calendar {calendar} from group {group}" : "{actor}-(e)k {group} taldearekin {calendar} egutegia partekatzeari utzi dio.",
+ "Untitled event" : "Izenik gabeko gertaera",
"{actor} created event {event} in calendar {calendar}" : "{actor} erabiltzaileak {event} gertaera sortu du {calendar} egutegian.",
"You created event {event} in calendar {calendar}" : "{calendar} egutegian {event} gertaera sortu duzu",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} erabiltzaileak {event} gertaera ezabatu du {calendar} egutegitik",
"You deleted event {event} from calendar {calendar}" : "{event} gertaera ezabatu duzu {calendar} egutegitik ",
- "{actor} updated event {event} in calendar {calendar}" : "{actor}-(r)ek {event} gertaera eguneratu du {calendar} egutegian",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} erabiltzaileak {event} gertaera eguneratu du {calendar} egutegian",
"You updated event {event} in calendar {calendar}" : "{event} gertaera eguneratu duzu {calendar} egutegian ",
- "{actor} restored event {event} of calendar {calendar}" : "{actor}-(e)k berrezarri du {calendar} egutegiko {event} gertaera ",
- "You restored event {event} of calendar {calendar}" : "Berrezarri duzu {calendar} egutegiko {event} gertaera ",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} erabiltzaileak {event} gertaera {sourceCalendar} egutegitik {targetCalendar} egutegira mugitu du",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{event} gertaera {sourceCalendar} egutegitik {targetCalendar} egutegira mugitu duzu",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} erabiltzaileak {calendar} egutegiko {event} gertaera leheneratu du",
+ "You restored event {event} of calendar {calendar}" : "{calendar} egutegiko {event} gertaera leheneratu duzu",
"Busy" : "Lanpetua",
- "{actor} created todo {todo} in list {calendar}" : "{actor}-ek {calendar} zerrendan {todo} zeregina sortu du.",
- "You created todo {todo} in list {calendar}" : "{calendar} zerrendan {todo} ekintza sortu duzu.",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor}-rek {calendar} zerrendan {todo} zeregina borratu du.",
- "You deleted todo {todo} from list {calendar}" : " {calendar} zerrendan {todo} zeregina borratu duzu.",
- "{actor} updated todo {todo} in list {calendar}" : "{actor}-rek {calendar} zerrendan {todo} zeregina eguneratu du.",
- "You updated todo {todo} in list {calendar}" : "{calendar} zerrendan {todo} zeregina eguneratu duzu.",
- "{actor} solved todo {todo} in list {calendar}" : "{actor}-rek {calendar} zerrendan {todo} zeregina bukatu du.",
- "You solved todo {todo} in list {calendar}" : " {calendar} zerrendan {todo} zeregina bukatu duzu.",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} erabiltzaileak {calendar} zerrendan {todo} zeregina berrireki du.",
- "You reopened todo {todo} in list {calendar}" : "{calendar} egutegian {todo} zeregina berrireki duzu. ",
- "Calendar, contacts and tasks" : "Egutegia, kontaktuak eta atazak",
- "A <strong>calendar</strong> was modified" : "Egutegia aldatu da",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} erabiltzaileak {calendar} zerrendan {todo} egitekoa sortu du",
+ "You created to-do {todo} in list {calendar}" : "{calendar} zerrendan {todo} egitekoa sortu duzu",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor}-(e)k {calendar} zerrendan {todo} egitekoa ezabatu du",
+ "You deleted to-do {todo} from list {calendar}" : " {calendar} zerrendan {todo} egitekoa ezabatu duzu",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor}-(e)k {calendar} zerrendan {todo} egitekoa eguneratu du",
+ "You updated to-do {todo} in list {calendar}" : "{calendar} zerrendan {todo} egitekoa eguneratu duzu",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor}-(e)k {calendar} zerrendan {todo} egitekoa burutu du.",
+ "You solved to-do {todo} in list {calendar}" : " {calendar} zerrendan {todo} egitekoa burutu duzu",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor}-(e)k {calendar} zerrendan {todo} egitekoa berrireki du",
+ "You reopened to-do {todo} in list {calendar}" : "{calendar} zerrendan {todo} egitekoa berrireki duzu",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor}-(e)k {todo} egitekoa {sourceCalendar} zerrendatik {targetCalendar} zerrendara mugitu du",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{todo} egitekoa {sourceCalendar} zerrendatik {targetCalendar} zerrendara mugitu duzu",
+ "Calendar, contacts and tasks" : "Egutegia, kontaktuak eta zereginak",
+ "A <strong>calendar</strong> was modified" : "<strong>Egutegi</strong> bat aldatu da",
"A calendar <strong>event</strong> was modified" : "Egutegiaren <strong>gertaera</strong> bat aldatu da",
- "A calendar <strong>todo</strong> was modified" : "Egutegiaren zeregin bat aldatu da",
+ "A calendar <strong>to-do</strong> was modified" : "Egutegi baten <strong>egiteko</strong> bat aldatu da",
"Contact birthdays" : "Kontaktuen urtebetetzeak",
"Death of %s" : "%s(r)en heriotza",
+ "Untitled calendar" : "Izenik gabeko egutegia",
"Calendar:" : "Egutegia:",
"Date:" : "Data:",
"Where:" : "Non:",
"Description:" : "Deskribapena:",
- "Untitled event" : "Izenik gabeko gertaera",
"_%n year_::_%n years_" : ["Urte %n","%n urte"],
"_%n month_::_%n months_" : ["Hilabete %n","%n hilabete"],
"_%n day_::_%n days_" : ["Egun %n","%n egun"],
"_%n hour_::_%n hours_" : ["Ordu %n","%n ordu"],
"_%n minute_::_%n minutes_" : ["Minutu %n","%n minutu"],
- "%s (in %s)" : "%s (%s(r)etik)",
+ "%s (in %s)" : "%s (%s(e)tik)",
"%s (%s ago)" : "%s (orain dela %s)",
"Calendar: %s" : "Egutegia: %s",
"Date: %s" : "Data: %s",
"Description: %s" : "Deskripzioa: %s",
"Where: %s" : "Non: %s",
"%1$s via %2$s" : "%2$s bidez, %1$s",
+ "In the past on %1$s for the entire day" : "Iraganean %1$s(e)an egun osorako",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Minutu batean %1$s(e)an egun osorako","%n minututan %1$s(e)an egun osorako"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Ordu batean %1$s(e)an egun osorako","%n ordutan %1$s(e)an egun osorako"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Egun batean %1$s(e)an egun osorako","%n egunetan %1$s(e)an egun osorako"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Aste batean %1$s(e)an egun osorako","%n astetan %1$s(e)an egun osorako"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Hilabete batean %1$s(e)an egun osorako","%n hilabetetan %1$s(e)an egun osorako"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Urte batean %1$s(e)an egun osorako","%n urtetan %1$s(e)an egun osorako"],
+ "In the past on %1$s between %2$s - %3$s" : "Iraganean %1$s(e)an %2$s eta %3$s artean",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Minutu batean %1$s(e)an %2$s eta %3$s artean","%n minututan %1$s(e)an %2$s eta %3$s artean"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Ordu batean %1$s(e)an %2$s eta %3$s artean","%n ordutan %1$s(e)an %2$s eta %3$s artean"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Egun batean %1$s(e)an %2$s eta %3$s artean","%n egunetan %1$s(e)an %2$s eta %3$s artean"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Aste batean %1$s(e)an %2$s eta %3$s artean","%n astetan %1$s(e)an %2$s eta %3$s artean"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Hilabete batean %1$s(e)an %2$s eta %3$s artean","%n hilabetetan %1$s(e)an %2$s eta %3$s artean"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Urte batean %1$s(e)an %2$s eta %3$s artean","%n urtetan %1$s(e)an %2$s eta %3$s artean"],
+ "Could not generate when statement" : "Ezin izan da noiz adierazpena sortu",
+ "Every Day for the entire day" : "Egunero egun osoan",
+ "Every Day for the entire day until %1$s" : "Egunero egun osoan %1$s arte",
+ "Every Day between %1$s - %2$s" : "Egunero %1$s(e)tatik %2$s(e)tara",
+ "Every Day between %1$s - %2$s until %3$s" : "Egunero %1$s eta %2$s artean %3$s arte",
+ "Every %1$d Days for the entire day" : "%1$d egunero egun osorako",
+ "Every %1$d Days for the entire day until %2$s" : "%1$d egunero egun osorako %2$s arte",
+ "Every %1$d Days between %2$s - %3$s" : "%1$d egunero %2$s eta %3$s artean ",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "%1$d egunero %2$s eta %3$s artean %4$s arte",
+ "Could not generate event recurrence statement" : "Ezin izan da gertaeraren errepikapen adierazpena sortu",
+ "Every Week on %1$s for the entire day" : "Astero %1$s(e)an egun osoan",
+ "Every Week on %1$s for the entire day until %2$s" : "Astero %1$s(e)an egun osorako %2$s arte",
+ "Every Week on %1$s between %2$s - %3$s" : "Astero %1$s(e)an %2$s eta %3$s artean",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Astero %1$s(e)an %2$s eta %3$s artean %4$s arte ",
+ "Every %1$d Weeks on %2$s for the entire day" : "%1$d astero %2$s(e)an egun osorako",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "%1$d astero %2$s(e)an egun osorako %3$s arte",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "%1$d astero %2$s(e)an %3$s eta %4$s artean",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "%1$d astero %2$s(e)an %3$s eta %4$s artean %5$s arte",
+ "Every Month on the %1$s for the entire day" : "Hilabetero %1$s(e)an egun osoan",
+ "Every Month on the %1$s for the entire day until %2$s" : "Hilabetero %1$s(e)an egun osorako %2$s arte",
+ "Every Month on the %1$s between %2$s - %3$s" : "Hilabetero %1$s(e)an %2$s eta %3$s artean",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Hilabetero %1$s(e)an %2$s eta %3$s artean %4$s arte ",
+ "Every %1$d Months on the %2$s for the entire day" : "%1$d hilabetero %2$s(e)an egun osorako",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "%1$d hilabetero %2$s(e)an egun osorako %3$s arte",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "%1$d hilabetero %2$s(e)an %3$s eta %4$s artean",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "%1$d hilabetero %2$s(e)an %3$s eta %4$s artean %5$s arte",
+ "Every Year in %1$s on the %2$s for the entire day" : "Urtero %1$s(e)n %2$s(e)an egun osorako",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Urtero %1$s(e)n %2$s(e)an egun osorako %3$s arte",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Urtero %1$s(e)n %2$s(e)an %3$s eta %4$s artean",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Urtero %1$s(e)n %2$s(e)an %3$s eta %4$s artean %5$s arte",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "%1$d urtero %2$s(e)n %3$s(e)an egun osorako",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "%1$d urtero %2$s(e)n %3$s(e)an egun osorako %4$s arte",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "%1$d urtero %2$s(e)n %3$s(e)an %4$s eta %5$s artean",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "%1$d urtero %2$s(e)n %3$s(e)an %4$s eta %5$s artean %6$s arte",
+ "On specific dates for the entire day until %1$s" : "Egun osoan data zehatzetan %1$s arte",
+ "On specific dates between %1$s - %2$s until %3$s" : "Data zehatzetan %1$s eta %2$s artean %3$s arte",
+ "In the past on %1$s" : "Iraganean %1$s(e)an",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Minutu batean %1$s(e)an","%n minututan %1$s(e)an"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Ordu batean %1$s(e)an","%n ordutan %1$s(e)an"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Egun batean %1$s(e)an","%n egunetan %1$s(e)an"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Aste batean %1$s(e)an","%n astetan %1$s(e)an"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Hilabete batean %1$s(e)an","%n hilabetetan %1$s(e)an"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Urte batean %1$s(e)an","%n urtetan %1$s(e)an"],
+ "In the past on %1$s then on %2$s" : "Iraganean %1$s(e)an eta %2$s(e)an",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Minutu batean %1$s(e)an eta %2$s(e)an","%n minututan %1$s(e)an eta %2$s(e)an"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Ordu batean %1$s(e)an eta %2$s(e)an","%n ordutan %1$s(e)an eta %2$s(e)an"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Egun batean %1$s(e)an eta %2$s(e)an","%n egunetan %1$s(e)an eta %2$s(e)an"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Aste batean %1$s(e)an eta %2$s(e)an","%n astetan %1$s(e)an eta %2$s(e)an"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Hilabete batean %1$s(e)an eta %2$s(e)an","%n hilabetetan %1$s(e)an eta %2$s(e)an"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Urte batean %1$s(e)an eta %2$s(e)an","%n urtetan %1$s(e)an eta %2$s(e)an"],
+ "In the past on %1$s then on %2$s and %3$s" : "Iraganean %1$s(e)an, %2$s(e)an eta %3$s(e)an",
+ "_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_" : ["Minutu batean %1$s(e)an, %2$s(e)an eta %3$s(e)an","%n minututan %1$s(e)an, %2$s(e)an eta %3$s(e)an"],
+ "_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_" : ["Ordu batean %1$s(e)an, %2$s(e)an eta %3$s(e)an","%n ordutan %1$s(e)an, %2$s(e)an eta %3$s(e)an"],
+ "_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_" : ["Egun batean %1$s(e)an, %2$s(e)an eta %3$s(e)an","%n egunetan %1$s(e)an, %2$s(e)an eta %3$s(e)an"],
+ "_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_" : ["Aste batean %1$s(e)an, %2$s(e)an eta %3$s(e)an","%n astetan %1$s(e)an, %2$s(e)an eta %3$s(e)an"],
+ "_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_" : ["Hilabete batean %1$s(e)an, %2$s(e)an eta %3$s(e)an","%n hilabetetan %1$s(e)an, %2$s(e)an eta %3$s(e)an"],
+ "_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_" : ["Urte batean %1$s(e)an, %2$s(e)an eta %3$s(e)an","%n urtetan %1$s(e)an, %2$s(e)an eta %3$s(e)an"],
+ "Could not generate next recurrence statement" : "Ezin izan da sortu hurrengo errepikapen-adierazpena",
"Cancelled: %1$s" : "Utzita: %1$s",
- "Invitation canceled" : "Gonbidapena ezeztatua",
+ "\"%1$s\" has been canceled" : "\"%1$s\" bertan behera utzi da",
"Re: %1$s" : "Er: %1$s",
- "Invitation updated" : "Gonbidapena eguneratu da",
+ "%1$s has accepted your invitation" : "%1$s zure gonbidapena onartu du",
+ "%1$s has tentatively accepted your invitation" : "%1$s zure gonbidapena behin-behinean onartu du",
+ "%1$s has declined your invitation" : "%1$s zure gonbidapenari uko egin dio",
+ "%1$s has responded to your invitation" : "%1$s -(e)k zure gonbidapena erantzun du",
+ "Invitation updated: %1$s" : "Gonbidapena eguneratuta: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s-k \"%2$s\" ekitaldia eguneratu du",
"Invitation: %1$s" : "Gonbidapena: %1$s",
- "Invitation" : "Gonbidapena",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s-(e)k \"%2$s\"(e)ra gonbidatu nahi zaitu",
+ "Organizer:" : "Antolatzailea:",
+ "Attendees:" : "Parte-hartzaileak:",
"Title:" : "Izenburua:",
- "Time:" : "Noiz:",
+ "When:" : "Noiz:",
"Location:" : "Kokapena:",
"Link:" : "Esteka:",
- "Organizer:" : "Antolatzailea:",
- "Attendees:" : "Partaideak:",
+ "Occurring:" : "Gertatzen:",
"Accept" : "Onartu",
"Decline" : "Uko egin",
"More options …" : "Aukera gehiago …",
- "More options at %s" : "Aukera gehiago %s(e)n ",
+ "More options at %s" : "Aukera gehiago hemen %s ",
+ "Monday" : "Astelehena",
+ "Tuesday" : "Asteartea",
+ "Wednesday" : "Asteazkena",
+ "Thursday" : "Osteguna",
+ "Friday" : "Ostirala",
+ "Saturday" : "Larunbata",
+ "Sunday" : "Igandea",
+ "January" : "Urtarrila",
+ "February" : "Otsaila",
+ "March" : "Martxoa",
+ "April" : "Apirila",
+ "May" : "Maiatza",
+ "June" : "Ekaina",
+ "July" : "Uztaila",
+ "August" : "Abuztua",
+ "September" : "Iraila",
+ "October" : "Urria",
+ "November" : "Azaroa",
+ "December" : "Abendua",
+ "First" : "Lehenengoa",
+ "Second" : "Bigarrena",
+ "Third" : "Hirugarrena",
+ "Fourth" : "Laugarrena",
+ "Fifth" : "Bosgarrena",
+ "Last" : "Azkena",
+ "Second Last" : "Azken aurrekoa",
+ "Third Last" : "Hirugarren azkena",
+ "Fourth Last" : "Laugarren azkena",
"Contacts" : "Kontaktuak",
"{actor} created address book {addressbook}" : "{actor}-(e)k {addressbook} helbide-liburua sortu du ",
- "You created address book {addressbook}" : "Sortu duzu {addressbook} helbide-liburua ",
- "{actor} deleted address book {addressbook}" : "{actor}-(e)k ezabatu du {addressbook} helbide liburua",
- "You deleted address book {addressbook}" : "Ezabatu duzu {addressbook} helbide-liburua",
- "{actor} updated address book {addressbook}" : "{actor}-(e)k eguneratu du {addressbook} helbide-liburua ",
- "You updated address book {addressbook}" : "Eguneratu duzu {addressbook} helbide-liburua ",
- "{actor} shared address book {addressbook} with you" : "{actor}-(e)k partekatu du {addressbook} helbide-liburua",
- "You shared address book {addressbook} with {user}" : "Partekatu duzu {addressbook} helbide-liburua",
- "{actor} shared address book {addressbook} with {user}" : "{actor}-(e)k partekatu du {addressbook} helbide-liburua {user}-(r)ekin",
+ "You created address book {addressbook}" : "{addressbook} helbide-liburua sortu duzu",
+ "{actor} deleted address book {addressbook}" : "{actor}-(e)k {addressbook} helbide liburua ezabatu du",
+ "You deleted address book {addressbook}" : "{addressbook} helbide-liburua ezabatu duzu",
+ "{actor} updated address book {addressbook}" : "{actor}-(e)k {addressbook} helbide-liburua eguneratu du",
+ "You updated address book {addressbook}" : "{addressbook} helbide-liburua eguneratu duzu",
+ "{actor} shared address book {addressbook} with you" : "{actor}-(e)k {addressbook} helbide-liburua partekatu du",
+ "You shared address book {addressbook} with {user}" : "{addressbook} helbide-liburua partekatu duzu",
+ "{actor} shared address book {addressbook} with {user}" : "{actor}-(e)k {addressbook} helbide-liburua partekatu du {user}-(r)ekin",
"{actor} unshared address book {addressbook} from you" : "{actor}-(e)k {addressbook} helbide-liburua zurekin partekatzeari utzi dio",
"You unshared address book {addressbook} from {user}" : "{addressbook} helbide-liburua {user}-(r)ekin partekatzeari utzi diozu",
"{actor} unshared address book {addressbook} from {user}" : "{actor}-(e)k {addressbook} helbide-liburua {user}-(r)ekin partekatzeari utzi dio",
- "{actor} unshared address book {addressbook} from themselves" : "{actor}-(e)k {addressbook} helbide-liburua beraiekin partekatzeari utzi dio",
- "You shared address book {addressbook} with group {group}" : "Partekatu duzu {addressbook} helbide-liburua {group} taldearekin",
- "{actor} shared address book {addressbook} with group {group}" : "{actor}-(e)k partekatu du {addressbook} helbide-liburua {group} taldearekin",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor}-(e)k {addressbook} helbide-liburuaren partekatzea eten du beretzat.",
+ "You shared address book {addressbook} with group {group}" : "{addressbook} helbide-liburua partekatu duzu {group} taldearekin",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor}-(e)k {addressbook} helbide-liburua partekatu du {group} taldearekin",
"You unshared address book {addressbook} from group {group}" : "{addressbook} helbide-liburua {group} taldearekin partekatzeari utzi diozu",
"{actor} unshared address book {addressbook} from group {group}" : "{actor}-(e)k {addressbook} helbide-liburua {group} taldearekin partekatzeari utzi dio",
- "{actor} created contact {card} in address book {addressbook}" : "{actor}-(e)k sortu du {card} kontaktua {addressbook} helbide-liburuan",
- "You created contact {card} in address book {addressbook}" : "Sortu duzu {card} kontaktua {addressbook} helbide-liburuan",
- "{actor} deleted contact {card} from address book {addressbook}" : "{actor}-(e)k ezabatu du {card} kontaktua {addressbook} helbide-liburutik",
- "You deleted contact {card} from address book {addressbook}" : "Ezabatu duzu {card} kontaktua {addressbook} helbide-liburutik",
- "{actor} updated contact {card} in address book {addressbook}" : "{actor}-(e)k eguneratu du {card} kontaktua {addressbook} helbide-liburuan",
- "You updated contact {card} in address book {addressbook}" : "Eguneratu duzu {card} kontaktua {addressbook} helbide-liburuan",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor}-(e)k {card} kontaktua sortu du {addressbook} helbide-liburuan",
+ "You created contact {card} in address book {addressbook}" : "{card} kontaktua sortu duzu {addressbook} helbide-liburuan",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor}-(e)k {card} kontaktua ezabatu du {addressbook} helbide-liburutik",
+ "You deleted contact {card} from address book {addressbook}" : "{card} kontaktua ezabatu duzu {addressbook} helbide-liburutik",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor}-(e)k {card} kontaktua eguneratu du {addressbook} helbide-liburuan",
+ "You updated contact {card} in address book {addressbook}" : "{card} kontaktua eguneratu duzu {addressbook} helbide-liburuan",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>kontaktu</strong> edo <strong>helbide-liburu</strong>bat aldatu da",
+ "Accounts" : "Kontuak",
+ "System address book which holds all accounts" : "Kontu guztiak dituen sistemaren helbide-liburua",
+ "File is not updatable: %1$s" : "Fitxategia ez da eguneragarria: %1$s",
+ "Failed to get storage for file" : "Fitxategirako biltegia lortzeak huts egin du",
+ "Could not write to final file, canceled by hook" : "Ezin izan da azken fitxategian idatzi, kakoak bertan behera utzi du",
+ "Could not write file contents" : "Ezin izan dira fitxategiaren edukiak idatzi",
"_%n byte_::_%n bytes_" : ["Byte %n","%n byte"],
- "Could not open file" : "Ezin izan da fitxategia ireki",
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Errore bat gertatu da fitxategia helburuko kokapenera kopiatzean (kopiatua: %1$s, espero zen fitxategi-tamaina: %2$s)",
+ "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." : "%1$sfitxategi-tamaina espero zen baina irakurria (Nextcloud bezerotik) eta idatzia (Nextcloud biltegian) %2$sizan da. Bidaltzailearen aldean sareko arazo bat izan daiteke edo zerbitzariaren arazo bat biltegian idazteko.",
+ "Could not rename part file to final file, canceled by hook" : "Ezin izan da zati-fitxategiaren izena aldatu azken fitxategira, kakoak bertan behera utzi du",
+ "Could not rename part file to final file" : "Ezin izan da zati-fitxategia azken fitxategira aldatu",
+ "Failed to check file size: %1$s" : "Ezin izan da egiaztatu fitxategiaren tamaina:%1$s",
+ "Could not open file: %1$s, file does seem to exist" : "Ezin da fitxategi hau ireki: %1$s, badirudi fitxategia existitzen dela",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Ezin da fitxategi hau ireki: %1$s, badirudi fitxategia ez dela existitzen",
+ "Encryption not ready: %1$s" : "Enkriptatzea ez dago prest:%1$s",
+ "Failed to open file: %1$s" : "Ezin izan da fitxategia ireki:%1$s",
+ "Failed to unlink: %1$s" : "Ezin izan da deskonektatu:%1$s",
+ "Failed to write file contents: %1$s" : "Ezin izan dira fitxategiaren edukiak idatzi:%1$s",
+ "File not found: %1$s" : "Ez da fitxategirik aurkitu:%1$s",
+ "Invalid target path" : "Helburu bide-izen baliogabea",
"System is in maintenance mode." : "Sistema mantentze moduan dago.",
"Upgrade needed" : "Bertsio-berritzea beharrezkoa",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Zure %s HTTPS erabiltzeko konfiguratu behar da CalDAV eta CardDAV erabiltzeko iOS eta macOSrekin.",
"Configures a CalDAV account" : "CalDAV kontu bat konfiguratzen du",
"Configures a CardDAV account" : "CardDAV kontu bat konfiguratzen du",
"Events" : "Gertaerak",
- "Tasks" : "Zereginak",
"Untitled task" : "Izenik gabeko zeregina",
"Completed on %s" : "%s-an osatua",
- "Due on %s by %s" : "%s-(e)tik %s-(e)an epemuga",
+ "Due on %s by %s" : "%s-(e)an epemuga %s-(e)k",
"Due on %s" : "%s-(e)an epemuga",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Ongi etorri Nextcloud Egutegira!\n\nHau gertaera erakusgarria da - aztertu plangintzaren malgutasuna Nextcloud Egutegiarekin nahi dituzun edizioak eginez!\n\nNextcloud Egutegia aukerarekin, hau egin dezakezu:\n- Sortu, editatu eta kudeatu gertaerak esfortzurik gabe.\n- Egutegi ugari sortu eta taldekideekin, lagunekin edo familiarekin partekatu.\n- Egiaztatu libre egotea eta bistaratu zure laneko orduak beste batzuei.\n- Aplikazio eta gailuekin arazorik gabe integratzea CalDAV bidez.\n- Zure esperientzia pertsonalizatu: gertaera errepikariak programatu, jakinarazpenak doitu eta bestelako ezarpenak.",
+ "Example event - open me!" : "Gertaera adibidea - ireki nazazu!",
+ "System Address Book" : "Sistemaren helbide-liburua",
+ "The system address book contains contact information for all users in your instance." : "Sistemaren helbide-liburuak zure instantziako erabiltzaile guztien kontaktu-informazioa dauka.",
+ "Enable System Address Book" : "Gaitu sistemaren helbide-liburua",
+ "DAV system address book" : "DAV sistemaren helbide-liburua",
+ "No outstanding DAV system address book sync." : "Ez dago DAV sistema helbide-liburuaren sinkronizazio arrarorik.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV sistemaren helbide-liburuaren sinkronizazioa oraindik ez da martxan jarri zure instantziak 1000 erabiltzaile baino gehiago dituelako edo akats bat gertatu delako. Mesedez, exekutatu eskuz \"occ dav:sync-system-addressbook\" deituz.",
+ "WebDAV endpoint" : "WebDAV amaiera-puntua",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Ezin izan da egiaztatu zure zerbitzaria WebDAV bidezko fitxategi-sinkronizazioa onartzeko ondo konfiguratuta badagoen. Mesedez, egiaztatu ezazu eskuz.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Zure web zerbitzaria ez dago behar bezala konfiguratuta fitxategien sinkronizazioa baimentzeko, WebDAV interfazea puskatuta dagoela dirudi.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Zure web zerbitzaria WebDAV bidezko fitxategien sinkronizazioa onartzeko ondo konfiguratuta dago.",
+ "Migrated calendar (%1$s)" : "Migratutako egutegia (%1$s)",
+ "Calendars including events, details and attendees" : "Egutegiak, gertaerak, xehetasunak eta parte-hartzaileak barne",
"Contacts and groups" : "Kontaktuak eta taldeak",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV amaiera-puntua",
- "Availability" : "Eskuragarritasuna",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Zure lan orduak konfiguratzen badituzu, beste erabiltzaileak bulegotik kanpo zaudela ikusiko dute bilera bat erreserbatzen dutenean.",
+ "Absence saved" : "Absentzia gordeta",
+ "Failed to save your absence settings" : "Ezin izan dira zure absentzia ezarpenak gorde",
+ "Absence cleared" : "Abszentzia ezabatuta",
+ "Failed to clear your absence settings" : "Ezin izan dira zure absentzia ezarpenak garbitu",
+ "First day" : "Lehen eguna",
+ "Last day (inclusive)" : "Azken eguna (barne)",
+ "Out of office replacement (optional)" : "Bulegotik kanpo ordezkatzea (aukerakoa)",
+ "Name of the replacement" : "Ordezkoaren izena",
+ "No results." : "Ez dago emaitzarik.",
+ "Start typing." : "Hasi idazten.",
+ "Short absence status" : "Absentzia-egoera laburra",
+ "Long absence Message" : "Absentzia-mezu luzea",
+ "Save" : "Gorde",
+ "Disable absence" : "Desgaitu absentzia",
+ "Failed to load availability" : "Ezin izan da eskuragarritasuna kargatu",
+ "Saved availability" : "Eskuragarritasuna gorde da",
+ "Failed to save availability" : "Ezin izan da eskuragarritasuna gorde",
"Time zone:" : "Ordu-zona:",
"to" : "honi",
"Delete slot" : "Ezabatu tartea",
"No working hours set" : "Ez dira laneko orduak ezarri",
"Add slot" : "Gehitu tartea",
- "Monday" : "Astelehena",
- "Tuesday" : "Asteartea",
- "Wednesday" : "Asteazkena",
- "Thursday" : "Osteguna",
- "Friday" : "Ostirala",
- "Saturday" : "Larunbata",
- "Sunday" : "Igandea",
- "Save" : "Gorde",
+ "Weekdays" : "Astegunak",
+ "Pick a start time for {dayName}" : "Hautatu hasiera ordu bat {dayName}(e)rako",
+ "Pick a end time for {dayName}" : "Hautatu bukaera ordu bat {dayName}(e)rako",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Eskuragarri ez egotean, ezarri automatikoki erabiltzailearen egoera \"Ez molestatu\" moduan jakinarazpen guztiak isilarazteko.",
+ "Cancel" : "Utzi",
+ "Import" : "Inportatu",
+ "Error while saving settings" : "Errorea ezarpenak gordetzean",
+ "Contact reset successfully" : "Kontaktua behar bezala berrezarri da",
+ "Error while resetting contact" : "Errorea kontaktua berrezartzean",
+ "Contact imported successfully" : "Kontaktua behar bezala inportatu da",
+ "Error while importing contact" : "Errorea kontaktua inportatzean",
+ "Import contact" : "Inportatu kontaktua",
+ "Reset to default" : "Berezarri balio lehenetsira",
+ "Import contacts" : "Inportatu kontaktuak",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : ".vcf fitxategi berri bat inportatzean, lehendik dagoen kontaktu lehenetsia ezabatu eta berriarekin ordeztuko da. Jarraitu nahi duzu?",
+ "Failed to save example event creation setting" : "Adibide gertaeraren sortze ezarpenak gordetzeak huts egin du",
+ "Failed to upload the example event" : "Adibide gertaera igotzeak huts egin du",
+ "Custom example event was saved successfully" : "Adibide gertaera pertsonalizatua behar bezala gorde da",
+ "Failed to delete the custom example event" : "Adibide gertaera pertsonalizatua ezabatzeak huts egin du",
+ "Custom example event was deleted successfully" : "Adibide gertaera pertsonalizatua behar bezala ezabatu da.",
+ "Import calendar event" : "Inportatu egutegiko gertaera",
+ "Uploading a new event will overwrite the existing one." : "Gertaera berri bat igotzeak dagoena gainidatz dezake",
+ "Upload event" : "Igo gertaera",
+ "Availability" : "Eskuragarritasuna",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Zure lan orduak konfiguratzen badituzu, beste pertsonek bulegotik kanpo zaudela ikusiko dute bilera bat erreserbatzen dutenean.",
+ "Absence" : "Absentzia",
+ "Configure your next absence period." : "Konfiguratu zure hurrengo absentzia aldia.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instalatu ezazu {calendarappstoreopen}Egutegi aplikazioa{linkclose} ere, edo {calendardocopen}konektatu zure ordenagailua eta mugikorra sinkronizatzeko ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Egiaztatu {emailopen}posta elektroniko zerbitzaria{linkclose} ondo konfiguratuta dagoela.",
"Calendar server" : "Egutegi-zerbitzaria",
- "Send invitations to attendees" : "Gonbidatutakoei gonbidapenak bidali",
+ "Send invitations to attendees" : "Parte-hartzaileei gonbidapenak bidali",
"Automatically generate a birthday calendar" : "Automatikoki sortu urtebetetzeen egutegia",
"Birthday calendars will be generated by a background job." : "Urtebetetze egutegiak atzealdeko lan batek sortuko ditu.",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Beraz ez dira gaitu ostean agertuko baina denbora pasa ahala agertuko dira.",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "Beraz ez dira gaitu bezain laster prest egongo, baina denbora bat pasatzean agertuko dira.",
"Send notifications for events" : "Bidali jakinarazpenak gertaerentzako",
"Notifications are sent via background jobs, so these must occur often enough." : "Jakinarazpenak atzealdeko lanen bidez bidaliko dira, beraz sarri gertatu behar dira.",
+ "Send reminder notifications to calendar sharees as well" : "Bidali gogorarazpen jakinarazpenak egutegi partekatzea dutenei ere",
+ "Reminders are always sent to organizers and attendees." : "Gogorarazpenak beti bidaltzen zaizkie antolatzaileei eta parte-hartzaileei.",
"Enable notifications for events via push" : "Gaitu push bidezko jakinarazpenak gertaerentzat",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instalatu ezazu {calendarappstoreopen}Egutegi aplikazioa{linkclose} ere, edo {calendardocopen}konektatu zure ordenagailua eta mugikorra sinkronizatzeko ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Mesedez egiaztatu {emailopen}posta elektroniko zerbitzaria{linkclose} ondo konfiguratuta dagoela.",
+ "Example content" : "Adibideko edukia",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Adibideko edukiak Nextcloud-en ezaugarriak erakusteko balio du. Eduki lehenetsia Nextcloud-ekin bidaltzen da, eta eduki pertsonalizatuarekin ordezka daiteke.",
"There was an error updating your attendance status." : "Errore bat gertatu da zure parte-hartze egoera eguneratzerakoan.",
"Please contact the organizer directly." : "Mesedez jarri harremanetan antolatzailearekin zuzenean.",
"Are you accepting the invitation?" : "Gonbidapena onartzen duzu?",
"Tentative" : "Behin behinekoa",
- "Number of guests" : "Gonbidatu kopurua",
- "Comment" : "Iruzkindu",
- "Your attendance was updated successfully." : "Zure parte-hartzea ondo eguneratu da.",
- "Calendar and tasks" : "Egutegia eta atazak"
+ "Your attendance was updated successfully." : "Zure parte-hartzea ondo eguneratu da."
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/dav/l10n/fa.js b/apps/dav/l10n/fa.js
new file mode 100644
index 00000000000..4ce74a149f2
--- /dev/null
+++ b/apps/dav/l10n/fa.js
@@ -0,0 +1,326 @@
+OC.L10N.register(
+ "dav",
+ {
+ "Calendar" : "تقویم",
+ "Tasks" : "وظایف",
+ "Personal" : "شخصی",
+ "{actor} created calendar {calendar}" : "{actor} تقویم {calendar} را ایجاد کرد",
+ "You created calendar {calendar}" : "شما تقویم {calendar} را ایجاد کردید",
+ "{actor} deleted calendar {calendar}" : "{actor} تقویم {calendar} را حذف کرد",
+ "You deleted calendar {calendar}" : "شما تقویم {calendar} را حذف کردید",
+ "{actor} updated calendar {calendar}" : "{actor} تقویم {calendar} را به‌روزرسانی کرد",
+ "You updated calendar {calendar}" : "شما تقویم {calendar} را به‌روزرسانی کردید",
+ "{actor} restored calendar {calendar}" : "{actor} تقویم {calendar} را بازیابی کرد",
+ "You restored calendar {calendar}" : "شما تقویم {calendar} را بازیابی کردید",
+ "You shared calendar {calendar} as public link" : "شما تقویم {calendar} را به‌عنوان پیوند عمومی به اشتراک گذاشتید",
+ "You removed public link for calendar {calendar}" : "شما پیوند عمومی تقویم {calendar} را حذف کردید",
+ "{actor} shared calendar {calendar} with you" : "{actor} تقویم {calendar} را با شما به اشتراک گذاشت",
+ "You shared calendar {calendar} with {user}" : "شما تقویم {calendar} را با {user} به اشتراک گذاشتید",
+ "{actor} shared calendar {calendar} with {user}" : "{actor} تقویم {calendar} را با {user} به اشتراک گذاشت",
+ "{actor} unshared calendar {calendar} from you" : "{actor} اشتراک‌گذاری تقویم {calendar} را از شما لغو کرد",
+ "You unshared calendar {calendar} from {user}" : "شما اشتراک‌گذاری تقویم {calendar} را از {user} لغو کردید",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor} اشتراک‌گذاری تقویم {calendar} را از {user} لغو کرد",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} اشتراک‌گذاری تقویم {calendar} را از خود لغو کرد",
+ "You shared calendar {calendar} with group {group}" : "شما تقویم {calendar} را با گروه {group} به اشتراک گذاشتید",
+ "{actor} shared calendar {calendar} with group {group}" : "{actor} تقویم {calendar} را با گروه {group} به اشتراک گذاشت",
+ "You unshared calendar {calendar} from group {group}" : "شما اشتراک‌گذاری تقویم {calendar} را از گروه {group} لغو کردید",
+ "{actor} unshared calendar {calendar} from group {group}" : "{actor} اشتراک‌گذاری تقویم {calendar} را از گروه {group} لغو کرد",
+ "Untitled event" : "رویداد بدون عنوان",
+ "{actor} created event {event} in calendar {calendar}" : "{actor} رویداد {event} را در تقویم {calendar} ایجاد کرد",
+ "You created event {event} in calendar {calendar}" : "شما رویداد {event} را در تقویم {calendar} ایجاد کردید",
+ "{actor} deleted event {event} from calendar {calendar}" : "{actor} رویداد {event} را از تقویم {calendar} حذف کرد",
+ "You deleted event {event} from calendar {calendar}" : "شما رویداد {event} را از تقویم {calendar} حذف کردید",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} رویداد {event} را در تقویم {calendar} به‌روزرسانی کرد",
+ "You updated event {event} in calendar {calendar}" : "شما رویداد {event} را در تقویم {calendar} به‌روزرسانی کردید",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} رویداد {event} را از تقویم {sourceCalendar} به تقویم {targetCalendar} منتقل کرد",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "شما رویداد {event} را از تقویم {sourceCalendar} به تقویم {targetCalendar} منتقل کردید",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} رویداد {event} را از تقویم {calendar} بازیابی کرد",
+ "You restored event {event} of calendar {calendar}" : "شما رویداد {event} را از تقویم {calendar} بازیابی کردید",
+ "Busy" : "مشغول",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} کار {todo} را در لیست {calendar} ایجاد کرد",
+ "You created to-do {todo} in list {calendar}" : "شما کار {todo} را در لیست {calendar} ایجاد کردید",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} کار {todo} را از لیست {calendar} حذف کرد",
+ "You deleted to-do {todo} from list {calendar}" : "شما کار {todo} را از لیست {calendar} حذف کردید",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} کار {todo} را در لیست {calendar} به‌روزرسانی کرد",
+ "You updated to-do {todo} in list {calendar}" : "شما کار {todo} را در لیست {calendar} به‌روزرسانی کردید",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} کار {todo} را در لیست {calendar} حل کرد",
+ "You solved to-do {todo} in list {calendar}" : "شما کار {todo} را در لیست {calendar} حل کردید",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} کار {todo} را در لیست {calendar} بازگشایی کرد",
+ "You reopened to-do {todo} in list {calendar}" : "شما کار {todo} را در لیست {calendar} بازگشایی کردید",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} کار {todo} را از لیست {sourceCalendar} به لیست {targetCalendar} منتقل کرد",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "شما کار {todo} را از لیست {sourceCalendar} به لیست {targetCalendar} منتقل کردید",
+ "Calendar, contacts and tasks" : "تقویم، مخاطبین و وظایف",
+ "A <strong>calendar</strong> was modified" : "یک <strong>تقویم</strong> تغییر کرد",
+ "A calendar <strong>event</strong> was modified" : "یک <strong>رویداد</strong> تقویم تغییر کرد",
+ "A calendar <strong>to-do</strong> was modified" : "یک <strong>کار</strong> تقویم تغییر کرد",
+ "Contact birthdays" : "تولد مخاطبین",
+ "Death of %s" : "فوت %s",
+ "Untitled calendar" : "تقویم بدون عنوان",
+ "Calendar:" : "تقویم:",
+ "Date:" : "تاریخ:",
+ "Where:" : "مکان:",
+ "Description:" : "توضیحات:",
+ "_%n year_::_%n years_" : ["%n سال","%n سال"],
+ "_%n month_::_%n months_" : ["%n ماه","%n ماه"],
+ "_%n day_::_%n days_" : ["%n روز","%n روز"],
+ "_%n hour_::_%n hours_" : ["%n ساعت","%n ساعت"],
+ "_%n minute_::_%n minutes_" : ["%n دقیقه","%n دقیقه"],
+ "%s (in %s)" : "%s (در %s)",
+ "%s (%s ago)" : "%s (%s پیش)",
+ "Calendar: %s" : "تقویم: %s",
+ "Date: %s" : "تاریخ: %s",
+ "Description: %s" : "توضیحات: %s",
+ "Where: %s" : "مکان: %s",
+ "%1$s via %2$s" : "%1$s از طریق %2$s",
+ "In the past on %1$s for the entire day" : "در گذشته در %1$s برای کل روز",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["در یک دقیقه در %1$s برای کل روز","در %n دقیقه در %1$s برای کل روز"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["در یک ساعت در %1$s برای کل روز","در %n ساعت در %1$s برای کل روز"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["در یک روز در %1$s برای کل روز","در %n روز در %1$s برای کل روز"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["در یک هفته در %1$s برای کل روز","در %n هفته در %1$s برای کل روز"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["در یک ماه در %1$s برای کل روز","در %n ماه در %1$s برای کل روز"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["در یک سال در %1$s برای کل روز","در %n سال در %1$s برای کل روز"],
+ "In the past on %1$s between %2$s - %3$s" : "در گذشته در %1$s بین %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["در یک دقیقه در %1$s بین %2$s - %3$s","در %n دقیقه در %1$s بین %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["در یک ساعت در %1$s بین %2$s - %3$s","در %n ساعت در %1$s بین %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["در یک روز در %1$s بین %2$s - %3$s","در %n روز در %1$s بین %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["در یک هفته در %1$s بین %2$s - %3$s","در %n هفته در %1$s بین %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["در یک ماه در %1$s بین %2$s - %3$s","در %n ماه در %1$s بین %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["در یک سال در %1$s بین %2$s - %3$s","در %n سال در %1$s بین %2$s - %3$s"],
+ "Could not generate when statement" : "امکان ایجاد عبارت زمان وجود ندارد",
+ "Every Day for the entire day" : "هر روز برای کل روز",
+ "Every Day for the entire day until %1$s" : "هر روز برای کل روز تا %1$s",
+ "Every Day between %1$s - %2$s" : "هر روز بین %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "هر روز بین %1$s - %2$s تا %3$s",
+ "Every %1$d Days for the entire day" : "هر %1$d روز برای کل روز",
+ "Every %1$d Days for the entire day until %2$s" : "هر %1$d روز برای کل روز تا %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "هر %1$d روز بین %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "هر %1$d روز بین %2$s - %3$s تا %4$s",
+ "Could not generate event recurrence statement" : "امکان ایجاد عبارت تکرار رویداد وجود ندارد",
+ "Every Week on %1$s for the entire day" : "هر هفته در %1$s برای کل روز",
+ "Every Week on %1$s for the entire day until %2$s" : "هر هفته در %1$s برای کل روز تا %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "هر هفته در %1$s بین %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "هر هفته در %1$s بین %2$s - %3$s تا %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "هر %1$d هفته در %2$s برای کل روز",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "هر %1$d هفته در %2$s برای کل روز تا %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "هر %1$d هفته در %2$s بین %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "هر %1$d هفته در %2$s بین %3$s - %4$s تا %5$s",
+ "Every Month on the %1$s for the entire day" : "هر ماه در %1$s برای کل روز",
+ "Every Month on the %1$s for the entire day until %2$s" : "هر ماه در %1$s برای کل روز تا %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "هر ماه در %1$s بین %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "هر ماه در %1$s بین %2$s - %3$s تا %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "هر %1$d ماه در %2$s برای کل روز",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "هر %1$d ماه در %2$s برای کل روز تا %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "هر %1$d ماه در %2$s بین %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "هر %1$d ماه در %2$s بین %3$s - %4$s تا %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "هر سال در %1$s در %2$s برای کل روز",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "هر سال در %1$s در %2$s برای کل روز تا %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "هر سال در %1$s در %2$s بین %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "هر سال در %1$s در %2$s بین %3$s - %4$s تا %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "هر %1$d سال در %2$s در %3$s برای کل روز",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "هر %1$d سال در %2$s در %3$s برای کل روز تا %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "هر %1$d سال در %2$s در %3$s بین %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "هر %1$d سال در %2$s در %3$s بین %4$s - %5$s تا %6$s",
+ "On specific dates for the entire day until %1$s" : "در تاریخ‌های مشخص برای کل روز تا %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "در تاریخ‌های مشخص بین %1$s - %2$s تا %3$s",
+ "In the past on %1$s" : "در گذشته در %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["در یک دقیقه در %1$s","در %n دقیقه در %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["در یک ساعت در %1$s","در %n ساعت در %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["در یک روز در %1$s","در %n روز در %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["در یک هفته در %1$s","در %n هفته در %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["در یک ماه در %1$s","در %n ماه در %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["در یک سال در %1$s","در %n سال در %1$s"],
+ "In the past on %1$s then on %2$s" : "در گذشته در %1$s سپس در %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["در یک دقیقه در %1$s سپس در %2$s","در %n دقیقه در %1$s سپس در %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["در یک ساعت در %1$s سپس در %2$s","در %n ساعت در %1$s سپس در %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["در یک روز در %1$s سپس در %2$s","در %n روز در %1$s سپس در %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["در یک هفته در %1$s سپس در %2$s","در %n هفته در %1$s سپس در %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["در یک ماه در %1$s سپس در %2$s","در %n ماه در %1$s سپس در %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["در یک سال در %1$s سپس در %2$s","در %n سال در %1$s سپس در %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "در گذشته در %1$s سپس در %2$s و %3$s",
+ "_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_" : ["در یک دقیقه در %1$s سپس در %2$s و %3$s","در %n دقیقه در %1$s سپس در %2$s و %3$s"],
+ "_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_" : ["در یک ساعت در %1$s سپس در %2$s و %3$s","در %n ساعت در %1$s سپس در %2$s و %3$s"],
+ "_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_" : ["در یک روز در %1$s سپس در %2$s و %3$s","در %n روز در %1$s سپس در %2$s و %3$s"],
+ "_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_" : ["در یک هفته در %1$s سپس در %2$s و %3$s","در %n هفته در %1$s سپس در %2$s و %3$s"],
+ "_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_" : ["در یک ماه در %1$s سپس در %2$s و %3$s","در %n ماه در %1$s سپس در %2$s و %3$s"],
+ "_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_" : ["در یک سال در %1$s سپس در %2$s و %3$s","در %n سال در %1$s سپس در %2$s و %3$s"],
+ "Could not generate next recurrence statement" : "امکان ایجاد عبارت تکرار بعدی وجود ندارد",
+ "Cancelled: %1$s" : "لغو شد: %1$s",
+ "\"%1$s\" has been canceled" : "\"%1$s\" لغو شده است",
+ "Re: %1$s" : "پاسخ: %1$s",
+ "%1$s has accepted your invitation" : "%1$s دعوت شما را پذیرفته است",
+ "%1$s has tentatively accepted your invitation" : "%1$s دعوت شما را به‌طور آزمایشی پذیرفته است",
+ "%1$s has declined your invitation" : "%1$s دعوت شما را رد کرده است",
+ "%1$s has responded to your invitation" : "%1$s به دعوت شما پاسخ داده است",
+ "Invitation updated: %1$s" : "دعوت به‌روزرسانی شد: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s رویداد \"%2$s\" را به‌روزرسانی کرد",
+ "Invitation: %1$s" : "دعوت: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s مایل است شما را به \"%2$s\" دعوت کند",
+ "Organizer:" : "برگزارکننده:",
+ "Attendees:" : "شرکت‌کنندگان:",
+ "Title:" : "عنوان:",
+ "When:" : "چه زمانی:",
+ "Location:" : "مکان:",
+ "Link:" : "پیوند:",
+ "Occurring:" : "در حال وقوع:",
+ "Accept" : "پذیرفتن",
+ "Decline" : "رد کردن",
+ "More options …" : "گزینه‌های بیشتر…",
+ "More options at %s" : "گزینه‌های بیشتر در %s",
+ "Monday" : "دوشنبه",
+ "Tuesday" : "سه‌شنبه",
+ "Wednesday" : "چهارشنبه",
+ "Thursday" : "پنجشنبه",
+ "Friday" : "جمعه",
+ "Saturday" : "شنبه",
+ "Sunday" : "یکشنبه",
+ "January" : "ژانویه",
+ "February" : "فوریه",
+ "March" : "مارس",
+ "April" : "آوریل",
+ "May" : "مه",
+ "June" : "ژوئن",
+ "July" : "ژوئیه",
+ "August" : "اوت",
+ "September" : "سپتامبر",
+ "October" : "اکتبر",
+ "November" : "نوامبر",
+ "December" : "دسامبر",
+ "First" : "اول",
+ "Second" : "دوم",
+ "Third" : "سوم",
+ "Fourth" : "چهارم",
+ "Fifth" : "پنجم",
+ "Last" : "آخر",
+ "Second Last" : "دومی از آخر",
+ "Third Last" : "سومی از آخر",
+ "Fourth Last" : "چهارمی از آخر",
+ "Fifth Last" : "پنجمی از آخر",
+ "Contacts" : "مخاطبین",
+ "{actor} created address book {addressbook}" : "{actor} دفترچه آدرس {addressbook} را ایجاد کرد",
+ "You created address book {addressbook}" : "شما دفترچه آدرس {addressbook} را ایجاد کردید",
+ "{actor} deleted address book {addressbook}" : "{actor} دفترچه آدرس {addressbook} را حذف کرد",
+ "You deleted address book {addressbook}" : "شما دفترچه آدرس {addressbook} را حذف کردید",
+ "{actor} updated address book {addressbook}" : "{actor} دفترچه آدرس {addressbook} را به‌روزرسانی کرد",
+ "You updated address book {addressbook}" : "شما دفترچه آدرس {addressbook} را به‌روزرسانی کردید",
+ "{actor} shared address book {addressbook} with you" : "{actor} دفترچه آدرس {addressbook} را با شما به اشتراک گذاشت",
+ "You shared address book {addressbook} with {user}" : "شما دفترچه آدرس {addressbook} را با {user} به اشتراک گذاشتید",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} دفترچه آدرس {addressbook} را با {user} به اشتراک گذاشت",
+ "{actor} unshared address book {addressbook} from you" : "{actor} اشتراک‌گذاری دفترچه آدرس {addressbook} را از شما لغو کرد",
+ "You unshared address book {addressbook} from {user}" : "شما اشتراک‌گذاری دفترچه آدرس {addressbook} را از {user} لغو کردید",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} اشتراک‌گذاری دفترچه آدرس {addressbook} را از {user} لغو کرد",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} اشتراک‌گذاری دفترچه آدرس {addressbook} را از خود لغو کرد",
+ "You shared address book {addressbook} with group {group}" : "شما دفترچه آدرس {addressbook} را با گروه {group} به اشتراک گذاشتید",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} دفترچه آدرس {addressbook} را با گروه {group} به اشتراک گذاشت",
+ "You unshared address book {addressbook} from group {group}" : "شما اشتراک‌گذاری دفترچه آدرس {addressbook} را از گروه {group} لغو کردید",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} اشتراک‌گذاری دفترچه آدرس {addressbook} را از گروه {group} لغو کرد",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} مخاطب {card} را در دفترچه آدرس {addressbook} ایجاد کرد",
+ "You created contact {card} in address book {addressbook}" : "شما مخاطب {card} را در دفترچه آدرس {addressbook} ایجاد کردید",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} مخاطب {card} را از دفترچه آدرس {addressbook} حذف کرد",
+ "You deleted contact {card} from address book {addressbook}" : "شما مخاطب {card} را از دفترچه آدرس {addressbook} حذف کردید",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} مخاطب {card} را در دفترچه آدرس {addressbook} به‌روزرسانی کرد",
+ "You updated contact {card} in address book {addressbook}" : "شما مخاطب {card} را در دفترچه آدرس {addressbook} به‌روزرسانی کردید",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "یک <strong>مخاطب</strong> یا <strong>دفترچه آدرس</strong> تغییر کرد",
+ "Accounts" : "حساب‌ها",
+ "System address book which holds all accounts" : "دفترچه آدرس سیستمی که شامل تمام حساب‌ها است",
+ "File is not updatable: %1$s" : "فایل قابل به‌روزرسانی نیست: %1$s",
+ "Failed to get storage for file" : "دریافت فضای ذخیره‌سازی برای فایل با شکست مواجه شد",
+ "Could not write to final file, canceled by hook" : "نوشتن در فایل نهایی امکان‌پذیر نبود، توسط هوک لغو شد",
+ "Could not write file contents" : "امکان نوشتن محتویات فایل وجود ندارد",
+ "_%n byte_::_%n bytes_" : ["%n بایت","%n بایت"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "خطا هنگام کپی فایل به مکان مقصد (کپی شده: %1$s، حجم فایل مورد انتظار: %2$s)",
+ "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." : "حجم فایل مورد انتظار %1$s بود اما %2$s خوانده (از کلاینت Nextcloud) و نوشته (در فضای ذخیره‌سازی Nextcloud) شد. این می‌تواند ناشی از مشکل شبکه در سمت ارسال یا مشکل نوشتن در فضای ذخیره‌سازی در سمت سرور باشد.",
+ "Could not rename part file to final file, canceled by hook" : "امکان تغییر نام فایل جزئی به فایل نهایی وجود نداشت، توسط هوک لغو شد",
+ "Could not rename part file to final file" : "امکان تغییر نام فایل جزئی به فایل نهایی وجود ندارد",
+ "Failed to check file size: %1$s" : "بررسی حجم فایل با شکست مواجه شد: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "امکان باز کردن فایل وجود ندارد: %1$s، به نظر می‌رسد فایل وجود دارد",
+ "Could not open file: %1$s, file doesn't seem to exist" : "امکان باز کردن فایل وجود ندارد: %1$s، به نظر می‌رسد فایل وجود ندارد",
+ "Encryption not ready: %1$s" : "رمزگذاری آماده نیست: %1$s",
+ "Failed to open file: %1$s" : "باز کردن فایل با شکست مواجه شد: %1$s",
+ "Failed to unlink: %1$s" : "حذف پیوند با شکست مواجه شد: %1$s",
+ "Failed to write file contents: %1$s" : "نوشتن محتویات فایل با شکست مواجه شد: %1$s",
+ "File not found: %1$s" : "فایل یافت نشد: %1$s",
+ "Invalid target path" : "مسیر مقصد نامعتبر است",
+ "System is in maintenance mode." : "سیستم در حالت نگهداری است.",
+ "Upgrade needed" : "نیاز به ارتقا",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "برای استفاده از CalDAV و CardDAV با iOS/macOS، %s شما باید برای استفاده از HTTPS پیکربندی شود.",
+ "Configures a CalDAV account" : "یک حساب CalDAV را پیکربندی می‌کند",
+ "Configures a CardDAV account" : "یک حساب CardDAV را پیکربندی می‌کند",
+ "Events" : "رویدادها",
+ "Untitled task" : "کار بدون عنوان",
+ "Completed on %s" : "تکمیل شده در %s",
+ "Due on %s by %s" : "موعد در %s توسط %s",
+ "Due on %s" : "موعد در %s",
+ "System Address Book" : "دفترچه آدرس سیستم",
+ "The system address book contains contact information for all users in your instance." : "دفترچه آدرس سیستم شامل اطلاعات تماس برای همه کاربران در نمونه شما است.",
+ "Enable System Address Book" : "فعال کردن دفترچه آدرس سیستم",
+ "DAV system address book" : "دفترچه آدرس سیستم DAV",
+ "No outstanding DAV system address book sync." : "هیچ همگام‌سازی دفترچه آدرس سیستم DAV در انتظار نیست.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "همگام‌سازی دفترچه آدرس سیستم DAV هنوز اجرا نشده است زیرا نمونه شما بیش از ۱۰۰۰ کاربر دارد یا خطایی رخ داده است. لطفاً آن را به‌صورت دستی با فراخوانی \"occ dav:sync-system-addressbook\" اجرا کنید.",
+ "WebDAV endpoint" : "نقطه پایانی WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "امکان بررسی اینکه سرور وب شما به درستی برای همگام‌سازی فایل از طریق WebDAV تنظیم شده است، وجود ندارد. لطفاً به صورت دستی بررسی کنید.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "سرور وب شما هنوز به درستی برای همگام‌سازی فایل تنظیم نشده است، زیرا به نظر می‌رسد رابط WebDAV خراب است.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "سرور وب شما به درستی برای همگام‌سازی فایل از طریق WebDAV تنظیم شده است.",
+ "Migrated calendar (%1$s)" : "تقویم منتقل شده (%1$s)",
+ "Calendars including events, details and attendees" : "تقویم‌ها شامل رویدادها، جزئیات و شرکت‌کنندگان",
+ "Contacts and groups" : "مخاطبین و گروه‌ها",
+ "WebDAV" : "وب‌دَو",
+ "Absence saved" : "غیبت ذخیره شد",
+ "Failed to save your absence settings" : "ذخیره تنظیمات غیبت شما با شکست مواجه شد",
+ "Absence cleared" : "غیبت پاک شد",
+ "Failed to clear your absence settings" : "پاک کردن تنظیمات غیبت شما با شکست مواجه شد",
+ "First day" : "روز اول",
+ "Last day (inclusive)" : "روز آخر (شامل)",
+ "Out of office replacement (optional)" : "جایگزین خارج از دفتر (اختیاری)",
+ "Name of the replacement" : "نام جایگزین",
+ "No results." : "نتیجه‌ای یافت نشد.",
+ "Start typing." : "شروع به تایپ کنید.",
+ "Short absence status" : "وضعیت کوتاه غیبت",
+ "Long absence Message" : "پیام طولانی غیبت",
+ "Save" : "ذخیره",
+ "Disable absence" : "غیرفعال کردن غیبت",
+ "Failed to load availability" : "بارگذاری در دسترس بودن با شکست مواجه شد",
+ "Saved availability" : "در دسترس بودن ذخیره شد",
+ "Failed to save availability" : "ذخیره در دسترس بودن با شکست مواجه شد",
+ "Time zone:" : "منطقه زمانی:",
+ "to" : "تا",
+ "Delete slot" : "حذف بازه زمانی",
+ "No working hours set" : "ساعات کاری تعیین نشده است",
+ "Add slot" : "افزودن بازه زمانی",
+ "Weekdays" : "روزهای هفته",
+ "Pick a start time for {dayName}" : "زمان شروع برای {dayName} را انتخاب کنید",
+ "Pick a end time for {dayName}" : "زمان پایان برای {dayName} را انتخاب کنید",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "وضعیت کاربر بصورت خودکار به \"مزاحم نشوید\" تغییر داده شود تا همه ی اعلان ها خاموش شوند.",
+ "Cancel" : "لغو",
+ "Import" : "وارد کردن",
+ "Error while saving settings" : "خطا هنگام ذخیره تنظیمات",
+ "Contact reset successfully" : "مخاطب با موفقیت بازنشانی شد",
+ "Error while resetting contact" : "خطا هنگام بازنشانی مخاطب",
+ "Contact imported successfully" : "مخاطب با موفقیت وارد شد",
+ "Error while importing contact" : "خطا هنگام وارد کردن مخاطب",
+ "Import contact" : "وارد کردن مخاطب",
+ "Reset to default" : "بازنشانی به پیش‌گزیده",
+ "Import contacts" : "وارد کردن مخاطبین",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "وارد کردن یک فایل .vcf جدید، مخاطب پیش‌فرض موجود را حذف کرده و آن را با مخاطب جدید جایگزین می‌کند. آیا می‌خواهید ادامه دهید؟",
+ "Availability" : "در دسترس بودن",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "اگر ساعات کاری خود را پیکربندی کنید، دیگران هنگام رزرو جلسه متوجه خواهند شد که شما در دفتر نیستید.",
+ "Absence" : "غیبت",
+ "Configure your next absence period." : "دوره غیبت بعدی خود را پیکربندی کنید.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "همچنین {calendarappstoreopen}برنامه تقویم{linkclose} را نصب کنید، یا {calendardocopen}دسکتاپ و موبایل خود را برای همگام‌سازی متصل کنید ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "لطفاً مطمئن شوید که {emailopen}سرور ایمیل{linkclose} را به درستی تنظیم کرده‌اید.",
+ "Calendar server" : "سرور تقویم",
+ "Send invitations to attendees" : "ارسال دعوت‌نامه‌ها به شرکت‌کنندگان",
+ "Automatically generate a birthday calendar" : "به‌طور خودکار یک تقویم تولد ایجاد کنید",
+ "Birthday calendars will be generated by a background job." : "تقویم‌های تولد توسط یک کار پس‌زمینه ایجاد خواهند شد.",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "بنابراین بلافاصله پس از فعال‌سازی در دسترس نخواهند بود، اما پس از مدتی نمایان می‌شوند.",
+ "Send notifications for events" : "ارسال اعلان‌ها برای رویدادها",
+ "Notifications are sent via background jobs, so these must occur often enough." : "اعلان‌ها از طریق کارهای پس‌زمینه ارسال می‌شوند، بنابراین این کارها باید به اندازه کافی مکرر انجام شوند.",
+ "Send reminder notifications to calendar sharees as well" : "ارسال اعلان‌های یادآوری به اشتراک‌گذاران تقویم نیز",
+ "Reminders are always sent to organizers and attendees." : "یادآوری‌ها همیشه برای برگزارکنندگان و شرکت‌کنندگان ارسال می‌شوند.",
+ "Enable notifications for events via push" : "فعال کردن اعلان‌ها برای رویدادها از طریق پوش",
+ "There was an error updating your attendance status." : "خطایی در به‌روزرسانی وضعیت حضور شما رخ داد.",
+ "Please contact the organizer directly." : "لطفاً مستقیماً با برگزارکننده تماس بگیرید.",
+ "Are you accepting the invitation?" : "آیا دعوت را می‌پذیرید؟",
+ "Tentative" : "آزمایشی",
+ "Your attendance was updated successfully." : "وضعیت حضور شما با موفقیت به‌روزرسانی شد."
+},
+"nplurals=2; plural=(n > 1);");
diff --git a/apps/dav/l10n/fa.json b/apps/dav/l10n/fa.json
new file mode 100644
index 00000000000..ca3b31c1f55
--- /dev/null
+++ b/apps/dav/l10n/fa.json
@@ -0,0 +1,324 @@
+{ "translations": {
+ "Calendar" : "تقویم",
+ "Tasks" : "وظایف",
+ "Personal" : "شخصی",
+ "{actor} created calendar {calendar}" : "{actor} تقویم {calendar} را ایجاد کرد",
+ "You created calendar {calendar}" : "شما تقویم {calendar} را ایجاد کردید",
+ "{actor} deleted calendar {calendar}" : "{actor} تقویم {calendar} را حذف کرد",
+ "You deleted calendar {calendar}" : "شما تقویم {calendar} را حذف کردید",
+ "{actor} updated calendar {calendar}" : "{actor} تقویم {calendar} را به‌روزرسانی کرد",
+ "You updated calendar {calendar}" : "شما تقویم {calendar} را به‌روزرسانی کردید",
+ "{actor} restored calendar {calendar}" : "{actor} تقویم {calendar} را بازیابی کرد",
+ "You restored calendar {calendar}" : "شما تقویم {calendar} را بازیابی کردید",
+ "You shared calendar {calendar} as public link" : "شما تقویم {calendar} را به‌عنوان پیوند عمومی به اشتراک گذاشتید",
+ "You removed public link for calendar {calendar}" : "شما پیوند عمومی تقویم {calendar} را حذف کردید",
+ "{actor} shared calendar {calendar} with you" : "{actor} تقویم {calendar} را با شما به اشتراک گذاشت",
+ "You shared calendar {calendar} with {user}" : "شما تقویم {calendar} را با {user} به اشتراک گذاشتید",
+ "{actor} shared calendar {calendar} with {user}" : "{actor} تقویم {calendar} را با {user} به اشتراک گذاشت",
+ "{actor} unshared calendar {calendar} from you" : "{actor} اشتراک‌گذاری تقویم {calendar} را از شما لغو کرد",
+ "You unshared calendar {calendar} from {user}" : "شما اشتراک‌گذاری تقویم {calendar} را از {user} لغو کردید",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor} اشتراک‌گذاری تقویم {calendar} را از {user} لغو کرد",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} اشتراک‌گذاری تقویم {calendar} را از خود لغو کرد",
+ "You shared calendar {calendar} with group {group}" : "شما تقویم {calendar} را با گروه {group} به اشتراک گذاشتید",
+ "{actor} shared calendar {calendar} with group {group}" : "{actor} تقویم {calendar} را با گروه {group} به اشتراک گذاشت",
+ "You unshared calendar {calendar} from group {group}" : "شما اشتراک‌گذاری تقویم {calendar} را از گروه {group} لغو کردید",
+ "{actor} unshared calendar {calendar} from group {group}" : "{actor} اشتراک‌گذاری تقویم {calendar} را از گروه {group} لغو کرد",
+ "Untitled event" : "رویداد بدون عنوان",
+ "{actor} created event {event} in calendar {calendar}" : "{actor} رویداد {event} را در تقویم {calendar} ایجاد کرد",
+ "You created event {event} in calendar {calendar}" : "شما رویداد {event} را در تقویم {calendar} ایجاد کردید",
+ "{actor} deleted event {event} from calendar {calendar}" : "{actor} رویداد {event} را از تقویم {calendar} حذف کرد",
+ "You deleted event {event} from calendar {calendar}" : "شما رویداد {event} را از تقویم {calendar} حذف کردید",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} رویداد {event} را در تقویم {calendar} به‌روزرسانی کرد",
+ "You updated event {event} in calendar {calendar}" : "شما رویداد {event} را در تقویم {calendar} به‌روزرسانی کردید",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} رویداد {event} را از تقویم {sourceCalendar} به تقویم {targetCalendar} منتقل کرد",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "شما رویداد {event} را از تقویم {sourceCalendar} به تقویم {targetCalendar} منتقل کردید",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} رویداد {event} را از تقویم {calendar} بازیابی کرد",
+ "You restored event {event} of calendar {calendar}" : "شما رویداد {event} را از تقویم {calendar} بازیابی کردید",
+ "Busy" : "مشغول",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} کار {todo} را در لیست {calendar} ایجاد کرد",
+ "You created to-do {todo} in list {calendar}" : "شما کار {todo} را در لیست {calendar} ایجاد کردید",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} کار {todo} را از لیست {calendar} حذف کرد",
+ "You deleted to-do {todo} from list {calendar}" : "شما کار {todo} را از لیست {calendar} حذف کردید",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} کار {todo} را در لیست {calendar} به‌روزرسانی کرد",
+ "You updated to-do {todo} in list {calendar}" : "شما کار {todo} را در لیست {calendar} به‌روزرسانی کردید",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} کار {todo} را در لیست {calendar} حل کرد",
+ "You solved to-do {todo} in list {calendar}" : "شما کار {todo} را در لیست {calendar} حل کردید",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} کار {todo} را در لیست {calendar} بازگشایی کرد",
+ "You reopened to-do {todo} in list {calendar}" : "شما کار {todo} را در لیست {calendar} بازگشایی کردید",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} کار {todo} را از لیست {sourceCalendar} به لیست {targetCalendar} منتقل کرد",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "شما کار {todo} را از لیست {sourceCalendar} به لیست {targetCalendar} منتقل کردید",
+ "Calendar, contacts and tasks" : "تقویم، مخاطبین و وظایف",
+ "A <strong>calendar</strong> was modified" : "یک <strong>تقویم</strong> تغییر کرد",
+ "A calendar <strong>event</strong> was modified" : "یک <strong>رویداد</strong> تقویم تغییر کرد",
+ "A calendar <strong>to-do</strong> was modified" : "یک <strong>کار</strong> تقویم تغییر کرد",
+ "Contact birthdays" : "تولد مخاطبین",
+ "Death of %s" : "فوت %s",
+ "Untitled calendar" : "تقویم بدون عنوان",
+ "Calendar:" : "تقویم:",
+ "Date:" : "تاریخ:",
+ "Where:" : "مکان:",
+ "Description:" : "توضیحات:",
+ "_%n year_::_%n years_" : ["%n سال","%n سال"],
+ "_%n month_::_%n months_" : ["%n ماه","%n ماه"],
+ "_%n day_::_%n days_" : ["%n روز","%n روز"],
+ "_%n hour_::_%n hours_" : ["%n ساعت","%n ساعت"],
+ "_%n minute_::_%n minutes_" : ["%n دقیقه","%n دقیقه"],
+ "%s (in %s)" : "%s (در %s)",
+ "%s (%s ago)" : "%s (%s پیش)",
+ "Calendar: %s" : "تقویم: %s",
+ "Date: %s" : "تاریخ: %s",
+ "Description: %s" : "توضیحات: %s",
+ "Where: %s" : "مکان: %s",
+ "%1$s via %2$s" : "%1$s از طریق %2$s",
+ "In the past on %1$s for the entire day" : "در گذشته در %1$s برای کل روز",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["در یک دقیقه در %1$s برای کل روز","در %n دقیقه در %1$s برای کل روز"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["در یک ساعت در %1$s برای کل روز","در %n ساعت در %1$s برای کل روز"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["در یک روز در %1$s برای کل روز","در %n روز در %1$s برای کل روز"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["در یک هفته در %1$s برای کل روز","در %n هفته در %1$s برای کل روز"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["در یک ماه در %1$s برای کل روز","در %n ماه در %1$s برای کل روز"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["در یک سال در %1$s برای کل روز","در %n سال در %1$s برای کل روز"],
+ "In the past on %1$s between %2$s - %3$s" : "در گذشته در %1$s بین %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["در یک دقیقه در %1$s بین %2$s - %3$s","در %n دقیقه در %1$s بین %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["در یک ساعت در %1$s بین %2$s - %3$s","در %n ساعت در %1$s بین %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["در یک روز در %1$s بین %2$s - %3$s","در %n روز در %1$s بین %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["در یک هفته در %1$s بین %2$s - %3$s","در %n هفته در %1$s بین %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["در یک ماه در %1$s بین %2$s - %3$s","در %n ماه در %1$s بین %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["در یک سال در %1$s بین %2$s - %3$s","در %n سال در %1$s بین %2$s - %3$s"],
+ "Could not generate when statement" : "امکان ایجاد عبارت زمان وجود ندارد",
+ "Every Day for the entire day" : "هر روز برای کل روز",
+ "Every Day for the entire day until %1$s" : "هر روز برای کل روز تا %1$s",
+ "Every Day between %1$s - %2$s" : "هر روز بین %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "هر روز بین %1$s - %2$s تا %3$s",
+ "Every %1$d Days for the entire day" : "هر %1$d روز برای کل روز",
+ "Every %1$d Days for the entire day until %2$s" : "هر %1$d روز برای کل روز تا %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "هر %1$d روز بین %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "هر %1$d روز بین %2$s - %3$s تا %4$s",
+ "Could not generate event recurrence statement" : "امکان ایجاد عبارت تکرار رویداد وجود ندارد",
+ "Every Week on %1$s for the entire day" : "هر هفته در %1$s برای کل روز",
+ "Every Week on %1$s for the entire day until %2$s" : "هر هفته در %1$s برای کل روز تا %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "هر هفته در %1$s بین %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "هر هفته در %1$s بین %2$s - %3$s تا %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "هر %1$d هفته در %2$s برای کل روز",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "هر %1$d هفته در %2$s برای کل روز تا %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "هر %1$d هفته در %2$s بین %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "هر %1$d هفته در %2$s بین %3$s - %4$s تا %5$s",
+ "Every Month on the %1$s for the entire day" : "هر ماه در %1$s برای کل روز",
+ "Every Month on the %1$s for the entire day until %2$s" : "هر ماه در %1$s برای کل روز تا %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "هر ماه در %1$s بین %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "هر ماه در %1$s بین %2$s - %3$s تا %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "هر %1$d ماه در %2$s برای کل روز",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "هر %1$d ماه در %2$s برای کل روز تا %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "هر %1$d ماه در %2$s بین %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "هر %1$d ماه در %2$s بین %3$s - %4$s تا %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "هر سال در %1$s در %2$s برای کل روز",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "هر سال در %1$s در %2$s برای کل روز تا %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "هر سال در %1$s در %2$s بین %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "هر سال در %1$s در %2$s بین %3$s - %4$s تا %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "هر %1$d سال در %2$s در %3$s برای کل روز",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "هر %1$d سال در %2$s در %3$s برای کل روز تا %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "هر %1$d سال در %2$s در %3$s بین %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "هر %1$d سال در %2$s در %3$s بین %4$s - %5$s تا %6$s",
+ "On specific dates for the entire day until %1$s" : "در تاریخ‌های مشخص برای کل روز تا %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "در تاریخ‌های مشخص بین %1$s - %2$s تا %3$s",
+ "In the past on %1$s" : "در گذشته در %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["در یک دقیقه در %1$s","در %n دقیقه در %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["در یک ساعت در %1$s","در %n ساعت در %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["در یک روز در %1$s","در %n روز در %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["در یک هفته در %1$s","در %n هفته در %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["در یک ماه در %1$s","در %n ماه در %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["در یک سال در %1$s","در %n سال در %1$s"],
+ "In the past on %1$s then on %2$s" : "در گذشته در %1$s سپس در %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["در یک دقیقه در %1$s سپس در %2$s","در %n دقیقه در %1$s سپس در %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["در یک ساعت در %1$s سپس در %2$s","در %n ساعت در %1$s سپس در %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["در یک روز در %1$s سپس در %2$s","در %n روز در %1$s سپس در %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["در یک هفته در %1$s سپس در %2$s","در %n هفته در %1$s سپس در %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["در یک ماه در %1$s سپس در %2$s","در %n ماه در %1$s سپس در %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["در یک سال در %1$s سپس در %2$s","در %n سال در %1$s سپس در %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "در گذشته در %1$s سپس در %2$s و %3$s",
+ "_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_" : ["در یک دقیقه در %1$s سپس در %2$s و %3$s","در %n دقیقه در %1$s سپس در %2$s و %3$s"],
+ "_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_" : ["در یک ساعت در %1$s سپس در %2$s و %3$s","در %n ساعت در %1$s سپس در %2$s و %3$s"],
+ "_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_" : ["در یک روز در %1$s سپس در %2$s و %3$s","در %n روز در %1$s سپس در %2$s و %3$s"],
+ "_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_" : ["در یک هفته در %1$s سپس در %2$s و %3$s","در %n هفته در %1$s سپس در %2$s و %3$s"],
+ "_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_" : ["در یک ماه در %1$s سپس در %2$s و %3$s","در %n ماه در %1$s سپس در %2$s و %3$s"],
+ "_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_" : ["در یک سال در %1$s سپس در %2$s و %3$s","در %n سال در %1$s سپس در %2$s و %3$s"],
+ "Could not generate next recurrence statement" : "امکان ایجاد عبارت تکرار بعدی وجود ندارد",
+ "Cancelled: %1$s" : "لغو شد: %1$s",
+ "\"%1$s\" has been canceled" : "\"%1$s\" لغو شده است",
+ "Re: %1$s" : "پاسخ: %1$s",
+ "%1$s has accepted your invitation" : "%1$s دعوت شما را پذیرفته است",
+ "%1$s has tentatively accepted your invitation" : "%1$s دعوت شما را به‌طور آزمایشی پذیرفته است",
+ "%1$s has declined your invitation" : "%1$s دعوت شما را رد کرده است",
+ "%1$s has responded to your invitation" : "%1$s به دعوت شما پاسخ داده است",
+ "Invitation updated: %1$s" : "دعوت به‌روزرسانی شد: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s رویداد \"%2$s\" را به‌روزرسانی کرد",
+ "Invitation: %1$s" : "دعوت: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s مایل است شما را به \"%2$s\" دعوت کند",
+ "Organizer:" : "برگزارکننده:",
+ "Attendees:" : "شرکت‌کنندگان:",
+ "Title:" : "عنوان:",
+ "When:" : "چه زمانی:",
+ "Location:" : "مکان:",
+ "Link:" : "پیوند:",
+ "Occurring:" : "در حال وقوع:",
+ "Accept" : "پذیرفتن",
+ "Decline" : "رد کردن",
+ "More options …" : "گزینه‌های بیشتر…",
+ "More options at %s" : "گزینه‌های بیشتر در %s",
+ "Monday" : "دوشنبه",
+ "Tuesday" : "سه‌شنبه",
+ "Wednesday" : "چهارشنبه",
+ "Thursday" : "پنجشنبه",
+ "Friday" : "جمعه",
+ "Saturday" : "شنبه",
+ "Sunday" : "یکشنبه",
+ "January" : "ژانویه",
+ "February" : "فوریه",
+ "March" : "مارس",
+ "April" : "آوریل",
+ "May" : "مه",
+ "June" : "ژوئن",
+ "July" : "ژوئیه",
+ "August" : "اوت",
+ "September" : "سپتامبر",
+ "October" : "اکتبر",
+ "November" : "نوامبر",
+ "December" : "دسامبر",
+ "First" : "اول",
+ "Second" : "دوم",
+ "Third" : "سوم",
+ "Fourth" : "چهارم",
+ "Fifth" : "پنجم",
+ "Last" : "آخر",
+ "Second Last" : "دومی از آخر",
+ "Third Last" : "سومی از آخر",
+ "Fourth Last" : "چهارمی از آخر",
+ "Fifth Last" : "پنجمی از آخر",
+ "Contacts" : "مخاطبین",
+ "{actor} created address book {addressbook}" : "{actor} دفترچه آدرس {addressbook} را ایجاد کرد",
+ "You created address book {addressbook}" : "شما دفترچه آدرس {addressbook} را ایجاد کردید",
+ "{actor} deleted address book {addressbook}" : "{actor} دفترچه آدرس {addressbook} را حذف کرد",
+ "You deleted address book {addressbook}" : "شما دفترچه آدرس {addressbook} را حذف کردید",
+ "{actor} updated address book {addressbook}" : "{actor} دفترچه آدرس {addressbook} را به‌روزرسانی کرد",
+ "You updated address book {addressbook}" : "شما دفترچه آدرس {addressbook} را به‌روزرسانی کردید",
+ "{actor} shared address book {addressbook} with you" : "{actor} دفترچه آدرس {addressbook} را با شما به اشتراک گذاشت",
+ "You shared address book {addressbook} with {user}" : "شما دفترچه آدرس {addressbook} را با {user} به اشتراک گذاشتید",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} دفترچه آدرس {addressbook} را با {user} به اشتراک گذاشت",
+ "{actor} unshared address book {addressbook} from you" : "{actor} اشتراک‌گذاری دفترچه آدرس {addressbook} را از شما لغو کرد",
+ "You unshared address book {addressbook} from {user}" : "شما اشتراک‌گذاری دفترچه آدرس {addressbook} را از {user} لغو کردید",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} اشتراک‌گذاری دفترچه آدرس {addressbook} را از {user} لغو کرد",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} اشتراک‌گذاری دفترچه آدرس {addressbook} را از خود لغو کرد",
+ "You shared address book {addressbook} with group {group}" : "شما دفترچه آدرس {addressbook} را با گروه {group} به اشتراک گذاشتید",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} دفترچه آدرس {addressbook} را با گروه {group} به اشتراک گذاشت",
+ "You unshared address book {addressbook} from group {group}" : "شما اشتراک‌گذاری دفترچه آدرس {addressbook} را از گروه {group} لغو کردید",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} اشتراک‌گذاری دفترچه آدرس {addressbook} را از گروه {group} لغو کرد",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} مخاطب {card} را در دفترچه آدرس {addressbook} ایجاد کرد",
+ "You created contact {card} in address book {addressbook}" : "شما مخاطب {card} را در دفترچه آدرس {addressbook} ایجاد کردید",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} مخاطب {card} را از دفترچه آدرس {addressbook} حذف کرد",
+ "You deleted contact {card} from address book {addressbook}" : "شما مخاطب {card} را از دفترچه آدرس {addressbook} حذف کردید",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} مخاطب {card} را در دفترچه آدرس {addressbook} به‌روزرسانی کرد",
+ "You updated contact {card} in address book {addressbook}" : "شما مخاطب {card} را در دفترچه آدرس {addressbook} به‌روزرسانی کردید",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "یک <strong>مخاطب</strong> یا <strong>دفترچه آدرس</strong> تغییر کرد",
+ "Accounts" : "حساب‌ها",
+ "System address book which holds all accounts" : "دفترچه آدرس سیستمی که شامل تمام حساب‌ها است",
+ "File is not updatable: %1$s" : "فایل قابل به‌روزرسانی نیست: %1$s",
+ "Failed to get storage for file" : "دریافت فضای ذخیره‌سازی برای فایل با شکست مواجه شد",
+ "Could not write to final file, canceled by hook" : "نوشتن در فایل نهایی امکان‌پذیر نبود، توسط هوک لغو شد",
+ "Could not write file contents" : "امکان نوشتن محتویات فایل وجود ندارد",
+ "_%n byte_::_%n bytes_" : ["%n بایت","%n بایت"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "خطا هنگام کپی فایل به مکان مقصد (کپی شده: %1$s، حجم فایل مورد انتظار: %2$s)",
+ "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." : "حجم فایل مورد انتظار %1$s بود اما %2$s خوانده (از کلاینت Nextcloud) و نوشته (در فضای ذخیره‌سازی Nextcloud) شد. این می‌تواند ناشی از مشکل شبکه در سمت ارسال یا مشکل نوشتن در فضای ذخیره‌سازی در سمت سرور باشد.",
+ "Could not rename part file to final file, canceled by hook" : "امکان تغییر نام فایل جزئی به فایل نهایی وجود نداشت، توسط هوک لغو شد",
+ "Could not rename part file to final file" : "امکان تغییر نام فایل جزئی به فایل نهایی وجود ندارد",
+ "Failed to check file size: %1$s" : "بررسی حجم فایل با شکست مواجه شد: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "امکان باز کردن فایل وجود ندارد: %1$s، به نظر می‌رسد فایل وجود دارد",
+ "Could not open file: %1$s, file doesn't seem to exist" : "امکان باز کردن فایل وجود ندارد: %1$s، به نظر می‌رسد فایل وجود ندارد",
+ "Encryption not ready: %1$s" : "رمزگذاری آماده نیست: %1$s",
+ "Failed to open file: %1$s" : "باز کردن فایل با شکست مواجه شد: %1$s",
+ "Failed to unlink: %1$s" : "حذف پیوند با شکست مواجه شد: %1$s",
+ "Failed to write file contents: %1$s" : "نوشتن محتویات فایل با شکست مواجه شد: %1$s",
+ "File not found: %1$s" : "فایل یافت نشد: %1$s",
+ "Invalid target path" : "مسیر مقصد نامعتبر است",
+ "System is in maintenance mode." : "سیستم در حالت نگهداری است.",
+ "Upgrade needed" : "نیاز به ارتقا",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "برای استفاده از CalDAV و CardDAV با iOS/macOS، %s شما باید برای استفاده از HTTPS پیکربندی شود.",
+ "Configures a CalDAV account" : "یک حساب CalDAV را پیکربندی می‌کند",
+ "Configures a CardDAV account" : "یک حساب CardDAV را پیکربندی می‌کند",
+ "Events" : "رویدادها",
+ "Untitled task" : "کار بدون عنوان",
+ "Completed on %s" : "تکمیل شده در %s",
+ "Due on %s by %s" : "موعد در %s توسط %s",
+ "Due on %s" : "موعد در %s",
+ "System Address Book" : "دفترچه آدرس سیستم",
+ "The system address book contains contact information for all users in your instance." : "دفترچه آدرس سیستم شامل اطلاعات تماس برای همه کاربران در نمونه شما است.",
+ "Enable System Address Book" : "فعال کردن دفترچه آدرس سیستم",
+ "DAV system address book" : "دفترچه آدرس سیستم DAV",
+ "No outstanding DAV system address book sync." : "هیچ همگام‌سازی دفترچه آدرس سیستم DAV در انتظار نیست.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "همگام‌سازی دفترچه آدرس سیستم DAV هنوز اجرا نشده است زیرا نمونه شما بیش از ۱۰۰۰ کاربر دارد یا خطایی رخ داده است. لطفاً آن را به‌صورت دستی با فراخوانی \"occ dav:sync-system-addressbook\" اجرا کنید.",
+ "WebDAV endpoint" : "نقطه پایانی WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "امکان بررسی اینکه سرور وب شما به درستی برای همگام‌سازی فایل از طریق WebDAV تنظیم شده است، وجود ندارد. لطفاً به صورت دستی بررسی کنید.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "سرور وب شما هنوز به درستی برای همگام‌سازی فایل تنظیم نشده است، زیرا به نظر می‌رسد رابط WebDAV خراب است.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "سرور وب شما به درستی برای همگام‌سازی فایل از طریق WebDAV تنظیم شده است.",
+ "Migrated calendar (%1$s)" : "تقویم منتقل شده (%1$s)",
+ "Calendars including events, details and attendees" : "تقویم‌ها شامل رویدادها، جزئیات و شرکت‌کنندگان",
+ "Contacts and groups" : "مخاطبین و گروه‌ها",
+ "WebDAV" : "وب‌دَو",
+ "Absence saved" : "غیبت ذخیره شد",
+ "Failed to save your absence settings" : "ذخیره تنظیمات غیبت شما با شکست مواجه شد",
+ "Absence cleared" : "غیبت پاک شد",
+ "Failed to clear your absence settings" : "پاک کردن تنظیمات غیبت شما با شکست مواجه شد",
+ "First day" : "روز اول",
+ "Last day (inclusive)" : "روز آخر (شامل)",
+ "Out of office replacement (optional)" : "جایگزین خارج از دفتر (اختیاری)",
+ "Name of the replacement" : "نام جایگزین",
+ "No results." : "نتیجه‌ای یافت نشد.",
+ "Start typing." : "شروع به تایپ کنید.",
+ "Short absence status" : "وضعیت کوتاه غیبت",
+ "Long absence Message" : "پیام طولانی غیبت",
+ "Save" : "ذخیره",
+ "Disable absence" : "غیرفعال کردن غیبت",
+ "Failed to load availability" : "بارگذاری در دسترس بودن با شکست مواجه شد",
+ "Saved availability" : "در دسترس بودن ذخیره شد",
+ "Failed to save availability" : "ذخیره در دسترس بودن با شکست مواجه شد",
+ "Time zone:" : "منطقه زمانی:",
+ "to" : "تا",
+ "Delete slot" : "حذف بازه زمانی",
+ "No working hours set" : "ساعات کاری تعیین نشده است",
+ "Add slot" : "افزودن بازه زمانی",
+ "Weekdays" : "روزهای هفته",
+ "Pick a start time for {dayName}" : "زمان شروع برای {dayName} را انتخاب کنید",
+ "Pick a end time for {dayName}" : "زمان پایان برای {dayName} را انتخاب کنید",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "وضعیت کاربر بصورت خودکار به \"مزاحم نشوید\" تغییر داده شود تا همه ی اعلان ها خاموش شوند.",
+ "Cancel" : "لغو",
+ "Import" : "وارد کردن",
+ "Error while saving settings" : "خطا هنگام ذخیره تنظیمات",
+ "Contact reset successfully" : "مخاطب با موفقیت بازنشانی شد",
+ "Error while resetting contact" : "خطا هنگام بازنشانی مخاطب",
+ "Contact imported successfully" : "مخاطب با موفقیت وارد شد",
+ "Error while importing contact" : "خطا هنگام وارد کردن مخاطب",
+ "Import contact" : "وارد کردن مخاطب",
+ "Reset to default" : "بازنشانی به پیش‌گزیده",
+ "Import contacts" : "وارد کردن مخاطبین",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "وارد کردن یک فایل .vcf جدید، مخاطب پیش‌فرض موجود را حذف کرده و آن را با مخاطب جدید جایگزین می‌کند. آیا می‌خواهید ادامه دهید؟",
+ "Availability" : "در دسترس بودن",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "اگر ساعات کاری خود را پیکربندی کنید، دیگران هنگام رزرو جلسه متوجه خواهند شد که شما در دفتر نیستید.",
+ "Absence" : "غیبت",
+ "Configure your next absence period." : "دوره غیبت بعدی خود را پیکربندی کنید.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "همچنین {calendarappstoreopen}برنامه تقویم{linkclose} را نصب کنید، یا {calendardocopen}دسکتاپ و موبایل خود را برای همگام‌سازی متصل کنید ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "لطفاً مطمئن شوید که {emailopen}سرور ایمیل{linkclose} را به درستی تنظیم کرده‌اید.",
+ "Calendar server" : "سرور تقویم",
+ "Send invitations to attendees" : "ارسال دعوت‌نامه‌ها به شرکت‌کنندگان",
+ "Automatically generate a birthday calendar" : "به‌طور خودکار یک تقویم تولد ایجاد کنید",
+ "Birthday calendars will be generated by a background job." : "تقویم‌های تولد توسط یک کار پس‌زمینه ایجاد خواهند شد.",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "بنابراین بلافاصله پس از فعال‌سازی در دسترس نخواهند بود، اما پس از مدتی نمایان می‌شوند.",
+ "Send notifications for events" : "ارسال اعلان‌ها برای رویدادها",
+ "Notifications are sent via background jobs, so these must occur often enough." : "اعلان‌ها از طریق کارهای پس‌زمینه ارسال می‌شوند، بنابراین این کارها باید به اندازه کافی مکرر انجام شوند.",
+ "Send reminder notifications to calendar sharees as well" : "ارسال اعلان‌های یادآوری به اشتراک‌گذاران تقویم نیز",
+ "Reminders are always sent to organizers and attendees." : "یادآوری‌ها همیشه برای برگزارکنندگان و شرکت‌کنندگان ارسال می‌شوند.",
+ "Enable notifications for events via push" : "فعال کردن اعلان‌ها برای رویدادها از طریق پوش",
+ "There was an error updating your attendance status." : "خطایی در به‌روزرسانی وضعیت حضور شما رخ داد.",
+ "Please contact the organizer directly." : "لطفاً مستقیماً با برگزارکننده تماس بگیرید.",
+ "Are you accepting the invitation?" : "آیا دعوت را می‌پذیرید؟",
+ "Tentative" : "آزمایشی",
+ "Your attendance was updated successfully." : "وضعیت حضور شما با موفقیت به‌روزرسانی شد."
+},"pluralForm" :"nplurals=2; plural=(n > 1);"
+} \ No newline at end of file
diff --git a/apps/dav/l10n/fi.js b/apps/dav/l10n/fi.js
deleted file mode 100644
index 2100387dea7..00000000000
--- a/apps/dav/l10n/fi.js
+++ /dev/null
@@ -1,146 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Kalenteri",
- "Todos" : "Tehtävät",
- "Personal" : "Henkilökohtainen",
- "{actor} created calendar {calendar}" : "{actor} loi kalenterin {calendar}",
- "You created calendar {calendar}" : "Loit kalenterin {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} poisti kalenterin {calendar}",
- "You deleted calendar {calendar}" : "Poistit kalenterin {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} päivitti kalenterin {calendar}",
- "You updated calendar {calendar}" : "Päivitit kalenterin {calendar}",
- "{actor} restored calendar {calendar}" : "{actor} palautti kalenterin {calendar}",
- "You restored calendar {calendar}" : "Palautit kalenterin {calendar}",
- "You shared calendar {calendar} as public link" : "Jaoit kalenterin {calendar} julkisena linkkinä",
- "You removed public link for calendar {calendar}" : "Poistit julkisen linkin kalenterilta {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} jakoi kalenterin {calendar} kanssasi",
- "You shared calendar {calendar} with {user}" : "Jaoit kalenterin {calendar} käyttäjälle {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} jakoi kalenterin {calendar} käyttäjälle {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} lopetti kalenterin {calendar} jakamisen kanssasi",
- "You unshared calendar {calendar} from {user}" : "Lopetit kalenterin {calendar} jakamisen käyttäjälle {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} lopetti kalenterin {calendar} jakamisen käyttäjälle {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} lopetti kalenterin {calendar} jakamisen itselleen",
- "You shared calendar {calendar} with group {group}" : "Jaoit kalenterin {calendar} ryhmälle {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} jakoi kalenterin {calendar} ryhmälle {group}",
- "You unshared calendar {calendar} from group {group}" : "Lopetit kalenterin {calendar} jakamisen ryhmälle {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} lopetti kalenterin {calendar} jakamisen ryhmälle {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} loi tapahtuman {event} kalenteriin {calendar}",
- "You created event {event} in calendar {calendar}" : "Loit tapahtuman {event} kalenteriin {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} poisti tapahtuman {event} kalenterista {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Poistit tapahtuman {event} kalenterista {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} päivitti tapahtuman {event} kalenteriin {calendar}",
- "You updated event {event} in calendar {calendar}" : "Päivitit tapahtuman {event} kalenteriin {calendar}",
- "Busy" : "Varattu",
- "{actor} created todo {todo} in list {calendar}" : "{actor} loi tehtävän {todo} listaan {calendar}",
- "You created todo {todo} in list {calendar}" : "Loit tehtävän {todo} listaan {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} poisti tehtävän {todo} listasta {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Poistit tehtävän {todo} listasta {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} päivitti tehtävän {todo} listassa {calendar}",
- "You updated todo {todo} in list {calendar}" : "Päivitit tehtävän {todo} listassa {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} suoritti tehtävän {todo} listasta {calendar}",
- "You solved todo {todo} in list {calendar}" : "Suoritit tehtävän {todo} listasta {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} avasi uudelleen tehtävän {todo} listassa {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Avasit uudelleen tehtävän {todo} listassa {calendar}",
- "Calendar, contacts and tasks" : "Kalenteri, yhteystiedot ja tehtävät",
- "A <strong>calendar</strong> was modified" : "<strong>Kalenteria</strong> on muokattu",
- "A calendar <strong>event</strong> was modified" : "Kalenterin <strong>tapahtumaa</strong> on muokattu",
- "A calendar <strong>todo</strong> was modified" : "Kalenterin <strong>tehtävää</strong> on muokattu",
- "Contact birthdays" : "Yhteystietojen syntymäpäivät",
- "Death of %s" : "%s kuolema",
- "Calendar:" : "Kalenteri:",
- "Date:" : "Päiväys:",
- "Where:" : "Missä:",
- "Description:" : "Kuvaus:",
- "Untitled event" : "Nimetön tapahtuma",
- "_%n year_::_%n years_" : ["%n vuosi","%n vuotta"],
- "_%n month_::_%n months_" : ["%n kuukausi","%n kuukautta"],
- "_%n day_::_%n days_" : ["%n päivä","%n päivää"],
- "_%n hour_::_%n hours_" : ["%n tunti","%n tuntia"],
- "_%n minute_::_%n minutes_" : ["%n minuutti","%n minuuttia"],
- "%s (%s ago)" : "%s (%s sitten)",
- "Calendar: %s" : "Kalenteri: %s",
- "Date: %s" : "Päiväys: %s",
- "Description: %s" : "Kuvaus: %s",
- "Where: %s" : "Missä: %s",
- "Cancelled: %1$s" : "Peruutettu: %1$s",
- "Invitation canceled" : "Kutsu peruttu",
- "Invitation updated" : "Kutsu päivitetty",
- "Invitation: %1$s" : "Kutsu: %1$s",
- "Invitation" : "Kutsu",
- "Title:" : "Otsikko:",
- "Time:" : "Aika:",
- "Location:" : "Sijainti:",
- "Link:" : "Linkki:",
- "Organizer:" : "Järjestäjä:",
- "Attendees:" : "Osallistujat:",
- "Accept" : "Hyväksy",
- "Decline" : "Kieltäydy",
- "More options …" : "Lisää valintoja…",
- "More options at %s" : "Lisää valintoja kohteessa %s",
- "Contacts" : "Yhteystiedot",
- "{actor} created address book {addressbook}" : "{actor} loi osoitekirjan {addressbook}",
- "You created address book {addressbook}" : "Loit osoitekirjan {addressbook}",
- "{actor} deleted address book {addressbook}" : "{actor} poisti osoitekirjan {addressbook}",
- "You deleted address book {addressbook}" : "Sinä poistit osoitekirjan {addressbook}",
- "{actor} updated address book {addressbook}" : "{actor} päivitti osoitekirjaa {addressbook}",
- "You updated address book {addressbook}" : "Sinä päivitit osoitekirjaa {addressbook}",
- "{actor} shared address book {addressbook} with you" : "{actor} jakoi osoitekirjan {addressbook} kanssasi",
- "You shared address book {addressbook} with {user}" : "Jaoit osoitekirjan {addressbook} käyttäjän {user} kanssa",
- "{actor} shared address book {addressbook} with {user}" : "{actor} jakoi osoitekirjan {addressbook} käyttäjän {user} kanssa",
- "You shared address book {addressbook} with group {group}" : "Jaoit osoitekirjan {addressbook} ryhmän {group} kanssa",
- "{actor} shared address book {addressbook} with group {group}" : "{actor} jakoi osoitekirjan {addressbook} ryhmän {group} kanssa",
- "{actor} created contact {card} in address book {addressbook}" : "{actor} loi yhteystiedon {card} osoitekirjaan {addressbook}",
- "You created contact {card} in address book {addressbook}" : "Loit yhteystiedon {card} osoitekirjaan {addressbook}",
- "{actor} deleted contact {card} from address book {addressbook}" : "{actor} poisti yhteystiedon {card} osoitekirjasta {addressbook}",
- "You deleted contact {card} from address book {addressbook}" : "Poistit yhteystiedon {card} osoitekirjasta {addressbook}",
- "{actor} updated contact {card} in address book {addressbook}" : "{actor} päivitti yhteystietoa {card} osoitekirjassa {addressbook}",
- "You updated contact {card} in address book {addressbook}" : "Päivitit yhteystiedon {card} osoitekirjassa {addressbook}",
- "A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>Yhteystietoa</strong> tai <strong>osoitekirjaa</strong> muokattiin",
- "File is not updatable: %1$s" : "Tiedosto ei ole päivitettävissä: %1$s",
- "_%n byte_::_%n bytes_" : ["%n tavu","%n tavua"],
- "Could not open file" : "Tiedoston avaaminen ei onnistunut",
- "File not found: %1$s" : "Tiedostoa ei löydy: %1$s",
- "System is in maintenance mode." : "Järjestelmä on huoltotilassa",
- "Upgrade needed" : "Päivitys tarvitaan",
- "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "%s tulee asettaa käyttämään HTTPS-yhteyttä, jotta CalDAVia ja CardDAVia voi käyttää iOSilla tai macOS:llä.",
- "Configures a CalDAV account" : "Määrittää CalDAV-tilin",
- "Configures a CardDAV account" : "Määrittää CardDAV-tilin",
- "Events" : "Tapahtumat",
- "Tasks" : "Tehtävät",
- "Untitled task" : "Nimetön tehtävä",
- "Contacts and groups" : "Yhteystiedot ja ryhmät",
- "WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV-päätepiste",
- "Availability" : "Saatavuus",
- "Time zone:" : "Aikavyöhyke:",
- "to" : "Vastaanottaja",
- "Delete slot" : "Poista aikarako",
- "No working hours set" : "Työskentelytunteja ei ole asetettu",
- "Add slot" : "Lisää aikarako",
- "Monday" : "Maanantai",
- "Tuesday" : "Tiistai",
- "Wednesday" : "Keskiviikko",
- "Thursday" : "Torstai",
- "Friday" : "Perjantai",
- "Saturday" : "Lauantai",
- "Sunday" : "Sunnuntai",
- "Save" : "Tallenna",
- "Calendar server" : "Kalenteripalvelin",
- "Send invitations to attendees" : "Lähetä kutsut osallistujille",
- "Automatically generate a birthday calendar" : "Luo syntymäpäiväkalenteri automaattisesti",
- "Birthday calendars will be generated by a background job." : "Syntymäpäiväkalenterit luodaan taustatyön toimesta.",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Sen vuoksi ne eivät ole välittömästi saatavilla käyttöönoton jälkeen, vaan ne tulevat näkyviin pienellä viiveellä.",
- "Send notifications for events" : "Lähetä ilmoitukset tapahtumista",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Asenna myös {calendarappstoreopen}kalenterisovellus{linkclose}, tai {calendardocopen}yhdistä tietokoneesi ja mobiililaitteesi synkronointiyhteyteen ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Varmista että määrität {emailopen}sähköpostipalvelimen{linkclose} asetukset oikein.",
- "There was an error updating your attendance status." : "Osallistumisesi tilaa päivittäessä tapahtui virhe.",
- "Please contact the organizer directly." : "Ota yhteys suoraan järjestäjään.",
- "Are you accepting the invitation?" : "Hyväksytkö kutsun?",
- "Tentative" : "Alustava",
- "Number of guests" : "Vieraiden määrä",
- "Comment" : "Kommentti",
- "Your attendance was updated successfully." : "Osallistumisesi päivitettiin onnistuneesti.",
- "Calendar and tasks" : "Kalenteri ja tehtävät"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/fi.json b/apps/dav/l10n/fi.json
deleted file mode 100644
index 2f12ed24667..00000000000
--- a/apps/dav/l10n/fi.json
+++ /dev/null
@@ -1,144 +0,0 @@
-{ "translations": {
- "Calendar" : "Kalenteri",
- "Todos" : "Tehtävät",
- "Personal" : "Henkilökohtainen",
- "{actor} created calendar {calendar}" : "{actor} loi kalenterin {calendar}",
- "You created calendar {calendar}" : "Loit kalenterin {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} poisti kalenterin {calendar}",
- "You deleted calendar {calendar}" : "Poistit kalenterin {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} päivitti kalenterin {calendar}",
- "You updated calendar {calendar}" : "Päivitit kalenterin {calendar}",
- "{actor} restored calendar {calendar}" : "{actor} palautti kalenterin {calendar}",
- "You restored calendar {calendar}" : "Palautit kalenterin {calendar}",
- "You shared calendar {calendar} as public link" : "Jaoit kalenterin {calendar} julkisena linkkinä",
- "You removed public link for calendar {calendar}" : "Poistit julkisen linkin kalenterilta {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} jakoi kalenterin {calendar} kanssasi",
- "You shared calendar {calendar} with {user}" : "Jaoit kalenterin {calendar} käyttäjälle {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} jakoi kalenterin {calendar} käyttäjälle {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} lopetti kalenterin {calendar} jakamisen kanssasi",
- "You unshared calendar {calendar} from {user}" : "Lopetit kalenterin {calendar} jakamisen käyttäjälle {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} lopetti kalenterin {calendar} jakamisen käyttäjälle {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} lopetti kalenterin {calendar} jakamisen itselleen",
- "You shared calendar {calendar} with group {group}" : "Jaoit kalenterin {calendar} ryhmälle {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} jakoi kalenterin {calendar} ryhmälle {group}",
- "You unshared calendar {calendar} from group {group}" : "Lopetit kalenterin {calendar} jakamisen ryhmälle {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} lopetti kalenterin {calendar} jakamisen ryhmälle {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} loi tapahtuman {event} kalenteriin {calendar}",
- "You created event {event} in calendar {calendar}" : "Loit tapahtuman {event} kalenteriin {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} poisti tapahtuman {event} kalenterista {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Poistit tapahtuman {event} kalenterista {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} päivitti tapahtuman {event} kalenteriin {calendar}",
- "You updated event {event} in calendar {calendar}" : "Päivitit tapahtuman {event} kalenteriin {calendar}",
- "Busy" : "Varattu",
- "{actor} created todo {todo} in list {calendar}" : "{actor} loi tehtävän {todo} listaan {calendar}",
- "You created todo {todo} in list {calendar}" : "Loit tehtävän {todo} listaan {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} poisti tehtävän {todo} listasta {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Poistit tehtävän {todo} listasta {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} päivitti tehtävän {todo} listassa {calendar}",
- "You updated todo {todo} in list {calendar}" : "Päivitit tehtävän {todo} listassa {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} suoritti tehtävän {todo} listasta {calendar}",
- "You solved todo {todo} in list {calendar}" : "Suoritit tehtävän {todo} listasta {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} avasi uudelleen tehtävän {todo} listassa {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Avasit uudelleen tehtävän {todo} listassa {calendar}",
- "Calendar, contacts and tasks" : "Kalenteri, yhteystiedot ja tehtävät",
- "A <strong>calendar</strong> was modified" : "<strong>Kalenteria</strong> on muokattu",
- "A calendar <strong>event</strong> was modified" : "Kalenterin <strong>tapahtumaa</strong> on muokattu",
- "A calendar <strong>todo</strong> was modified" : "Kalenterin <strong>tehtävää</strong> on muokattu",
- "Contact birthdays" : "Yhteystietojen syntymäpäivät",
- "Death of %s" : "%s kuolema",
- "Calendar:" : "Kalenteri:",
- "Date:" : "Päiväys:",
- "Where:" : "Missä:",
- "Description:" : "Kuvaus:",
- "Untitled event" : "Nimetön tapahtuma",
- "_%n year_::_%n years_" : ["%n vuosi","%n vuotta"],
- "_%n month_::_%n months_" : ["%n kuukausi","%n kuukautta"],
- "_%n day_::_%n days_" : ["%n päivä","%n päivää"],
- "_%n hour_::_%n hours_" : ["%n tunti","%n tuntia"],
- "_%n minute_::_%n minutes_" : ["%n minuutti","%n minuuttia"],
- "%s (%s ago)" : "%s (%s sitten)",
- "Calendar: %s" : "Kalenteri: %s",
- "Date: %s" : "Päiväys: %s",
- "Description: %s" : "Kuvaus: %s",
- "Where: %s" : "Missä: %s",
- "Cancelled: %1$s" : "Peruutettu: %1$s",
- "Invitation canceled" : "Kutsu peruttu",
- "Invitation updated" : "Kutsu päivitetty",
- "Invitation: %1$s" : "Kutsu: %1$s",
- "Invitation" : "Kutsu",
- "Title:" : "Otsikko:",
- "Time:" : "Aika:",
- "Location:" : "Sijainti:",
- "Link:" : "Linkki:",
- "Organizer:" : "Järjestäjä:",
- "Attendees:" : "Osallistujat:",
- "Accept" : "Hyväksy",
- "Decline" : "Kieltäydy",
- "More options …" : "Lisää valintoja…",
- "More options at %s" : "Lisää valintoja kohteessa %s",
- "Contacts" : "Yhteystiedot",
- "{actor} created address book {addressbook}" : "{actor} loi osoitekirjan {addressbook}",
- "You created address book {addressbook}" : "Loit osoitekirjan {addressbook}",
- "{actor} deleted address book {addressbook}" : "{actor} poisti osoitekirjan {addressbook}",
- "You deleted address book {addressbook}" : "Sinä poistit osoitekirjan {addressbook}",
- "{actor} updated address book {addressbook}" : "{actor} päivitti osoitekirjaa {addressbook}",
- "You updated address book {addressbook}" : "Sinä päivitit osoitekirjaa {addressbook}",
- "{actor} shared address book {addressbook} with you" : "{actor} jakoi osoitekirjan {addressbook} kanssasi",
- "You shared address book {addressbook} with {user}" : "Jaoit osoitekirjan {addressbook} käyttäjän {user} kanssa",
- "{actor} shared address book {addressbook} with {user}" : "{actor} jakoi osoitekirjan {addressbook} käyttäjän {user} kanssa",
- "You shared address book {addressbook} with group {group}" : "Jaoit osoitekirjan {addressbook} ryhmän {group} kanssa",
- "{actor} shared address book {addressbook} with group {group}" : "{actor} jakoi osoitekirjan {addressbook} ryhmän {group} kanssa",
- "{actor} created contact {card} in address book {addressbook}" : "{actor} loi yhteystiedon {card} osoitekirjaan {addressbook}",
- "You created contact {card} in address book {addressbook}" : "Loit yhteystiedon {card} osoitekirjaan {addressbook}",
- "{actor} deleted contact {card} from address book {addressbook}" : "{actor} poisti yhteystiedon {card} osoitekirjasta {addressbook}",
- "You deleted contact {card} from address book {addressbook}" : "Poistit yhteystiedon {card} osoitekirjasta {addressbook}",
- "{actor} updated contact {card} in address book {addressbook}" : "{actor} päivitti yhteystietoa {card} osoitekirjassa {addressbook}",
- "You updated contact {card} in address book {addressbook}" : "Päivitit yhteystiedon {card} osoitekirjassa {addressbook}",
- "A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>Yhteystietoa</strong> tai <strong>osoitekirjaa</strong> muokattiin",
- "File is not updatable: %1$s" : "Tiedosto ei ole päivitettävissä: %1$s",
- "_%n byte_::_%n bytes_" : ["%n tavu","%n tavua"],
- "Could not open file" : "Tiedoston avaaminen ei onnistunut",
- "File not found: %1$s" : "Tiedostoa ei löydy: %1$s",
- "System is in maintenance mode." : "Järjestelmä on huoltotilassa",
- "Upgrade needed" : "Päivitys tarvitaan",
- "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "%s tulee asettaa käyttämään HTTPS-yhteyttä, jotta CalDAVia ja CardDAVia voi käyttää iOSilla tai macOS:llä.",
- "Configures a CalDAV account" : "Määrittää CalDAV-tilin",
- "Configures a CardDAV account" : "Määrittää CardDAV-tilin",
- "Events" : "Tapahtumat",
- "Tasks" : "Tehtävät",
- "Untitled task" : "Nimetön tehtävä",
- "Contacts and groups" : "Yhteystiedot ja ryhmät",
- "WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV-päätepiste",
- "Availability" : "Saatavuus",
- "Time zone:" : "Aikavyöhyke:",
- "to" : "Vastaanottaja",
- "Delete slot" : "Poista aikarako",
- "No working hours set" : "Työskentelytunteja ei ole asetettu",
- "Add slot" : "Lisää aikarako",
- "Monday" : "Maanantai",
- "Tuesday" : "Tiistai",
- "Wednesday" : "Keskiviikko",
- "Thursday" : "Torstai",
- "Friday" : "Perjantai",
- "Saturday" : "Lauantai",
- "Sunday" : "Sunnuntai",
- "Save" : "Tallenna",
- "Calendar server" : "Kalenteripalvelin",
- "Send invitations to attendees" : "Lähetä kutsut osallistujille",
- "Automatically generate a birthday calendar" : "Luo syntymäpäiväkalenteri automaattisesti",
- "Birthday calendars will be generated by a background job." : "Syntymäpäiväkalenterit luodaan taustatyön toimesta.",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Sen vuoksi ne eivät ole välittömästi saatavilla käyttöönoton jälkeen, vaan ne tulevat näkyviin pienellä viiveellä.",
- "Send notifications for events" : "Lähetä ilmoitukset tapahtumista",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Asenna myös {calendarappstoreopen}kalenterisovellus{linkclose}, tai {calendardocopen}yhdistä tietokoneesi ja mobiililaitteesi synkronointiyhteyteen ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Varmista että määrität {emailopen}sähköpostipalvelimen{linkclose} asetukset oikein.",
- "There was an error updating your attendance status." : "Osallistumisesi tilaa päivittäessä tapahtui virhe.",
- "Please contact the organizer directly." : "Ota yhteys suoraan järjestäjään.",
- "Are you accepting the invitation?" : "Hyväksytkö kutsun?",
- "Tentative" : "Alustava",
- "Number of guests" : "Vieraiden määrä",
- "Comment" : "Kommentti",
- "Your attendance was updated successfully." : "Osallistumisesi päivitettiin onnistuneesti.",
- "Calendar and tasks" : "Kalenteri ja tehtävät"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/fr.js b/apps/dav/l10n/fr.js
index 8865cbb6157..10c0abec9ce 100644
--- a/apps/dav/l10n/fr.js
+++ b/apps/dav/l10n/fr.js
@@ -2,87 +2,199 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Agenda",
- "Todos" : "Tâches",
+ "Tasks" : "Tâches",
"Personal" : "Personnel",
"{actor} created calendar {calendar}" : "{actor} a créé l'agenda {calendar}",
"You created calendar {calendar}" : "Vous avez créé l'agenda {calendar}",
"{actor} deleted calendar {calendar}" : "{actor} a supprimé l'agenda {calendar}",
- "You deleted calendar {calendar}" : "Vous avez supprimé l'agenda {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} a mis à jour l'agenda {calendar}",
- "You updated calendar {calendar}" : "Vous avez mis à jour l'agenda {calendar}",
- "{actor} restored calendar {calendar}" : "{actor} a restauré l'agenda {calendar}",
- "You restored calendar {calendar}" : "Vous avez restauré l'agenda {calendar}",
- "You shared calendar {calendar} as public link" : "Vous avez partagé l'agenda {calendar} avec un lien public ",
- "You removed public link for calendar {calendar}" : "Vous avez supprimé le lien public pour l'agenda {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} a partagé l'agenda {calendar} avec vous",
- "You shared calendar {calendar} with {user}" : "Vous avez partagé l'agenda {calendar} avec {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} a partagé l'agenda {calendar} avec {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} a cessé de partager l'agenda {calendar} avec vous",
- "You unshared calendar {calendar} from {user}" : "Vous avez cessé de partager l'agenda {calendar} avec {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} a cessé de partager l'agenda {calendar} avec {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} a cessé de partager l'agenda {calendar} avec lui-même",
- "You shared calendar {calendar} with group {group}" : "Vous avez partagé l'agenda {calendar} avec le groupe {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} a partagé l'agenda {calendar} avec le groupe {group}",
- "You unshared calendar {calendar} from group {group}" : "Vous avez cessé de partager l'agenda {calendar} avec le groupe {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} a cessé de partager l'agenda {calendar} avec le groupe {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} a créé l'évènement {event} dans l'agenda {calendar}",
- "You created event {event} in calendar {calendar}" : "Vous avez créé l'évènement {event} dans l'agenda {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} a supprimé l'évènement {event} de l'agenda {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Vous avez supprimé l'évènement {event} de l'agenda {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} a mis à jour l'évènement {event} dans l'agenda {calendar}",
- "You updated event {event} in calendar {calendar}" : "Vous avez mis à jour l'évènement {event} dans l'agenda {calendar}",
- "{actor} restored event {event} of calendar {calendar}" : "{actor} a restauré l'événement {event} dans l'agenda {calendar}",
- "You restored event {event} of calendar {calendar}" : "Vous avez restauré l'événement {event} dans l'agenda {calendar}",
+ "You deleted calendar {calendar}" : "Vous avez supprimé l’agenda {calendar}",
+ "{actor} updated calendar {calendar}" : "{actor} a mis à jour l’agenda {calendar}",
+ "You updated calendar {calendar}" : "Vous avez mis à jour l’agenda {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} a restauré l’agenda {calendar}",
+ "You restored calendar {calendar}" : "Vous avez restauré l’agenda {calendar}",
+ "You shared calendar {calendar} as public link" : "Vous avez partagé l’agenda {calendar} avec un lien public ",
+ "You removed public link for calendar {calendar}" : "Vous avez supprimé le lien public pour l’agenda {calendar}",
+ "{actor} shared calendar {calendar} with you" : "{actor} a partagé l’agenda {calendar} avec vous",
+ "You shared calendar {calendar} with {user}" : "Vous avez partagé l’agenda {calendar} avec {user}",
+ "{actor} shared calendar {calendar} with {user}" : "{actor} a partagé l’agenda {calendar} avec {user}",
+ "{actor} unshared calendar {calendar} from you" : "{actor} a cessé de partager l’agenda {calendar} avec vous",
+ "You unshared calendar {calendar} from {user}" : "Vous avez cessé de partager l’agenda {calendar} avec {user}",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor} a cessé de partager l’agenda {calendar} avec {user}",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} a cessé de partager l’agenda {calendar} avec lui-même",
+ "You shared calendar {calendar} with group {group}" : "Vous avez partagé l’agenda {calendar} avec le groupe {group}",
+ "{actor} shared calendar {calendar} with group {group}" : "{actor} a partagé l’agenda {calendar} avec le groupe {group}",
+ "You unshared calendar {calendar} from group {group}" : "Vous avez cessé de partager l’agenda {calendar} avec le groupe {group}",
+ "{actor} unshared calendar {calendar} from group {group}" : "{actor} a cessé de partager l’agenda {calendar} avec le groupe {group}",
+ "Untitled event" : "Événement sans titre",
+ "{actor} created event {event} in calendar {calendar}" : "{actor} a créé l’évènement {event} dans l’agenda {calendar}",
+ "You created event {event} in calendar {calendar}" : "Vous avez créé l’évènement {event} dans l’agenda {calendar}",
+ "{actor} deleted event {event} from calendar {calendar}" : "{actor} a supprimé l’évènement {event} de l’agenda {calendar}",
+ "You deleted event {event} from calendar {calendar}" : "Vous avez supprimé l’évènement {event} de l’agenda {calendar}",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} a mis à jour l’évènement {event} dans l’agenda {calendar}",
+ "You updated event {event} in calendar {calendar}" : "Vous avez mis à jour l’évènement {event} dans l’agenda {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} a déplacé l'événement {event} du calendrier {sourceCalendar} au calendrier {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Vous avez déplacé l'événement {event} du calendrier {sourceCalendar} au calendrier {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} a restauré l’événement {event} dans l’agenda {calendar}",
+ "You restored event {event} of calendar {calendar}" : "Vous avez restauré l’événement {event} dans l’agenda {calendar}",
"Busy" : "Occupé",
- "{actor} created todo {todo} in list {calendar}" : "{actor} a créé la tâche {todo} dans la liste {calendar}",
- "You created todo {todo} in list {calendar}" : "Vous avez créé la tâche {todo} dans la liste {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} a supprimé la tâche {todo} de la liste {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Vous avez supprimé la tâche {todo} de la liste {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} a mis à jour la tâche {todo} dans la liste {calendar}",
- "You updated todo {todo} in list {calendar}" : "Vous avez mis à jour la tâche {todo} dans la liste {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} a terminé la tâche {todo} de la liste {calendar}",
- "You solved todo {todo} in list {calendar}" : "Vous avez terminé la tâche {todo} de la liste {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} a réouvert la tâche {todo} dans la liste {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Vous avez réouvert la tâche {todo} dans la liste {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} a créé la tâche {todo} dans la liste {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Vous avez créé un pense-bête {todo} dans la liste {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} a supprimé un pense-bête {todo} de la liste {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Vous avez supprimé le pense-bête {todo} de la liste {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} a mis à jour le pense-bête {todo} dans la liste {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Vous avez mis à jour le pense-bête {todo} dans la liste {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} a résolu le pense-bête {todo} dans la liste {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Vous avez résolu le pense-bête {todo} dans la liste {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} a réouvert le pense-bête {todo} dans la liste {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Vous avez réouvert le pense-bête {todo} dans la liste {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} a déplacé le pense-bête {todo} de la liste {sourceCalendar} à la liste {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Vous avez déplacé le pense-bête {todo} de la liste {sourceCalendar} à la liste {targetCalendar}",
"Calendar, contacts and tasks" : "Agenda, contacts et tâches",
"A <strong>calendar</strong> was modified" : "Un <strong>agenda</strong> a été modifié",
- "A calendar <strong>event</strong> was modified" : "Un <strong>événement</strong> de l'agenda a été modifié",
- "A calendar <strong>todo</strong> was modified" : "Une <strong>liste de tâches</strong> de l'agenda a été modifiée",
+ "A calendar <strong>event</strong> was modified" : "Un <strong>événement</strong> de l’agenda a été modifié",
+ "A calendar <strong>to-do</strong> was modified" : "Un calendrier <strong>de tâches</strong> a été modifié",
"Contact birthdays" : "Anniversaires des contacts",
"Death of %s" : "Mort de %s",
- "Calendar:" : "Agenda:",
- "Date:" : "Date:",
+ "Untitled calendar" : "Calendrier sans titre",
+ "Calendar:" : "Agenda :",
+ "Date:" : "Date :",
"Where:" : "Où :",
"Description:" : "Description :",
- "Untitled event" : "Événement sans titre",
- "_%n year_::_%n years_" : ["%n an","%n ans"],
- "_%n month_::_%n months_" : ["%n mois","%n mois"],
- "_%n day_::_%n days_" : ["%n jour","%n jours"],
- "_%n hour_::_%n hours_" : ["%n heure","%n heures"],
- "_%n minute_::_%n minutes_" : ["%n minute","%n minutes"],
+ "_%n year_::_%n years_" : ["%n an","%n ans","%n ans"],
+ "_%n month_::_%n months_" : ["%n mois","%n mois","%n mois"],
+ "_%n day_::_%n days_" : ["%n jour","%n jours","%n jours"],
+ "_%n hour_::_%n hours_" : ["%n heure","%n heures","%n heures"],
+ "_%n minute_::_%n minutes_" : ["%n minute","%n minutes","%n minutes"],
"%s (in %s)" : "%s (dans %s)",
"%s (%s ago)" : "%s (il y a %s)",
- "Calendar: %s" : "Agenda: %s",
- "Date: %s" : "Date: %s",
- "Description: %s" : "Description: %s",
+ "Calendar: %s" : "Agenda : %s",
+ "Date: %s" : "Date : %s ",
+ "Description: %s" : "Description : %s",
"Where: %s" : "Où : %s",
"%1$s via %2$s" : "%1$s via %2$s",
+ "In the past on %1$s for the entire day" : "Dans le passé toute la journée du %1$s ",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Dans une minute le %1$s pour la journée entière","Dans %n minutes le %1$s pour la journée entière","Dans %n minutes le %1$s pour la journée entière"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Dans une heure le %1$s pour la journée entière","Dans %n heures le %1$s pour la journée entière","Dans %n heures le %1$s pour la journée entière"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Dans un jour le %1$s pour la journée entière","Dans %n jours le %1$s pour la journée entière","Dans %n jours le %1$s pour la journée entière"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Dans une semaine le %1$s pour la journée entière","Dans %n semaines le %1$s pour la journée entière","Dans %n semaines le %1$s pour la journée entière"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Dans un mois toute la journée du %1$s","Dans %n mois toute la journée du %1$s","Dans %n mois toute la journée du %1$s"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Dans un an toute la journée du %1$s","Dans %n années toute la journée du %1$s","Dans %n années toute la journée du %1$s"],
+ "In the past on %1$s between %2$s - %3$s" : "Dans le passé le %1$s entre %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Dans une minute le %1$s entre %2$s - %3$s","Dans %n minutes le %1$s entre %2$s - %3$s","Dans %n minutes le %1$s entre %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Dans une heure le %1$s entre %2$s et %3$s","Dans %n heures le %1$s entre %2$s et %3$s","Dans %n heures le %1$s entre %2$s et %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Demain le %1$s entre %2$s et %3$s","Dans %n jours le %1$s entre %2$s et %3$s","Dans %n jours le %1$s entre %2$s et %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["La semaine prochaine le %1$s entre %2$s et %3$s","Dans %n semaines le %1$s entre %2$s et %3$s","Dans %n semaines le %1$s entre %2$s et %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Dans un mois le %1$s entre %2$s et %3$s","Dans %n mois le %1$s entre %2$s et %3$s","Dans %n mois le %1$s entre %2$s et %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["L'an prochain le %1$s entre %2$s et %3$s","Dans %n ans le %1$s entre %2$s et %3$s","Dans %n ans le %1$s entre %2$s et %3$s"],
+ "Could not generate when statement" : "Impossible de déterminer quand",
+ "Every Day for the entire day" : "Chaque jour pour toute la journée",
+ "Every Day for the entire day until %1$s" : "Chaque jour pour toute la journée jusqu'au %1$s",
+ "Every Day between %1$s - %2$s" : "Chaque jour entre %1$s et %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Chaque jour entre %1$s et %2$s jusqu'au %3$s",
+ "Every %1$d Days for the entire day" : "Tous les %1$d jours pour la journée entière",
+ "Every %1$d Days for the entire day until %2$s" : "Tous les %1$d jours pour la journée entière jusqu'au %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Tous les %1$d jours entre %2$s et %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Tous les %1$d jours entre %2$s et %3$s jusqu'au %4$s",
+ "Could not generate event recurrence statement" : "Impossible de générer la phrase de récurrence de l'événement",
+ "Every Week on %1$s for the entire day" : "Chaque semaine le %1$s pour la journée entière",
+ "Every Week on %1$s for the entire day until %2$s" : "Chaque semaine le %1$s pour la journée entière jusqu'au%2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Chaque semaine le %1$s entre %2$s et %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Chaque semaine le %1$s entre %2$s et %3$s jusqu'au %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Toutes les %1$d semaines le %2$s pour la journée entière",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Toutes les %1$d semaines le %2$s pour la journée entière jusqu'au %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Toutes les %1$d semaines le %2$s entre %3$s et %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Toutes les %1$d semaines le %2$s entre %3$s et %4$s jusqu'au %5$s",
+ "Every Month on the %1$s for the entire day" : "Chaque mois le %1$s pour la journée entière",
+ "Every Month on the %1$s for the entire day until %2$s" : "Chaque mois le %1$s pour la journée entière jusqu'au %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Chaque mois le %1$s entre %2$s et %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Chaque mois le %1$s entre %2$s et %3$s jusqu'au %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Tous les %1$d mois le %2$s pour la journée entière",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Tous les %1$d mois le %2$s pour la journée entière jusqu'au %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Tous les %1$d mois le %2$s entre %3$s et %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Tous les %1$d mois le %2$s entre %3$s et %4$s jusqu'au %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Chaque année en %1$s le %2$s pour la journée entière",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Chaque année en %1$s le %2$s pour la journée entière jusqu'au %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Chaque année en %1$s le %2$s entre %3$s et %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Chaque année en %1$s le %2$s entre %3$s et %4$s jusqu'au %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Tous les %1$d ans en %2$s le %3$s pour la journée entière",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Tous les %1$d ans en %2$s le %3$s pour la journée entière jusqu'au%4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Tous les %1$d ans en %2$s le %3$s entre %4$s et %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Tous les %1$d ans en %2$s le %3$s entre %4$s - %5$s jusqu'au %6$s",
+ "On specific dates for the entire day until %1$s" : "À une date spécifique pour la journée entière jusqu'au %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "À des dates spécifiques entre %1$s et %2$s jusqu'au %3$s",
+ "In the past on %1$s" : "Dans le passé sur %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Dans une minute sur %1$s","Dans %n minutes sur %1$s","Dans %n minutes sur %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Dans une heure sur %1$s","Dans %n heures sur %1$s","Dans %n sur %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Demain le %1$s","Dans %n jours le %1$s","Dans %n jours le %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["La semaine prochaine le %1$s","Dans %n semaines le %1$s","Dans %n semaines le %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Le mois prochain le %1$s","Dans %n mois le %1$s","Dans %n mois le %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["L'an prochain le %1$s","Dans %n ans le %1$s","Dans %n ans le %1$s"],
+ "In the past on %1$s then on %2$s" : "Dans le passé le %1$s puis le %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Dans une minute le %1$s puis le %2$s","Dans %n minutes le %1$s puis le %2$s","Dans %n minutes le %1$s puis le %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Dans une heure le %1$s puis le %2$s","Dans %n heures le %1$s puis le %2$s","Dans %n heures le %1$s puis le %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Demain le %1$s puis le %2$s","Dans %n jours le %1$s puis le %2$s","Dans %n jours le %1$s puis le %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["La semaine prochaine le %1$s puis le %2$s","Dans %n semaines le %1$s puis le %2$s","Dans %n semaines le %1$s puis le %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Le mois prochain le %1$s puis le %2$s","Dans %n mois le %1$s puis le %2$s","Dans %n mois le %1$s puis le %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["L'an prochain le %1$s puis le %2$s","Dans %n ans le %1$s puis le %2$s","Dans %n ans le %1$s puis le %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "Dans le passé le %1$s puis le %2$s et %3$s",
+ "_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_" : ["Dans une minute le %1$s puis le %2$s et %3$s","Dans %n minutes le %1$s puis le %2$s et %3$s","Dans %n minutes le %1$s puis le %2$s et %3$s"],
+ "_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_" : ["Dans une heure le %1$s puis le %2$s et %3$s","Dans %n heures le %1$s puis le %2$s et %3$s","Dans %n heures le %1$s puis le %2$s et %3$s"],
+ "_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_" : ["Demain le %1$s puis le %2$s et %3$s","Dans %n jours le %1$s puis le %2$s et %3$s","Dans %n jours le %1$s puis le %2$s et %3$s"],
+ "_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_" : ["La semaine prochaine le %1$s puis le %2$s et %3$s","Dans %n semaines le %1$s puis le %2$s et %3$s","Dans %n semaines le %1$s puis le %2$s et %3$s"],
+ "_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_" : ["Dans un mois le %1$s puis le %2$s et %3$s","Dans %n mois le %1$s puis le %2$s et %3$s","Dans %n mois le %1$s puis le %2$s et %3$s"],
+ "_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_" : ["Dans un an sur %1$spuis sur %2$s et %3$s","Dans %n années sur %1$s puis sur %2$s et %3$s","Dans %n années sur %1$s puis sur %2$s et %3$s"],
+ "Could not generate next recurrence statement" : "Impossible de déterminer la prochaine récurrence",
"Cancelled: %1$s" : "Annulé : %1$s",
- "Invitation canceled" : "Invitation annulée",
+ "\"%1$s\" has been canceled" : "\"%1$s\" a été annulé(e)",
"Re: %1$s" : "Re : %1$s",
- "Invitation updated" : "Invitation mise à jour",
+ "%1$s has accepted your invitation" : "%1$s a accepté votre invitation",
+ "%1$s has tentatively accepted your invitation" : "%1$s a provisoirement accepté votre invitation",
+ "%1$s has declined your invitation" : "%1$s a décliné votre invitation",
+ "%1$s has responded to your invitation" : "%1$s a répondu à votre invitation",
+ "Invitation updated: %1$s" : "Invitation mise à jour : %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s a mis à jour l'évènement %2$s",
"Invitation: %1$s" : "Invitation : %1$s",
- "Invitation" : "Invitation",
- "Title:" : "Titre :",
- "Time:" : "Heure :",
- "Location:" : "Localisation :",
- "Link:" : "Lien :",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s souhaite vous inviter à \"%2$s\"",
"Organizer:" : "Organisateur :",
"Attendees:" : "Participants :",
+ "Title:" : "Titre :",
+ "When:" : "Quand :",
+ "Location:" : "Lieu :",
+ "Link:" : "Lien :",
+ "Occurring:" : "Ayant lieu : ",
"Accept" : "Accepter",
- "Decline" : "Refuser",
- "More options …" : "Plus d'options …",
+ "Decline" : "Décliner",
+ "More options …" : "Plus d'options…",
"More options at %s" : "Plus d'options à %s",
+ "Monday" : "Lundi",
+ "Tuesday" : "Mardi",
+ "Wednesday" : "Mercredi",
+ "Thursday" : "Jeudi",
+ "Friday" : "Vendredi",
+ "Saturday" : "Samedi",
+ "Sunday" : "Dimanche",
+ "January" : "Janvier",
+ "February" : "Février",
+ "March" : "Mars",
+ "April" : "Avril",
+ "May" : "Mai",
+ "June" : "Juin",
+ "July" : "Juillet",
+ "August" : "Août",
+ "September" : "Septembre",
+ "October" : "Octobre",
+ "November" : "Novembre",
+ "December" : "Décembre",
+ "First" : "Première",
+ "Second" : "Deuxième",
+ "Third" : "Troisième",
+ "Fourth" : "Quatrième",
+ "Fifth" : "Cinquième",
+ "Last" : "Dernièr",
+ "Second Last" : "Avant-dernier",
+ "Third Last" : "Troisième dernier",
+ "Fourth Last" : "Quatrième dernier",
+ "Fifth Last" : "Cinquième dernier",
"Contacts" : "Contacts",
"{actor} created address book {addressbook}" : "{actor} a créé le carnet d'adresses {addressbook}",
"You created address book {addressbook}" : "Vous avez créé le carnet d'adresses {addressbook}",
@@ -108,70 +220,118 @@ OC.L10N.register(
"{actor} updated contact {card} in address book {addressbook}" : "{actor} a mis à jour le contact {card} dans le carnet d'adresses {addressbook}",
"You updated contact {card} in address book {addressbook}" : "Vous avez mis à jour le contact {card} dans le carnet d'adresses {addressbook}",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "Un <strong>contact</strong> ou <strong>carnet d'adresses</strong> a été modifié",
+ "Accounts" : "Comptes",
+ "System address book which holds all accounts" : "Carnet d'adresses système qui regroupe tous les comptes",
"File is not updatable: %1$s" : "Ce fichier ne peut pas être mis à jour : %1$s",
+ "Failed to get storage for file" : "Impossible d'obtenir l'espace de stockage pour le fichier",
"Could not write to final file, canceled by hook" : "Impossible d'écrire dans le fichier final, annulé par le hook",
"Could not write file contents" : "Impossible d'écrire le contenu du fichier",
- "_%n byte_::_%n bytes_" : ["%n octet","%n octets"],
+ "_%n byte_::_%n bytes_" : ["%n octet","%n octets","%n octets"],
"Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Erreur en copiant le fichier à destination (copié : %1$s, taille du fichier attendue : %2$s)",
"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." : "Taille du fichier attendue : %1$s mais taille du fichier lue (depuis le client Nextcloud) et écrit (dans le stockage Nextcloud) : %2$s. Cela peut être un problème de réseau au niveau du client ou un problème de stockage au niveau du serveur.",
"Could not rename part file to final file, canceled by hook" : "Impossible de renommer le fichier partiel en fichier final, annulé par le hook",
"Could not rename part file to final file" : "Impossible de renommer le fichier partiel en fichier définitif.",
"Failed to check file size: %1$s" : "Échec à la vérification de la taille du fichier : %1$s",
- "Could not open file" : "Impossible d'ouvrir le fichier",
+ "Could not open file: %1$s, file does seem to exist" : "Impossible d'ouvrir le fichier %1$s, le fichier semble présent.",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Impossible d'ouvrir le fichier %1$s, le fichier ne semble pas présent.",
"Encryption not ready: %1$s" : "Encryption pas prête : %1$s",
"Failed to open file: %1$s" : "Échec à l'ouverture du fichier : %1$s",
"Failed to unlink: %1$s" : "Échec à la suppression :%1$s",
- "Invalid chunk name" : "Nom de morceau invalide",
- "Could not rename part file assembled from chunks" : "Impossible de renommer le fichier partiel à partir des morceaux",
"Failed to write file contents: %1$s" : "Échec à l'écriture du contenu du fichier : %1$s",
"File not found: %1$s" : "Fichier non trouvé : %1$s",
+ "Invalid target path" : "Chemin d’accès invalide",
"System is in maintenance mode." : "Le système est en mode maintenance.",
"Upgrade needed" : "Mise à jour requise",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Votre %s a besoin d'être configuré pour utiliser le HTTPS dans le but d'utiliser CalDAV et CardDAV avec iOS/macOS.",
"Configures a CalDAV account" : "Configure un compte CalDAV",
"Configures a CardDAV account" : "Configure un compte CardDAV",
"Events" : "Évènements",
- "Tasks" : "Tâches",
"Untitled task" : "Tâche sans titre",
"Completed on %s" : "Terminé le %s",
"Due on %s by %s" : "Echéance le %s pour %s",
"Due on %s" : "Echéance le %s",
+ "Example event - open me!" : "Exemple d'événement - ouvrez-moi !",
+ "System Address Book" : "Carnet d'adresses du système",
+ "The system address book contains contact information for all users in your instance." : "Le carnet d'adresses du système contient les informations de contact de tous les utilisateurs de votre instance.",
+ "Enable System Address Book" : "Activer le carnet d'adresses du système",
+ "DAV system address book" : "Carnet d'adresses système DAV",
+ "No outstanding DAV system address book sync." : "Pas de synchronisation DAV en cours du carnet d'adresses système.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "La synchronisation du carnet d'adresses système DAV n'a pas encore été effectuée car votre instance a plus de 1 000 utilisateurs ou parce qu'une erreur est survenue. Merci de l'exécuter manuellement en tapant la commande \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "Point de terminaison WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Impossible de vérifier si votre serveur web est correctement configuré pour permettre la synchronisation de fichiers via WebDAV. Veuillez vérifier manuellement.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Votre serveur web n’est pas encore correctement configuré pour la synchronisation de fichiers parce que l’interface WebDAV semble ne pas fonctionner.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Votre serveur web est correctement configuré pour permettre la synchronisation de fichiers via WebDAV.",
"Migrated calendar (%1$s)" : "Agenda migré (%1$s)",
+ "Calendars including events, details and attendees" : "Calendriers incluant des événements, détails et participants",
"Contacts and groups" : "Contacts et groupes",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "Point d'accès WebDAV",
- "Availability" : "Disponibilité",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Si vous configurez vos heures de travail, les autres utilisateurs verront si vous êtes disponible quand ils planifient une réunion.",
+ "Absence saved" : "Message d’absence sauvegardé",
+ "Failed to save your absence settings" : "L'enregistrement des paramètres d'absence a échoué",
+ "Absence cleared" : "Message d’absence supprimé",
+ "Failed to clear your absence settings" : "Les paramètres d'absence n'ont pas été effacés",
+ "First day" : "Premier jour",
+ "Last day (inclusive)" : "Dernier jour (inclus)",
+ "Out of office replacement (optional)" : "Remplaçant pendant l'absence (optionnel)",
+ "Name of the replacement" : "Nom du remplaçant",
+ "No results." : "Pas de résultat.",
+ "Start typing." : "Commencez à taper.",
+ "Short absence status" : "Résumé du message d’absence",
+ "Long absence Message" : "Message d’absence complet",
+ "Save" : "Enregistrer",
+ "Disable absence" : "Désactiver le message d’absence",
+ "Failed to load availability" : "Impossible de charger les disponibilités",
+ "Saved availability" : "Disponibilités sauvegardées",
+ "Failed to save availability" : "Impossible de sauvegarder les disponibilités",
"Time zone:" : "Fuseau horaire :",
"to" : "à",
"Delete slot" : "Supprimer le créneau",
"No working hours set" : "Heures de travail non définies",
"Add slot" : "Ajouter un créneau",
- "Monday" : "Lundi",
- "Tuesday" : "Mardi",
- "Wednesday" : "Mercredi",
- "Thursday" : "Jeudi",
- "Friday" : "Vendredi",
- "Saturday" : "Samedi",
- "Sunday" : "Dimanche",
- "Save" : "Enregistrer",
+ "Weekdays" : "Jours de la semaine",
+ "Pick a start time for {dayName}" : "Choisissez une heure de début pour {dayName}",
+ "Pick a end time for {dayName}" : "Choisissez une heure de fin pour {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Définir automatiquement le statut « Ne pas déranger » en dehors des heures de disponibilités pour désactiver toutes les notifications.",
+ "Cancel" : "Annuler",
+ "Import" : "Importation",
+ "Error while saving settings" : "Erreur lors de l'enregistrement des paramètres",
+ "Contact reset successfully" : "Contact réinitialisé avec succès",
+ "Error while resetting contact" : "Erreur lors de la remise à zéro du contact",
+ "Contact imported successfully" : "Contact importé avec succès",
+ "Error while importing contact" : "Erreur lors de l'importation du contact",
+ "Import contact" : "Importer un contact",
+ "Reset to default" : "Restaurer les valeurs par défaut",
+ "Import contacts" : "Importer des contacts",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Importer un nouveau fichier .vcf supprimera le contact par défaut existant et le remplacera. Voulez-vous continuer ?",
+ "Failed to save example event creation setting" : "Échec de la sauvegarde du paramètre de l'exemple de création d'événement",
+ "Failed to upload the example event" : "Échec du téléversement de l'exemple d'événement",
+ "Custom example event was saved successfully" : "L'exemple d'événement personnalisé a bien été enregistré",
+ "Failed to delete the custom example event" : "Échec de la suppression de l'exemple d'événement personnalisé ",
+ "Custom example event was deleted successfully" : "L'exemple de contenu personnalisé a bien été supprimé",
+ "Import calendar event" : "Importer l'événement du calendrier",
+ "Uploading a new event will overwrite the existing one." : "Charger un nouvel événement qui va remplacer l'actuel.",
+ "Upload event" : "Téléverser un événement",
+ "Availability" : "Disponibilités",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Si vous configurez vos heures de travail, les autres personnes verront si vous êtes disponible quand ils planifient une réunion.",
+ "Absence" : "Absence",
+ "Configure your next absence period." : "Configurez votre prochaine période d'absence.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installer aussi {calendarappstoreopen}l'application Calendrier{linkclose}, ou {calendardocopen}connecter à votre PC & téléphone pour synchroniser ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Merci de vérifier d'avoir correctement configuré {emailopen}le serveur de messagerie{linkclose}.",
"Calendar server" : "Serveur de calendrier",
"Send invitations to attendees" : "Envoyer des invitations aux participants",
- "Automatically generate a birthday calendar" : "Générer automatiquement un agenda d'anniversaire",
- "Birthday calendars will be generated by a background job." : "Les agendas d'anniversaire seront générés par une tâche de fond.",
+ "Automatically generate a birthday calendar" : "Générer automatiquement un agenda d’anniversaire",
+ "Birthday calendars will be generated by a background job." : "Les agendas d’anniversaire seront générés par une tâche en arrière-plan.",
"Hence they will not be available immediately after enabling but will show up after some time." : "Par conséquent, ils ne seront pas disponibles immédiatement après l'activation mais apparaîtront après un certain temps.",
"Send notifications for events" : "Envoyer une notification pour les évènements",
"Notifications are sent via background jobs, so these must occur often enough." : "Les notifications sont envoyées par des tâches de fond qui doivent, par conséquent, être exécutées régulièrement.",
+ "Send reminder notifications to calendar sharees as well" : "Envoyez également des notifications de rappel aux personnes partageant le calendrier",
+ "Reminders are always sent to organizers and attendees." : "Des rappels sont toujours envoyés aux organisateurs et aux participants.",
"Enable notifications for events via push" : "Activer les notifications push pour les évènements",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installer aussi {calendarappstoreopen}l'application Calendrier{linkclose}, ou {calendardocopen}connecter à votre PC & téléphone pour synchroniser ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Merci de vérifier d'avoir correctement configuré {emailopen}le serveur de courriel{linkclose}.",
+ "Example content" : "Exemple de contenu",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Les exemples de contenu servent à présenter les fonctionnalités de Nextcloud. Le contenu par défaut est fourni avec Nextcloud et peut être remplacé par du contenu personnalisé.",
"There was an error updating your attendance status." : "Une erreur s'est produite lors de la mise à jour de votre statut de présence.",
"Please contact the organizer directly." : "Merci de contacter l'organisateur directement.",
"Are you accepting the invitation?" : "Acceptez-vous l'invitation ?",
"Tentative" : "Provisoire",
- "Number of guests" : "Nombre d'invités",
- "Comment" : "Commentaire",
- "Your attendance was updated successfully." : "Votre présence a été mise à jour avec succès.",
- "Calendar and tasks" : "Agenda et tâches"
+ "Your attendance was updated successfully." : "Votre présence a été mise à jour avec succès."
},
-"nplurals=2; plural=(n > 1);");
+"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;");
diff --git a/apps/dav/l10n/fr.json b/apps/dav/l10n/fr.json
index 48623273c79..edc99486824 100644
--- a/apps/dav/l10n/fr.json
+++ b/apps/dav/l10n/fr.json
@@ -1,86 +1,198 @@
{ "translations": {
"Calendar" : "Agenda",
- "Todos" : "Tâches",
+ "Tasks" : "Tâches",
"Personal" : "Personnel",
"{actor} created calendar {calendar}" : "{actor} a créé l'agenda {calendar}",
"You created calendar {calendar}" : "Vous avez créé l'agenda {calendar}",
"{actor} deleted calendar {calendar}" : "{actor} a supprimé l'agenda {calendar}",
- "You deleted calendar {calendar}" : "Vous avez supprimé l'agenda {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} a mis à jour l'agenda {calendar}",
- "You updated calendar {calendar}" : "Vous avez mis à jour l'agenda {calendar}",
- "{actor} restored calendar {calendar}" : "{actor} a restauré l'agenda {calendar}",
- "You restored calendar {calendar}" : "Vous avez restauré l'agenda {calendar}",
- "You shared calendar {calendar} as public link" : "Vous avez partagé l'agenda {calendar} avec un lien public ",
- "You removed public link for calendar {calendar}" : "Vous avez supprimé le lien public pour l'agenda {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} a partagé l'agenda {calendar} avec vous",
- "You shared calendar {calendar} with {user}" : "Vous avez partagé l'agenda {calendar} avec {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} a partagé l'agenda {calendar} avec {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} a cessé de partager l'agenda {calendar} avec vous",
- "You unshared calendar {calendar} from {user}" : "Vous avez cessé de partager l'agenda {calendar} avec {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} a cessé de partager l'agenda {calendar} avec {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} a cessé de partager l'agenda {calendar} avec lui-même",
- "You shared calendar {calendar} with group {group}" : "Vous avez partagé l'agenda {calendar} avec le groupe {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} a partagé l'agenda {calendar} avec le groupe {group}",
- "You unshared calendar {calendar} from group {group}" : "Vous avez cessé de partager l'agenda {calendar} avec le groupe {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} a cessé de partager l'agenda {calendar} avec le groupe {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} a créé l'évènement {event} dans l'agenda {calendar}",
- "You created event {event} in calendar {calendar}" : "Vous avez créé l'évènement {event} dans l'agenda {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} a supprimé l'évènement {event} de l'agenda {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Vous avez supprimé l'évènement {event} de l'agenda {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} a mis à jour l'évènement {event} dans l'agenda {calendar}",
- "You updated event {event} in calendar {calendar}" : "Vous avez mis à jour l'évènement {event} dans l'agenda {calendar}",
- "{actor} restored event {event} of calendar {calendar}" : "{actor} a restauré l'événement {event} dans l'agenda {calendar}",
- "You restored event {event} of calendar {calendar}" : "Vous avez restauré l'événement {event} dans l'agenda {calendar}",
+ "You deleted calendar {calendar}" : "Vous avez supprimé l’agenda {calendar}",
+ "{actor} updated calendar {calendar}" : "{actor} a mis à jour l’agenda {calendar}",
+ "You updated calendar {calendar}" : "Vous avez mis à jour l’agenda {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} a restauré l’agenda {calendar}",
+ "You restored calendar {calendar}" : "Vous avez restauré l’agenda {calendar}",
+ "You shared calendar {calendar} as public link" : "Vous avez partagé l’agenda {calendar} avec un lien public ",
+ "You removed public link for calendar {calendar}" : "Vous avez supprimé le lien public pour l’agenda {calendar}",
+ "{actor} shared calendar {calendar} with you" : "{actor} a partagé l’agenda {calendar} avec vous",
+ "You shared calendar {calendar} with {user}" : "Vous avez partagé l’agenda {calendar} avec {user}",
+ "{actor} shared calendar {calendar} with {user}" : "{actor} a partagé l’agenda {calendar} avec {user}",
+ "{actor} unshared calendar {calendar} from you" : "{actor} a cessé de partager l’agenda {calendar} avec vous",
+ "You unshared calendar {calendar} from {user}" : "Vous avez cessé de partager l’agenda {calendar} avec {user}",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor} a cessé de partager l’agenda {calendar} avec {user}",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} a cessé de partager l’agenda {calendar} avec lui-même",
+ "You shared calendar {calendar} with group {group}" : "Vous avez partagé l’agenda {calendar} avec le groupe {group}",
+ "{actor} shared calendar {calendar} with group {group}" : "{actor} a partagé l’agenda {calendar} avec le groupe {group}",
+ "You unshared calendar {calendar} from group {group}" : "Vous avez cessé de partager l’agenda {calendar} avec le groupe {group}",
+ "{actor} unshared calendar {calendar} from group {group}" : "{actor} a cessé de partager l’agenda {calendar} avec le groupe {group}",
+ "Untitled event" : "Événement sans titre",
+ "{actor} created event {event} in calendar {calendar}" : "{actor} a créé l’évènement {event} dans l’agenda {calendar}",
+ "You created event {event} in calendar {calendar}" : "Vous avez créé l’évènement {event} dans l’agenda {calendar}",
+ "{actor} deleted event {event} from calendar {calendar}" : "{actor} a supprimé l’évènement {event} de l’agenda {calendar}",
+ "You deleted event {event} from calendar {calendar}" : "Vous avez supprimé l’évènement {event} de l’agenda {calendar}",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} a mis à jour l’évènement {event} dans l’agenda {calendar}",
+ "You updated event {event} in calendar {calendar}" : "Vous avez mis à jour l’évènement {event} dans l’agenda {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} a déplacé l'événement {event} du calendrier {sourceCalendar} au calendrier {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Vous avez déplacé l'événement {event} du calendrier {sourceCalendar} au calendrier {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} a restauré l’événement {event} dans l’agenda {calendar}",
+ "You restored event {event} of calendar {calendar}" : "Vous avez restauré l’événement {event} dans l’agenda {calendar}",
"Busy" : "Occupé",
- "{actor} created todo {todo} in list {calendar}" : "{actor} a créé la tâche {todo} dans la liste {calendar}",
- "You created todo {todo} in list {calendar}" : "Vous avez créé la tâche {todo} dans la liste {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} a supprimé la tâche {todo} de la liste {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Vous avez supprimé la tâche {todo} de la liste {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} a mis à jour la tâche {todo} dans la liste {calendar}",
- "You updated todo {todo} in list {calendar}" : "Vous avez mis à jour la tâche {todo} dans la liste {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} a terminé la tâche {todo} de la liste {calendar}",
- "You solved todo {todo} in list {calendar}" : "Vous avez terminé la tâche {todo} de la liste {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} a réouvert la tâche {todo} dans la liste {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Vous avez réouvert la tâche {todo} dans la liste {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} a créé la tâche {todo} dans la liste {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Vous avez créé un pense-bête {todo} dans la liste {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} a supprimé un pense-bête {todo} de la liste {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Vous avez supprimé le pense-bête {todo} de la liste {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} a mis à jour le pense-bête {todo} dans la liste {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Vous avez mis à jour le pense-bête {todo} dans la liste {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} a résolu le pense-bête {todo} dans la liste {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Vous avez résolu le pense-bête {todo} dans la liste {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} a réouvert le pense-bête {todo} dans la liste {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Vous avez réouvert le pense-bête {todo} dans la liste {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} a déplacé le pense-bête {todo} de la liste {sourceCalendar} à la liste {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Vous avez déplacé le pense-bête {todo} de la liste {sourceCalendar} à la liste {targetCalendar}",
"Calendar, contacts and tasks" : "Agenda, contacts et tâches",
"A <strong>calendar</strong> was modified" : "Un <strong>agenda</strong> a été modifié",
- "A calendar <strong>event</strong> was modified" : "Un <strong>événement</strong> de l'agenda a été modifié",
- "A calendar <strong>todo</strong> was modified" : "Une <strong>liste de tâches</strong> de l'agenda a été modifiée",
+ "A calendar <strong>event</strong> was modified" : "Un <strong>événement</strong> de l’agenda a été modifié",
+ "A calendar <strong>to-do</strong> was modified" : "Un calendrier <strong>de tâches</strong> a été modifié",
"Contact birthdays" : "Anniversaires des contacts",
"Death of %s" : "Mort de %s",
- "Calendar:" : "Agenda:",
- "Date:" : "Date:",
+ "Untitled calendar" : "Calendrier sans titre",
+ "Calendar:" : "Agenda :",
+ "Date:" : "Date :",
"Where:" : "Où :",
"Description:" : "Description :",
- "Untitled event" : "Événement sans titre",
- "_%n year_::_%n years_" : ["%n an","%n ans"],
- "_%n month_::_%n months_" : ["%n mois","%n mois"],
- "_%n day_::_%n days_" : ["%n jour","%n jours"],
- "_%n hour_::_%n hours_" : ["%n heure","%n heures"],
- "_%n minute_::_%n minutes_" : ["%n minute","%n minutes"],
+ "_%n year_::_%n years_" : ["%n an","%n ans","%n ans"],
+ "_%n month_::_%n months_" : ["%n mois","%n mois","%n mois"],
+ "_%n day_::_%n days_" : ["%n jour","%n jours","%n jours"],
+ "_%n hour_::_%n hours_" : ["%n heure","%n heures","%n heures"],
+ "_%n minute_::_%n minutes_" : ["%n minute","%n minutes","%n minutes"],
"%s (in %s)" : "%s (dans %s)",
"%s (%s ago)" : "%s (il y a %s)",
- "Calendar: %s" : "Agenda: %s",
- "Date: %s" : "Date: %s",
- "Description: %s" : "Description: %s",
+ "Calendar: %s" : "Agenda : %s",
+ "Date: %s" : "Date : %s ",
+ "Description: %s" : "Description : %s",
"Where: %s" : "Où : %s",
"%1$s via %2$s" : "%1$s via %2$s",
+ "In the past on %1$s for the entire day" : "Dans le passé toute la journée du %1$s ",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Dans une minute le %1$s pour la journée entière","Dans %n minutes le %1$s pour la journée entière","Dans %n minutes le %1$s pour la journée entière"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Dans une heure le %1$s pour la journée entière","Dans %n heures le %1$s pour la journée entière","Dans %n heures le %1$s pour la journée entière"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Dans un jour le %1$s pour la journée entière","Dans %n jours le %1$s pour la journée entière","Dans %n jours le %1$s pour la journée entière"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Dans une semaine le %1$s pour la journée entière","Dans %n semaines le %1$s pour la journée entière","Dans %n semaines le %1$s pour la journée entière"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Dans un mois toute la journée du %1$s","Dans %n mois toute la journée du %1$s","Dans %n mois toute la journée du %1$s"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Dans un an toute la journée du %1$s","Dans %n années toute la journée du %1$s","Dans %n années toute la journée du %1$s"],
+ "In the past on %1$s between %2$s - %3$s" : "Dans le passé le %1$s entre %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Dans une minute le %1$s entre %2$s - %3$s","Dans %n minutes le %1$s entre %2$s - %3$s","Dans %n minutes le %1$s entre %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Dans une heure le %1$s entre %2$s et %3$s","Dans %n heures le %1$s entre %2$s et %3$s","Dans %n heures le %1$s entre %2$s et %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Demain le %1$s entre %2$s et %3$s","Dans %n jours le %1$s entre %2$s et %3$s","Dans %n jours le %1$s entre %2$s et %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["La semaine prochaine le %1$s entre %2$s et %3$s","Dans %n semaines le %1$s entre %2$s et %3$s","Dans %n semaines le %1$s entre %2$s et %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Dans un mois le %1$s entre %2$s et %3$s","Dans %n mois le %1$s entre %2$s et %3$s","Dans %n mois le %1$s entre %2$s et %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["L'an prochain le %1$s entre %2$s et %3$s","Dans %n ans le %1$s entre %2$s et %3$s","Dans %n ans le %1$s entre %2$s et %3$s"],
+ "Could not generate when statement" : "Impossible de déterminer quand",
+ "Every Day for the entire day" : "Chaque jour pour toute la journée",
+ "Every Day for the entire day until %1$s" : "Chaque jour pour toute la journée jusqu'au %1$s",
+ "Every Day between %1$s - %2$s" : "Chaque jour entre %1$s et %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Chaque jour entre %1$s et %2$s jusqu'au %3$s",
+ "Every %1$d Days for the entire day" : "Tous les %1$d jours pour la journée entière",
+ "Every %1$d Days for the entire day until %2$s" : "Tous les %1$d jours pour la journée entière jusqu'au %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Tous les %1$d jours entre %2$s et %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Tous les %1$d jours entre %2$s et %3$s jusqu'au %4$s",
+ "Could not generate event recurrence statement" : "Impossible de générer la phrase de récurrence de l'événement",
+ "Every Week on %1$s for the entire day" : "Chaque semaine le %1$s pour la journée entière",
+ "Every Week on %1$s for the entire day until %2$s" : "Chaque semaine le %1$s pour la journée entière jusqu'au%2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Chaque semaine le %1$s entre %2$s et %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Chaque semaine le %1$s entre %2$s et %3$s jusqu'au %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Toutes les %1$d semaines le %2$s pour la journée entière",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Toutes les %1$d semaines le %2$s pour la journée entière jusqu'au %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Toutes les %1$d semaines le %2$s entre %3$s et %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Toutes les %1$d semaines le %2$s entre %3$s et %4$s jusqu'au %5$s",
+ "Every Month on the %1$s for the entire day" : "Chaque mois le %1$s pour la journée entière",
+ "Every Month on the %1$s for the entire day until %2$s" : "Chaque mois le %1$s pour la journée entière jusqu'au %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Chaque mois le %1$s entre %2$s et %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Chaque mois le %1$s entre %2$s et %3$s jusqu'au %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Tous les %1$d mois le %2$s pour la journée entière",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Tous les %1$d mois le %2$s pour la journée entière jusqu'au %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Tous les %1$d mois le %2$s entre %3$s et %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Tous les %1$d mois le %2$s entre %3$s et %4$s jusqu'au %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Chaque année en %1$s le %2$s pour la journée entière",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Chaque année en %1$s le %2$s pour la journée entière jusqu'au %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Chaque année en %1$s le %2$s entre %3$s et %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Chaque année en %1$s le %2$s entre %3$s et %4$s jusqu'au %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Tous les %1$d ans en %2$s le %3$s pour la journée entière",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Tous les %1$d ans en %2$s le %3$s pour la journée entière jusqu'au%4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Tous les %1$d ans en %2$s le %3$s entre %4$s et %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Tous les %1$d ans en %2$s le %3$s entre %4$s - %5$s jusqu'au %6$s",
+ "On specific dates for the entire day until %1$s" : "À une date spécifique pour la journée entière jusqu'au %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "À des dates spécifiques entre %1$s et %2$s jusqu'au %3$s",
+ "In the past on %1$s" : "Dans le passé sur %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Dans une minute sur %1$s","Dans %n minutes sur %1$s","Dans %n minutes sur %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Dans une heure sur %1$s","Dans %n heures sur %1$s","Dans %n sur %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Demain le %1$s","Dans %n jours le %1$s","Dans %n jours le %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["La semaine prochaine le %1$s","Dans %n semaines le %1$s","Dans %n semaines le %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Le mois prochain le %1$s","Dans %n mois le %1$s","Dans %n mois le %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["L'an prochain le %1$s","Dans %n ans le %1$s","Dans %n ans le %1$s"],
+ "In the past on %1$s then on %2$s" : "Dans le passé le %1$s puis le %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Dans une minute le %1$s puis le %2$s","Dans %n minutes le %1$s puis le %2$s","Dans %n minutes le %1$s puis le %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Dans une heure le %1$s puis le %2$s","Dans %n heures le %1$s puis le %2$s","Dans %n heures le %1$s puis le %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Demain le %1$s puis le %2$s","Dans %n jours le %1$s puis le %2$s","Dans %n jours le %1$s puis le %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["La semaine prochaine le %1$s puis le %2$s","Dans %n semaines le %1$s puis le %2$s","Dans %n semaines le %1$s puis le %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Le mois prochain le %1$s puis le %2$s","Dans %n mois le %1$s puis le %2$s","Dans %n mois le %1$s puis le %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["L'an prochain le %1$s puis le %2$s","Dans %n ans le %1$s puis le %2$s","Dans %n ans le %1$s puis le %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "Dans le passé le %1$s puis le %2$s et %3$s",
+ "_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_" : ["Dans une minute le %1$s puis le %2$s et %3$s","Dans %n minutes le %1$s puis le %2$s et %3$s","Dans %n minutes le %1$s puis le %2$s et %3$s"],
+ "_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_" : ["Dans une heure le %1$s puis le %2$s et %3$s","Dans %n heures le %1$s puis le %2$s et %3$s","Dans %n heures le %1$s puis le %2$s et %3$s"],
+ "_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_" : ["Demain le %1$s puis le %2$s et %3$s","Dans %n jours le %1$s puis le %2$s et %3$s","Dans %n jours le %1$s puis le %2$s et %3$s"],
+ "_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_" : ["La semaine prochaine le %1$s puis le %2$s et %3$s","Dans %n semaines le %1$s puis le %2$s et %3$s","Dans %n semaines le %1$s puis le %2$s et %3$s"],
+ "_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_" : ["Dans un mois le %1$s puis le %2$s et %3$s","Dans %n mois le %1$s puis le %2$s et %3$s","Dans %n mois le %1$s puis le %2$s et %3$s"],
+ "_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_" : ["Dans un an sur %1$spuis sur %2$s et %3$s","Dans %n années sur %1$s puis sur %2$s et %3$s","Dans %n années sur %1$s puis sur %2$s et %3$s"],
+ "Could not generate next recurrence statement" : "Impossible de déterminer la prochaine récurrence",
"Cancelled: %1$s" : "Annulé : %1$s",
- "Invitation canceled" : "Invitation annulée",
+ "\"%1$s\" has been canceled" : "\"%1$s\" a été annulé(e)",
"Re: %1$s" : "Re : %1$s",
- "Invitation updated" : "Invitation mise à jour",
+ "%1$s has accepted your invitation" : "%1$s a accepté votre invitation",
+ "%1$s has tentatively accepted your invitation" : "%1$s a provisoirement accepté votre invitation",
+ "%1$s has declined your invitation" : "%1$s a décliné votre invitation",
+ "%1$s has responded to your invitation" : "%1$s a répondu à votre invitation",
+ "Invitation updated: %1$s" : "Invitation mise à jour : %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s a mis à jour l'évènement %2$s",
"Invitation: %1$s" : "Invitation : %1$s",
- "Invitation" : "Invitation",
- "Title:" : "Titre :",
- "Time:" : "Heure :",
- "Location:" : "Localisation :",
- "Link:" : "Lien :",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s souhaite vous inviter à \"%2$s\"",
"Organizer:" : "Organisateur :",
"Attendees:" : "Participants :",
+ "Title:" : "Titre :",
+ "When:" : "Quand :",
+ "Location:" : "Lieu :",
+ "Link:" : "Lien :",
+ "Occurring:" : "Ayant lieu : ",
"Accept" : "Accepter",
- "Decline" : "Refuser",
- "More options …" : "Plus d'options …",
+ "Decline" : "Décliner",
+ "More options …" : "Plus d'options…",
"More options at %s" : "Plus d'options à %s",
+ "Monday" : "Lundi",
+ "Tuesday" : "Mardi",
+ "Wednesday" : "Mercredi",
+ "Thursday" : "Jeudi",
+ "Friday" : "Vendredi",
+ "Saturday" : "Samedi",
+ "Sunday" : "Dimanche",
+ "January" : "Janvier",
+ "February" : "Février",
+ "March" : "Mars",
+ "April" : "Avril",
+ "May" : "Mai",
+ "June" : "Juin",
+ "July" : "Juillet",
+ "August" : "Août",
+ "September" : "Septembre",
+ "October" : "Octobre",
+ "November" : "Novembre",
+ "December" : "Décembre",
+ "First" : "Première",
+ "Second" : "Deuxième",
+ "Third" : "Troisième",
+ "Fourth" : "Quatrième",
+ "Fifth" : "Cinquième",
+ "Last" : "Dernièr",
+ "Second Last" : "Avant-dernier",
+ "Third Last" : "Troisième dernier",
+ "Fourth Last" : "Quatrième dernier",
+ "Fifth Last" : "Cinquième dernier",
"Contacts" : "Contacts",
"{actor} created address book {addressbook}" : "{actor} a créé le carnet d'adresses {addressbook}",
"You created address book {addressbook}" : "Vous avez créé le carnet d'adresses {addressbook}",
@@ -106,70 +218,118 @@
"{actor} updated contact {card} in address book {addressbook}" : "{actor} a mis à jour le contact {card} dans le carnet d'adresses {addressbook}",
"You updated contact {card} in address book {addressbook}" : "Vous avez mis à jour le contact {card} dans le carnet d'adresses {addressbook}",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "Un <strong>contact</strong> ou <strong>carnet d'adresses</strong> a été modifié",
+ "Accounts" : "Comptes",
+ "System address book which holds all accounts" : "Carnet d'adresses système qui regroupe tous les comptes",
"File is not updatable: %1$s" : "Ce fichier ne peut pas être mis à jour : %1$s",
+ "Failed to get storage for file" : "Impossible d'obtenir l'espace de stockage pour le fichier",
"Could not write to final file, canceled by hook" : "Impossible d'écrire dans le fichier final, annulé par le hook",
"Could not write file contents" : "Impossible d'écrire le contenu du fichier",
- "_%n byte_::_%n bytes_" : ["%n octet","%n octets"],
+ "_%n byte_::_%n bytes_" : ["%n octet","%n octets","%n octets"],
"Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Erreur en copiant le fichier à destination (copié : %1$s, taille du fichier attendue : %2$s)",
"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." : "Taille du fichier attendue : %1$s mais taille du fichier lue (depuis le client Nextcloud) et écrit (dans le stockage Nextcloud) : %2$s. Cela peut être un problème de réseau au niveau du client ou un problème de stockage au niveau du serveur.",
"Could not rename part file to final file, canceled by hook" : "Impossible de renommer le fichier partiel en fichier final, annulé par le hook",
"Could not rename part file to final file" : "Impossible de renommer le fichier partiel en fichier définitif.",
"Failed to check file size: %1$s" : "Échec à la vérification de la taille du fichier : %1$s",
- "Could not open file" : "Impossible d'ouvrir le fichier",
+ "Could not open file: %1$s, file does seem to exist" : "Impossible d'ouvrir le fichier %1$s, le fichier semble présent.",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Impossible d'ouvrir le fichier %1$s, le fichier ne semble pas présent.",
"Encryption not ready: %1$s" : "Encryption pas prête : %1$s",
"Failed to open file: %1$s" : "Échec à l'ouverture du fichier : %1$s",
"Failed to unlink: %1$s" : "Échec à la suppression :%1$s",
- "Invalid chunk name" : "Nom de morceau invalide",
- "Could not rename part file assembled from chunks" : "Impossible de renommer le fichier partiel à partir des morceaux",
"Failed to write file contents: %1$s" : "Échec à l'écriture du contenu du fichier : %1$s",
"File not found: %1$s" : "Fichier non trouvé : %1$s",
+ "Invalid target path" : "Chemin d’accès invalide",
"System is in maintenance mode." : "Le système est en mode maintenance.",
"Upgrade needed" : "Mise à jour requise",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Votre %s a besoin d'être configuré pour utiliser le HTTPS dans le but d'utiliser CalDAV et CardDAV avec iOS/macOS.",
"Configures a CalDAV account" : "Configure un compte CalDAV",
"Configures a CardDAV account" : "Configure un compte CardDAV",
"Events" : "Évènements",
- "Tasks" : "Tâches",
"Untitled task" : "Tâche sans titre",
"Completed on %s" : "Terminé le %s",
"Due on %s by %s" : "Echéance le %s pour %s",
"Due on %s" : "Echéance le %s",
+ "Example event - open me!" : "Exemple d'événement - ouvrez-moi !",
+ "System Address Book" : "Carnet d'adresses du système",
+ "The system address book contains contact information for all users in your instance." : "Le carnet d'adresses du système contient les informations de contact de tous les utilisateurs de votre instance.",
+ "Enable System Address Book" : "Activer le carnet d'adresses du système",
+ "DAV system address book" : "Carnet d'adresses système DAV",
+ "No outstanding DAV system address book sync." : "Pas de synchronisation DAV en cours du carnet d'adresses système.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "La synchronisation du carnet d'adresses système DAV n'a pas encore été effectuée car votre instance a plus de 1 000 utilisateurs ou parce qu'une erreur est survenue. Merci de l'exécuter manuellement en tapant la commande \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "Point de terminaison WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Impossible de vérifier si votre serveur web est correctement configuré pour permettre la synchronisation de fichiers via WebDAV. Veuillez vérifier manuellement.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Votre serveur web n’est pas encore correctement configuré pour la synchronisation de fichiers parce que l’interface WebDAV semble ne pas fonctionner.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Votre serveur web est correctement configuré pour permettre la synchronisation de fichiers via WebDAV.",
"Migrated calendar (%1$s)" : "Agenda migré (%1$s)",
+ "Calendars including events, details and attendees" : "Calendriers incluant des événements, détails et participants",
"Contacts and groups" : "Contacts et groupes",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "Point d'accès WebDAV",
- "Availability" : "Disponibilité",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Si vous configurez vos heures de travail, les autres utilisateurs verront si vous êtes disponible quand ils planifient une réunion.",
+ "Absence saved" : "Message d’absence sauvegardé",
+ "Failed to save your absence settings" : "L'enregistrement des paramètres d'absence a échoué",
+ "Absence cleared" : "Message d’absence supprimé",
+ "Failed to clear your absence settings" : "Les paramètres d'absence n'ont pas été effacés",
+ "First day" : "Premier jour",
+ "Last day (inclusive)" : "Dernier jour (inclus)",
+ "Out of office replacement (optional)" : "Remplaçant pendant l'absence (optionnel)",
+ "Name of the replacement" : "Nom du remplaçant",
+ "No results." : "Pas de résultat.",
+ "Start typing." : "Commencez à taper.",
+ "Short absence status" : "Résumé du message d’absence",
+ "Long absence Message" : "Message d’absence complet",
+ "Save" : "Enregistrer",
+ "Disable absence" : "Désactiver le message d’absence",
+ "Failed to load availability" : "Impossible de charger les disponibilités",
+ "Saved availability" : "Disponibilités sauvegardées",
+ "Failed to save availability" : "Impossible de sauvegarder les disponibilités",
"Time zone:" : "Fuseau horaire :",
"to" : "à",
"Delete slot" : "Supprimer le créneau",
"No working hours set" : "Heures de travail non définies",
"Add slot" : "Ajouter un créneau",
- "Monday" : "Lundi",
- "Tuesday" : "Mardi",
- "Wednesday" : "Mercredi",
- "Thursday" : "Jeudi",
- "Friday" : "Vendredi",
- "Saturday" : "Samedi",
- "Sunday" : "Dimanche",
- "Save" : "Enregistrer",
+ "Weekdays" : "Jours de la semaine",
+ "Pick a start time for {dayName}" : "Choisissez une heure de début pour {dayName}",
+ "Pick a end time for {dayName}" : "Choisissez une heure de fin pour {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Définir automatiquement le statut « Ne pas déranger » en dehors des heures de disponibilités pour désactiver toutes les notifications.",
+ "Cancel" : "Annuler",
+ "Import" : "Importation",
+ "Error while saving settings" : "Erreur lors de l'enregistrement des paramètres",
+ "Contact reset successfully" : "Contact réinitialisé avec succès",
+ "Error while resetting contact" : "Erreur lors de la remise à zéro du contact",
+ "Contact imported successfully" : "Contact importé avec succès",
+ "Error while importing contact" : "Erreur lors de l'importation du contact",
+ "Import contact" : "Importer un contact",
+ "Reset to default" : "Restaurer les valeurs par défaut",
+ "Import contacts" : "Importer des contacts",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Importer un nouveau fichier .vcf supprimera le contact par défaut existant et le remplacera. Voulez-vous continuer ?",
+ "Failed to save example event creation setting" : "Échec de la sauvegarde du paramètre de l'exemple de création d'événement",
+ "Failed to upload the example event" : "Échec du téléversement de l'exemple d'événement",
+ "Custom example event was saved successfully" : "L'exemple d'événement personnalisé a bien été enregistré",
+ "Failed to delete the custom example event" : "Échec de la suppression de l'exemple d'événement personnalisé ",
+ "Custom example event was deleted successfully" : "L'exemple de contenu personnalisé a bien été supprimé",
+ "Import calendar event" : "Importer l'événement du calendrier",
+ "Uploading a new event will overwrite the existing one." : "Charger un nouvel événement qui va remplacer l'actuel.",
+ "Upload event" : "Téléverser un événement",
+ "Availability" : "Disponibilités",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Si vous configurez vos heures de travail, les autres personnes verront si vous êtes disponible quand ils planifient une réunion.",
+ "Absence" : "Absence",
+ "Configure your next absence period." : "Configurez votre prochaine période d'absence.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installer aussi {calendarappstoreopen}l'application Calendrier{linkclose}, ou {calendardocopen}connecter à votre PC & téléphone pour synchroniser ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Merci de vérifier d'avoir correctement configuré {emailopen}le serveur de messagerie{linkclose}.",
"Calendar server" : "Serveur de calendrier",
"Send invitations to attendees" : "Envoyer des invitations aux participants",
- "Automatically generate a birthday calendar" : "Générer automatiquement un agenda d'anniversaire",
- "Birthday calendars will be generated by a background job." : "Les agendas d'anniversaire seront générés par une tâche de fond.",
+ "Automatically generate a birthday calendar" : "Générer automatiquement un agenda d’anniversaire",
+ "Birthday calendars will be generated by a background job." : "Les agendas d’anniversaire seront générés par une tâche en arrière-plan.",
"Hence they will not be available immediately after enabling but will show up after some time." : "Par conséquent, ils ne seront pas disponibles immédiatement après l'activation mais apparaîtront après un certain temps.",
"Send notifications for events" : "Envoyer une notification pour les évènements",
"Notifications are sent via background jobs, so these must occur often enough." : "Les notifications sont envoyées par des tâches de fond qui doivent, par conséquent, être exécutées régulièrement.",
+ "Send reminder notifications to calendar sharees as well" : "Envoyez également des notifications de rappel aux personnes partageant le calendrier",
+ "Reminders are always sent to organizers and attendees." : "Des rappels sont toujours envoyés aux organisateurs et aux participants.",
"Enable notifications for events via push" : "Activer les notifications push pour les évènements",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installer aussi {calendarappstoreopen}l'application Calendrier{linkclose}, ou {calendardocopen}connecter à votre PC & téléphone pour synchroniser ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Merci de vérifier d'avoir correctement configuré {emailopen}le serveur de courriel{linkclose}.",
+ "Example content" : "Exemple de contenu",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Les exemples de contenu servent à présenter les fonctionnalités de Nextcloud. Le contenu par défaut est fourni avec Nextcloud et peut être remplacé par du contenu personnalisé.",
"There was an error updating your attendance status." : "Une erreur s'est produite lors de la mise à jour de votre statut de présence.",
"Please contact the organizer directly." : "Merci de contacter l'organisateur directement.",
"Are you accepting the invitation?" : "Acceptez-vous l'invitation ?",
"Tentative" : "Provisoire",
- "Number of guests" : "Nombre d'invités",
- "Comment" : "Commentaire",
- "Your attendance was updated successfully." : "Votre présence a été mise à jour avec succès.",
- "Calendar and tasks" : "Agenda et tâches"
-},"pluralForm" :"nplurals=2; plural=(n > 1);"
+ "Your attendance was updated successfully." : "Votre présence a été mise à jour avec succès."
+},"pluralForm" :"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"
} \ No newline at end of file
diff --git a/apps/dav/l10n/ga.js b/apps/dav/l10n/ga.js
new file mode 100644
index 00000000000..011e809501a
--- /dev/null
+++ b/apps/dav/l10n/ga.js
@@ -0,0 +1,338 @@
+OC.L10N.register(
+ "dav",
+ {
+ "Calendar" : "Féilire",
+ "Tasks" : "Tascanna",
+ "Personal" : "Pearsanta",
+ "{actor} created calendar {calendar}" : "{actor} féilire cruthaithe {calendar}",
+ "You created calendar {calendar}" : "Chruthaigh tú féilire {calendar}",
+ "{actor} deleted calendar {calendar}" : "Scriosadh {actor} féilire {calendar}",
+ "You deleted calendar {calendar}" : "Scrios tú an féilire {calendar}",
+ "{actor} updated calendar {calendar}" : "{actor} féilire nuashonraithe {calendar}",
+ "You updated calendar {calendar}" : "Nuashonraigh tú an féilire {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} féilire athchóirithe {calendar}",
+ "You restored calendar {calendar}" : "D'athchóirigh tú an féilire {calendar}",
+ "You shared calendar {calendar} as public link" : "Roinn tú féilire {calendar} mar nasc poiblí",
+ "You removed public link for calendar {calendar}" : "Bhain tú nasc poiblí don fhéilire {calendar}",
+ "{actor} shared calendar {calendar} with you" : "Roinn {actor} féilire {calendar} leat",
+ "You shared calendar {calendar} with {user}" : "Roinn tú féilire {calendar} le {user} ",
+ "{actor} shared calendar {calendar} with {user}" : "Roinn {actor} féilire {calendar} le {user}",
+ "{actor} unshared calendar {calendar} from you" : "{actor} féilire {calendar} neamhroinnte uait",
+ "You unshared calendar {calendar} from {user}" : "Dhíroinn tú {calendar} ó {user}",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor} féilire {calendar} neamhroinnte ó {user}",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} féilire {calendar} neamhroinnte astu féin",
+ "You shared calendar {calendar} with group {group}" : "Roinn tú féilire {calendar} le grúpa {group}",
+ "{actor} shared calendar {calendar} with group {group}" : "Roinn {actor} féilire {calendar} le grúpa {group}",
+ "You unshared calendar {calendar} from group {group}" : "Dhíroinn tú féilire {calendar} ón ngrúpa {group}",
+ "{actor} unshared calendar {calendar} from group {group}" : "{actor} féilire {calendar} neamhroinnte ó ghrúpa {group}",
+ "Untitled event" : "Imeacht gan teideal",
+ "{actor} created event {event} in calendar {calendar}" : "Chruthaigh {actor} imeacht {event} san fhéilire {calendar}",
+ "You created event {event} in calendar {calendar}" : "Chruthaigh tú {event} imeacht san fhéilire {calendar}",
+ "{actor} deleted event {event} from calendar {calendar}" : "Scrios {actor} imeacht {event} ón bhféilire {calendar}",
+ "You deleted event {event} from calendar {calendar}" : "Scrios tú {event} imeacht ón bhféilire {calendar}",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} imeacht nuashonraithe {event} san fhéilire {calendar}",
+ "You updated event {event} in calendar {calendar}" : "Nuashonraigh tú {event} imeacht san fhéilire {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Bhog {actor} imeacht {event} ó fhéilire {sourceCalendar} go féilire {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Bhog tú {event} imeacht ó fhéilire {sourceCalendar} go féilire {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "D'aischuir {actor} imeacht {event} den fhéilire {calendar}",
+ "You restored event {event} of calendar {calendar}" : "D'aischuir tú imeacht {event} den fhéilire {calendar}",
+ "Busy" : "Gnóthach",
+ "{actor} created to-do {todo} in list {calendar}" : "Chruthaigh {actor} to-do {todo} sa liosta {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Chruthaigh tú to-do {todo} sa liosta {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "Scriosadh {actor} to-do {todo} ón liosta {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Scrios tú to-do {todo} ón liosta {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "Nuashonraigh {actor} to-do {todo} sa liosta {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Nuashonraigh tú to-do {todo} sa liosta {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "Réitigh {actor} to-do {todo} sa liosta {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Réitigh tú le déanamh {todo} sa liosta {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "D'athoscail {actor} to-do {todo} sa liosta {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "D'oscail tú to-do {todo} sa liosta {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Bhog {actor} {todo} ó liosta {sourceCalendar} chun {targetCalendar} a liostú",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Bhog tú chun {todo} a dhéanamh ón liosta {sourceCalendar} go dtí an liosta {targetCalendar}",
+ "Calendar, contacts and tasks" : "Féilire, teagmhálacha agus tascanna",
+ "A <strong>calendar</strong> was modified" : "Athraíodh <strong>calendar</strong> ",
+ "A calendar <strong>event</strong> was modified" : "Athraíodh féilire <strong>event</strong> ",
+ "A calendar <strong>to-do</strong> was modified" : "Athraíodh féilire<strong>to-do</strong> ",
+ "Contact birthdays" : "Déan teagmháil le breithlaethanta",
+ "Death of %s" : "Bás%s",
+ "Untitled calendar" : "Féilire gan teideal",
+ "Calendar:" : "Féilire:",
+ "Date:" : "Dáta:",
+ "Where:" : "Cá háit:",
+ "Description:" : "Cur síos:",
+ "_%n year_::_%n years_" : ["%n bhliain","%n bliain","%n bliain","%n bliain","%n bliain"],
+ "_%n month_::_%n months_" : ["%n mí","%n míonna","%n míonna","%n míonna","%n míonna"],
+ "_%n day_::_%n days_" : ["%n lá","%n laethanta","%n laethanta","%n laethanta","%n laethanta"],
+ "_%n hour_::_%n hours_" : ["%n uair an chloig","%n uair an chloig","%n uair an chloig","%n uair an chloig","%n uair an chloig"],
+ "_%n minute_::_%n minutes_" : ["%n nóiméad","%n nóiméad","%n nóiméad","%n nóiméad","%n nóiméad"],
+ "%s (in %s)" : "%s (i %s)",
+ "%s (%s ago)" : "%s (%s ó shin)",
+ "Calendar: %s" : "Féilire: %s",
+ "Date: %s" : "Dáta: %s",
+ "Description: %s" : "Cur síos: %s",
+ "Where: %s" : "Cá: %s",
+ "%1$s via %2$s" : "%1$s trí %2$s",
+ "In the past on %1$s for the entire day" : "San am atá thart ar %1$s don lá ar fad",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["I gceann nóiméid ar%1$s don lá ar fad","I gceann %n nóiméad ar %1$s ar feadh an lae ar fad","I gceann %n nóiméad ar %1$s ar feadh an lae ar fad","I gceann %n nóiméad ar %1$s ar feadh an lae ar fad","I gceann %n nóiméad ar %1$s ar feadh an lae ar fad"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["I gceann uair an chloig ar %1$s don lá ar fad","I gceann %n uair an chloig ar %1$s don lá ar fad","I gceann %n uair an chloig ar %1$s don lá ar fad","I gceann %n uair an chloig ar %1$s don lá ar fad","I gceann %n uair an chloig ar %1$s don lá ar fad"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["I lá ar %1$s don lá ar fad","I gceann %n lá ar %1$s don lá ar fad","I gceann %n lá ar %1$s don lá ar fad","I gceann %n lá ar %1$s don lá ar fad","I gceann %n lá ar %1$s don lá ar fad"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["I gceann seachtaine ar %1$s don lá ar fad","I gceann %n seachtain ar %1$s don lá ar fad","I gceann %n seachtain ar %1$s don lá ar fad","I gceann %n seachtain ar %1$s don lá ar fad","I gceann %n seachtain ar %1$s don lá ar fad"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["I gceann míosa ar %1$s don lá ar fad","I gceann %n mí ar %1$s don lá ar fad","I gceann %n mí ar %1$s don lá ar fad","I gceann %n mí ar %1$s don lá ar fad","I gceann %n mí ar %1$s don lá ar fad"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["I gceann bliana ar %1$s don lá ar fad","I gceann %n bliain ar %1$s don lá ar fad","I gceann %n bliain ar %1$s don lá ar fad","I gceann %n bliain ar %1$s don lá ar fad","I gceann %n bliain ar %1$s don lá ar fad"],
+ "In the past on %1$s between %2$s - %3$s" : "San am a chuaigh thart ar %1$s idir %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["I gceann nóiméid ar %1$s idir %2$s - %3$s","I %n nóiméad ar %1$s idir %2$s - %3$s","I %n nóiméad ar %1$s idir %2$s - %3$s","I %n nóiméad ar %1$s idir %2$s - %3$s","I %n nóiméad ar %1$s idir %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["I gceann uair an chloig ar %1$s idir %2$s - %3$s","I %n uair an chloig ar %1$s idir %2$s - %3$s","I %n uair an chloig ar %1$s idir %2$s - %3$s","I %n uair an chloig ar %1$s idir %2$s - %3$s","I %n uair an chloig ar %1$s idir %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["I lá ar %1$s idir %2$s - %3$s","I %n lá ar %1$s idir %2$s - %3$s","I %n lá ar %1$s idir %2$s - %3$s","I %n lá ar %1$s idir %2$s - %3$s","I %n lá ar %1$s idir %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["I gceann seachtaine ar %1$s idir %2$s - %3$s","I %n seachtain ar %1$s idir %2$s - %3$s","I %n seachtain ar %1$s idir %2$s - %3$s","I %n seachtain ar %1$s idir %2$s - %3$s","I %n seachtain ar %1$s idir %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["I gceann míosa ar %1$s idir %2$s - %3$s","I %n mí ar %1$s idir %2$s - %3$s","I %n mí ar %1$s idir %2$s - %3$s","I %n mí ar %1$s idir %2$s - %3$s","I %n mí ar %1$s idir %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["I mbliain ar %1$s idir %2$s - %3$s","I gceann %n bliain ar %1$s idir %2$s - %3$s","I gceann %n bliain ar %1$s idir %2$s - %3$s","I gceann %n bliain ar %1$s idir %2$s - %3$s","I gceann %n bliain ar %1$s idir %2$s - %3$s"],
+ "Could not generate when statement" : "Níorbh fhéidir a ghiniúint nuair a ráiteas",
+ "Every Day for the entire day" : "Gach Lá don lá ar fad",
+ "Every Day for the entire day until %1$s" : "Gach Lá ar feadh an lae ar fad go dtí %1$s",
+ "Every Day between %1$s - %2$s" : "Gach Lá idir %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Gach Lá idir %1$s - %2$s go %3$s",
+ "Every %1$d Days for the entire day" : "Gach %1$d lá don lá ar fad",
+ "Every %1$d Days for the entire day until %2$s" : "Gach %1$d Laethanta don lá iomlán go dtí %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Gach %1$d Laethanta idir %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Gach %1$d Laethanta idir %2$s - %3$s go %4$s",
+ "Could not generate event recurrence statement" : "Níorbh fhéidir ráiteas atarlaithe teagmhais a ghiniúint",
+ "Every Week on %1$s for the entire day" : "Gach seachtain ar %1$s don lá ar fad",
+ "Every Week on %1$s for the entire day until %2$s" : "Gach seachtain ar %1$s don lá ar fad go dtí %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Gach Seachtain ar %1$s idir %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Gach Seachtain ar %1$s idir %2$s - %3$s go %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Gach %1$d seachtain ar %2$s don lá ar fad",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Gach %1$d seachtain ar %2$s ar feadh an lae ar fad go dtí %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Gach %1$d Seachtain ar %2$s idir %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Gach %1$d Seachtain ar %2$s idir %3$s - %4$s go %5$s",
+ "Every Month on the %1$s for the entire day" : "Gach Mí ar an %1$s don lá ar fad",
+ "Every Month on the %1$s for the entire day until %2$s" : "Gach Mí ar an %1$s don lá iomlán go dtí %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Gach Mí ar an %1$s idir %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Gach Mí ar an %1$s idir %2$s - %3$s go %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Gach %1$d Míonna ar an %2$s ar feadh an lae ar fad",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Gach %1$d Míonna ar an %2$s ar feadh an lae ar fad go dtí %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Gach %1$d Mí ar an %2$s idir %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Gach %1$d Mí ar an %2$s idir %3$s - %4$s go %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Gach Bliain i %1$s ar an %2$s ar feadh an lae ar fad",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Gach Bliain i %1$s ar an %2$s ar feadh an lae ar fad go dtí %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Gach Bliain i %1$s ar an %2$s idir %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Gach Bliain i %1$s ar an %2$s idir %3$s - %4$s go %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Gach %1$d Bliain i %2$s ar an %3$s don lá ar fad",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Gach %1$d Bliain i %2$s ar an %3$s don lá ar fad go dtí %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Gach %1$d Bliain i %2$s ar an %3$s idir %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Gach %1$d Bliain i %2$s ar an %3$s idir %4$s - %5$s go %6$s",
+ "On specific dates for the entire day until %1$s" : "Ar dhátaí ar leith don lá iomlán go dtí %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "Ar dhátaí sonracha idir %1$s - %2$s go %3$s",
+ "In the past on %1$s" : "San am a chuaigh thart ar %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["I gceann nóiméid ar %1$s","I gceann %n nóiméad ar %1$s","I gceann %n nóiméad ar %1$s","I gceann %n nóiméad ar %1$s","I gceann %n nóiméad ar %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["I gceann uair an chloig ar %1$s","I gceann %n uair ar %1$s","I gceann %n uair ar %1$s","I gceann %n uair ar %1$s","I gceann %n uair ar %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["I gceann lá ar %1$s","I gceann %n lá ar %1$s","I gceann %n lá ar %1$s","I gceann %n lá ar %1$s","I gceann %n lá ar %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["I gceann seachtaine ar %1$s","I gceann %n seachtain ar %1$s","I gceann %n seachtain ar %1$s","I gceann %n seachtain ar %1$s","I gceann %n seachtain ar %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["I gceann míosa ar %1$s","I gceann %n mí ar %1$s","I gceann %n mí ar %1$s","I gceann %n mí ar %1$s","I gceann %n mí ar %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["I gceann bliana ar %1$s","I gceann %n bliain ar %1$s","I gceann %n bliain ar %1$s","I gceann %n bliain ar %1$s","I gceann %n bliain ar %1$s"],
+ "In the past on %1$s then on %2$s" : "San am a chuaigh thart ar %1$s agus ansin ar %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["I gceann nóiméid ar %1$s agus ansin ar %2$s","I gceann %n nóiméad ar %1$s ansin ar %2$s","I gceann %n nóiméad ar %1$s ansin ar %2$s","I gceann %n nóiméad ar %1$s ansin ar %2$s","I gceann %n nóiméad ar %1$s ansin ar %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["I gceann uair an chloig ar %1$s agus ansin ar %2$s","I gceann %n uair an chloig ar %1$s ansin ar %2$s","I gceann %n uair an chloig ar %1$s ansin ar %2$s","I gceann %n uair an chloig ar %1$s ansin ar %2$s","I gceann %n uair an chloig ar %1$s ansin ar %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["I gceann lá ar %1$s ansin ar %2$s","I gceann %n lá ar %1$s ansin ar %2$s","I gceann %n lá ar %1$s ansin ar %2$s","I gceann %n lá ar %1$s ansin ar %2$s","I gceann %n lá ar %1$s ansin ar %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["I gceann seachtaine ar %1$s ansin ar %2$s","I gceann %n seachtain ar %1$s ansin ar %2$s","I gceann %n seachtain ar %1$s ansin ar %2$s","I gceann %n seachtain ar %1$s ansin ar %2$s","I gceann %n seachtain ar %1$s ansin ar %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["I gceann míosa ar %1$s ansin ar %2$s","I gceann %n mí ar %1$s ansin ar %2$s","I gceann %n mí ar %1$s ansin ar %2$s","I gceann %n mí ar %1$s ansin ar %2$s","I gceann %n mí ar %1$s ansin ar %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["I mbliain ar %1$s ansin ar %2$s","I gceann %n bliain ar %1$s ansin ar %2$s","I gceann %n bliain ar %1$s ansin ar %2$s","I gceann %n bliain ar %1$s ansin ar %2$s","I gceann %n bliain ar %1$s ansin ar %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "San am a chuaigh thart ar %1$s ansin ar %2$s agus %3$s",
+ "_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_" : ["I gceann nóiméid ar %1$s ansin ar %2$s agus %3$s","I gceann %n nóiméad ar %1$s ansin ar %2$s agus %3$s","I gceann %n nóiméad ar %1$s ansin ar %2$s agus %3$s","I gceann %n nóiméad ar %1$s ansin ar %2$s agus %3$s","I gceann %n nóiméad ar %1$s ansin ar %2$s agus %3$s"],
+ "_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_" : ["I gceann uair an chloig ar %1$s ansin ar %2$s agus %3$s","I gceann %n uair an chloig ar %1$s ansin ar %2$s agus %3$s","I gceann %n uair an chloig ar %1$s ansin ar %2$s agus %3$s","I gceann %n uair an chloig ar %1$s ansin ar %2$s agus %3$s","I gceann %n uair an chloig ar %1$s ansin ar %2$s agus %3$s"],
+ "_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_" : ["I gceann lá ar %1$s ansin ar %2$s agus %3$s","I gceann %n lá ar %1$s ansin ar %2$s agus %3$s","I gceann %n lá ar %1$s ansin ar %2$s agus %3$s","I gceann %n lá ar %1$s ansin ar %2$s agus %3$s","I gceann %n lá ar %1$s ansin ar %2$s agus %3$s"],
+ "_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_" : ["I gceann seachtaine ar %1$s ansin ar %2$s agus %3$s","I gceann %n seachtain ar %1$s ansin ar %2$s agus %3$s","I gceann %n seachtain ar %1$s ansin ar %2$s agus %3$s","I gceann %n seachtain ar %1$s ansin ar %2$s agus %3$s","I gceann %n seachtain ar %1$s ansin ar %2$s agus %3$s"],
+ "_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_" : ["I gceann míosa ar %1$s ansin ar %2$s agus %3$s","I gceann %n mí ar %1$s ansin ar %2$s agus %3$s","I gceann %n mí ar %1$s ansin ar %2$s agus %3$s","I gceann %n mí ar %1$s ansin ar %2$s agus %3$s","I gceann %n mí ar %1$s ansin ar %2$s agus %3$s"],
+ "_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_" : ["I mbliain ar %1$s ansin ar %2$s agus %3$s","I gceann %n bliain ar %1$s ansin ar %2$s agus %3$s","I gceann %n bliain ar %1$s ansin ar %2$s agus %3$s","I gceann %n bliain ar %1$s ansin ar %2$s agus %3$s","I gceann %n bliain ar %1$s ansin ar %2$s agus %3$s"],
+ "Could not generate next recurrence statement" : "Níorbh fhéidir an chéad ráiteas atarlaithe eile a ghiniúint",
+ "Cancelled: %1$s" : "Ar ceal: %1$s",
+ "\"%1$s\" has been canceled" : "\"%1$s\" curtha ar ceal",
+ "Re: %1$s" : "Maidir le: %1$s",
+ "%1$s has accepted your invitation" : "%1$s ghlac sé le do chuireadh",
+ "%1$s has tentatively accepted your invitation" : "Ghlac %1$s sé go sealadach le do chuireadh",
+ "%1$s has declined your invitation" : "Dhiúltaigh %1$s do chuireadh",
+ "%1$s has responded to your invitation" : "D'fhreagair %1$s do chuireadh",
+ "Invitation updated: %1$s" : "Nuashonraíodh an cuireadh: %1$s",
+ "%1$s updated the event \"%2$s\"" : "Nuashonraigh %1$s imeacht \"%2$s\"",
+ "Invitation: %1$s" : "Cuireadh: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "Ba mhaith le %1$s cuireadh a thabhairt duit chuig \"%2$s\"",
+ "Organizer:" : "Eagraí:",
+ "Attendees:" : "Lucht freastail:",
+ "Title:" : "Teideal:",
+ "When:" : "Cathain:",
+ "Location:" : "Suíomh:",
+ "Link:" : "Nasc:",
+ "Occurring:" : "Ag tarlú:",
+ "Accept" : "Glac",
+ "Decline" : "Meath",
+ "More options …" : "Tuilleadh roghanna…",
+ "More options at %s" : "Tuilleadh roghanna ag %s",
+ "Monday" : "Luan",
+ "Tuesday" : "Máirt",
+ "Wednesday" : "Céadaoin",
+ "Thursday" : "Déardaoin",
+ "Friday" : "Aoine",
+ "Saturday" : "Sathairn",
+ "Sunday" : "Domhnach",
+ "January" : "Eanáir",
+ "February" : "Feabhra",
+ "March" : "Márta",
+ "April" : "Aibreán",
+ "May" : "Bealtaine",
+ "June" : "Meitheamh",
+ "July" : "Iúil",
+ "August" : "Lúnasa",
+ "September" : "Meán Fómhair",
+ "October" : "Deireadh Fómhair",
+ "November" : "Samhain",
+ "December" : "Nollaig",
+ "First" : "Ar dtús",
+ "Second" : "Dara",
+ "Third" : "Tríú",
+ "Fourth" : "Ceathrú",
+ "Fifth" : "Cúigiú",
+ "Last" : "Seo caite",
+ "Second Last" : "Dara Deireanach",
+ "Third Last" : "An Tríú Deireanach",
+ "Fourth Last" : "Ceathrú Deireanach",
+ "Fifth Last" : "An Cúigiú Deireanach",
+ "Contacts" : "Teagmhálaithe",
+ "{actor} created address book {addressbook}" : "chruthaigh {actor} leabhar seoltaí {addressbook}",
+ "You created address book {addressbook}" : "Chruthaigh tú leabhar seoltaí {addressbook}",
+ "{actor} deleted address book {addressbook}" : "Scrios {actor} leabhar seoltaí {addressbook}",
+ "You deleted address book {addressbook}" : "Scrios tú leabhar seoltaí {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} leabhar seoltaí nuashonraithe {addressbook}",
+ "You updated address book {addressbook}" : "Nuashonraigh tú leabhar seoltaí {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "Roinn {actor} leabhar seoltaí {addressbook} leat",
+ "You shared address book {addressbook} with {user}" : "Roinn tú leabhar seoltaí {addressbook} le {user}",
+ "{actor} shared address book {addressbook} with {user}" : "Roinn {actor} leabhar seoltaí {addressbook} le {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} leabhar seoltaí neamhroinnte {addressbook} uait",
+ "You unshared address book {addressbook} from {user}" : "Dhíroinn tú leabhar seoltaí {addressbook} ó {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} leabhar seoltaí neamhroinnte {addressbook} ó {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} leabhar seoltaí neamhroinnte {addressbook} uathu féin",
+ "You shared address book {addressbook} with group {group}" : "Roinn tú leabhar seoltaí {addressbook} le grúpa {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "Roinn {actor} leabhar seoltaí {addressbook} le grúpa {group}",
+ "You unshared address book {addressbook} from group {group}" : "Dhíroinn tú leabhar seoltaí {addressbook} ó ghrúpa {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} leabhar seoltaí neamhroinnte {addressbook} ó ghrúpa {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "Chruthaigh {actor} teagmhálaí {card} sa leabhar seoltaí {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Chruthaigh tú teagmhálaí {card} sa leabhar seoltaí {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "Scrios {actor} teagmhálaí {card} ón leabhar seoltaí {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Scrios tú teagmhálaí {card} ón leabhar seoltaí {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "Nuashonraigh {actor} teagmhálaí {card} sa leabhar seoltaí {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Nuashonraigh tú teagmhálaí {card} sa leabhar seoltaí {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Athraíodh <strong>contact</strong> nó <strong>address book</strong> seoltaí",
+ "Accounts" : "Cuntais",
+ "System address book which holds all accounts" : "Leabhar seoltaí córais ina bhfuil gach cuntas",
+ "File is not updatable: %1$s" : "Ní féidir an comhad a nuashonrú: %1$s",
+ "Failed to get storage for file" : "Theip ar stóras a fháil don chomhad",
+ "Could not write to final file, canceled by hook" : "Níorbh fhéidir scríobh chuig an gcomhad deiridh, cealaithe le hook",
+ "Could not write file contents" : "Níorbh fhéidir inneachar an chomhaid a scríobh",
+ "_%n byte_::_%n bytes_" : ["%n beart ","%n bearta ","%n bearta","%n bearta","%n bearta"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Earráid agus an comhad á chóipeáil go dtí an suíomh sprice (cóipeáladh: %1$s, bhíothas ag súil le méid comhaid: %2$s)",
+ "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." : "Bhíothas ag súil le méid comhaid de %1$s ach léite (ó chliant Nextcloud) agus scríobh (go stóras Nextcloud) %2$s. D'fhéadfadh fadhb líonra a bheith ann ar an taobh seolta nó fadhb ag scríobh chuig an stóras ar thaobh an fhreastalaí.",
+ "Could not rename part file to final file, canceled by hook" : "Níorbh fhéidir páirtchomhad a athainmniú go comhad deiridh, curtha ar ceal le hook",
+ "Could not rename part file to final file" : "Níorbh fhéidir páirtchomhad a athainmniú go dtí an comhad deiridh",
+ "Failed to check file size: %1$s" : "Níorbh fhéidir méid an chomhaid a sheiceáil: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "Níorbh fhéidir comhad a oscailt: %1$s, is cosúil go bhfuil an comhad ann",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Níorbh fhéidir comhad a oscailt: %1$s, is cosúil nach bhfuil an comhad ann",
+ "Encryption not ready: %1$s" : "Níl an criptiúchán réidh: %1$s",
+ "Failed to open file: %1$s" : "Níorbh fhéidir an comhad a oscailt: %1$s",
+ "Failed to unlink: %1$s" : "Theip ar dhínascadh: %1$s",
+ "Failed to write file contents: %1$s" : "Níorbh fhéidir inneachar an chomhaid a scríobh: %1$s",
+ "File not found: %1$s" : "Comhad gan aimsiú: %1$s",
+ "Invalid target path" : "Conair sprice neamhbhailí",
+ "System is in maintenance mode." : "Tá an córas i mód cothabhála.",
+ "Upgrade needed" : "Uasghrádú ag teastáil",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Ní mór do %s a chumrú le HTTPS a úsáid chun CalDAV agus CardDAV a úsáid le iOS/macOS.",
+ "Configures a CalDAV account" : "Cumraíonn sé cuntas CalDAV",
+ "Configures a CardDAV account" : "Cumraíonn sé cuntas CardDAV",
+ "Events" : "Imeachtaí",
+ "Untitled task" : "Tasc gan teideal",
+ "Completed on %s" : "Críochnaithe ar %s",
+ "Due on %s by %s" : "Dlite ar %s faoi %s",
+ "Due on %s" : "Dlite ar %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Fáilte go Féilire Nextcloud!\n\nSeo sampla imeachta - déan iniúchadh ar sholúbthacht na pleanála le Féilire Nextcloud trí aon eagarthóireacht is mian leat a dhéanamh!\n\nLe Féilire Nextcloud, is féidir leat:\n- Imeachtaí a chruthú, a chur in eagar agus a bhainistiú gan stró.\n- Ilfhéilirí a chruthú agus iad a roinnt le comhghleacaithe foirne, cairde nó teaghlach.\n- Infhaighteacht a sheiceáil agus do chuid amanna gnóthacha a thaispeáint do dhaoine eile.\n- Comhtháthú gan uaim le haipeanna agus gléasanna trí CalDAV.\n- Do thaithí a shaincheapadh: imeachtaí athfhillteacha a sceidealú, fógraí agus socruithe eile a choigeartú.",
+ "Example event - open me!" : "Imeacht shamplach - oscail mé!",
+ "System Address Book" : "Leabhar Seoltaí Córais",
+ "The system address book contains contact information for all users in your instance." : "Tá faisnéis teagmhála i leabhar seoltaí an chórais do gach úsáideoir i do chás.",
+ "Enable System Address Book" : "Cumasaigh Leabhar Seoltaí an Chórais",
+ "DAV system address book" : "Leabhar seoltaí córas DAV",
+ "No outstanding DAV system address book sync." : "Níl sioncronú leabhar seoltaí córais DAV gan íoc.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Níor rith sioncronú leabhar seoltaí an chórais DAV fós toisc go bhfuil níos mó ná 1000 úsáideoir ag do chás nó toisc gur tharla earráid. Rith de láimh é le do thoil trí ghlao a chur ar \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "Críochphointe WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Níorbh fhéidir a sheiceáil go bhfuil do fhreastalaí gréasáin socraithe i gceart chun sioncrónú comhad thar WebDAV a cheadú. Seiceáil le do thoil de láimh.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Níl do fhreastalaí gréasáin socraithe i gceart fós chun sioncrónú comhad a cheadú, mar is cosúil go bhfuil comhéadan WebDAV briste.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Tá do fhreastalaí gréasáin socraithe i gceart chun sioncrónú comhad thar WebDAV a cheadú.",
+ "Migrated calendar (%1$s)" : "Féilire aistrithe (%1$s)",
+ "Calendars including events, details and attendees" : "Féilirí lena n-áirítear imeachtaí, sonraí agus lucht freastail",
+ "Contacts and groups" : "Teagmhálaithe agus grúpaí",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "Neamhláithreacht shábháil",
+ "Failed to save your absence settings" : "Theip ar do shocruithe asláithreachta a shábháil",
+ "Absence cleared" : "Neamhláithreacht glanta",
+ "Failed to clear your absence settings" : "Theip ar do shocruithe asláithreachta a ghlanadh",
+ "First day" : "Céad lá",
+ "Last day (inclusive)" : "Lá deiridh (san áireamh)",
+ "Out of office replacement (optional)" : "Athsholáthar as oifig (roghnach)",
+ "Name of the replacement" : "Ainm an athsholáthair",
+ "No results." : "Gan torthaí.",
+ "Start typing." : "Tosaigh ag clóscríobh.",
+ "Short absence status" : "Stádas asláithreachta gearr",
+ "Long absence Message" : "Neamhláithreacht fada Teachtaireacht",
+ "Save" : "Sábháil",
+ "Disable absence" : "Díchumasaigh neamhláithreacht",
+ "Failed to load availability" : "Theip ar infhaighteacht a lódáil",
+ "Saved availability" : "Infhaighteacht shábháilte",
+ "Failed to save availability" : "Theip ar infhaighteacht a shábháil",
+ "Time zone:" : "Crios ama:",
+ "to" : "chun",
+ "Delete slot" : "Scrios sliotán",
+ "No working hours set" : "Níl aon uaireanta oibre socraithe",
+ "Add slot" : "Cuir sliotán",
+ "Weekdays" : "Laethanta na seachtaine",
+ "Pick a start time for {dayName}" : "Roghnaigh am tosaithe le haghaidh {dayName}",
+ "Pick a end time for {dayName}" : "Roghnaigh am críochnaithe le haghaidh {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Socraigh stádas úsáideora go huathoibríoch mar \"Ná cuir isteach\" taobh amuigh den infhaighteacht chun gach fógra a bhalbhú.",
+ "Cancel" : "Cealaigh",
+ "Import" : "Iompórtáil",
+ "Error while saving settings" : "Earráid agus na socruithe á sábháil",
+ "Contact reset successfully" : "D'éirigh le hathshocrú teagmhála",
+ "Error while resetting contact" : "Earráid agus an teagmhálaí á athshocrú",
+ "Contact imported successfully" : "D'éirigh le hiompórtáil an teagmhálaí",
+ "Error while importing contact" : "Earráid agus an teagmhálaí á iompórtáil",
+ "Import contact" : "Iompórtáil teagmhála",
+ "Reset to default" : "Athshocraigh go réamhshocrú",
+ "Import contacts" : "Teagmhálaithe a allmhairiú",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Má dhéantar comhad .vcf nua a iompórtáil, scriosfar an teagmhálaí réamhshocraithe atá ann cheana féin agus cuirfear an ceann nua ina ionad. Ar mhaith leat leanúint ar aghaidh?",
+ "Failed to save example event creation setting" : "Theip ar shocrú cruthaithe imeachta samplach a shábháil",
+ "Failed to upload the example event" : "Theip ar an imeacht samplach a uaslódáil",
+ "Custom example event was saved successfully" : "Sábháladh imeacht samplach saincheaptha go rathúil",
+ "Failed to delete the custom example event" : "Theip ar an imeacht samplach saincheaptha a scriosadh",
+ "Custom example event was deleted successfully" : "Scriosadh imeacht samplach saincheaptha go rathúil",
+ "Import calendar event" : "Imeacht féilire a allmhairiú",
+ "Uploading a new event will overwrite the existing one." : "Scríobhfar an ceann atá ann cheana má uaslódálann tú imeacht nua.",
+ "Upload event" : "Uaslódáil imeacht",
+ "Availability" : "Infhaighteacht",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Má dhéanann tú do chuid uaireanta oibre a chumrú, feicfidh daoine eile nuair a bhíonn tú as oifig nuair a chuireann siad cruinniú in áirithe.",
+ "Absence" : "Neamhláithreacht",
+ "Configure your next absence period." : "Cumraigh do chéad tréimhse neamhláithreachta eile.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Suiteáil an {calendarappstoreopen}Féilire aip{linkclose} freisin, nó {calendardocopen}ceangail do dheasc & do ghutháin phóca le haghaidh sioncronaithe ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Cinntigh le do thoil go socróidh tú {emailopen}an freastalaí ríomhphoist{linkclose} i gceart.",
+ "Calendar server" : "Freastalaí féilire",
+ "Send invitations to attendees" : "Seol cuirí chuig an lucht freastail",
+ "Automatically generate a birthday calendar" : "Gin féilire lá breithe go huathoibríoch",
+ "Birthday calendars will be generated by a background job." : "Ginfear féilirí lá breithe le post cúlra.",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "Mar sin ní bheidh siad ar fáil díreach tar éis iad a chumasú ach taispeánfar iad tar éis roinnt ama.",
+ "Send notifications for events" : "Seol fógraí le haghaidh imeachtaí",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Seoltar fógraí trí phoist chúlra, mar sin caithfidh siad tarlú minic go leor.",
+ "Send reminder notifications to calendar sharees as well" : "Seol fógraí meabhrúcháin chuig scaireanna féilire freisin",
+ "Reminders are always sent to organizers and attendees." : "Seoltar meabhrúcháin chuig na heagraithe agus an lucht freastail i gcónaí.",
+ "Enable notifications for events via push" : "Cumasaigh fógraí le haghaidh imeachtaí trí bhrú",
+ "Example content" : "Ábhar samplach",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Feidhmíonn ábhar samplach chun gnéithe Nextcloud a thaispeáint. Seoltar ábhar réamhshocraithe le Nextcloud, agus is féidir ábhar saincheaptha a chur ina áit.",
+ "There was an error updating your attendance status." : "Tharla earráid agus do stádas freastail á nuashonrú.",
+ "Please contact the organizer directly." : "Téigh i dteagmháil leis an eagraí go díreach le do thoil.",
+ "Are you accepting the invitation?" : "An bhfuil tú ag glacadh leis an gcuireadh?",
+ "Tentative" : "Sealadach",
+ "Your attendance was updated successfully." : "D'éirigh le do thinreamh a nuashonrú."
+},
+"nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4);");
diff --git a/apps/dav/l10n/ga.json b/apps/dav/l10n/ga.json
new file mode 100644
index 00000000000..0436a274efa
--- /dev/null
+++ b/apps/dav/l10n/ga.json
@@ -0,0 +1,336 @@
+{ "translations": {
+ "Calendar" : "Féilire",
+ "Tasks" : "Tascanna",
+ "Personal" : "Pearsanta",
+ "{actor} created calendar {calendar}" : "{actor} féilire cruthaithe {calendar}",
+ "You created calendar {calendar}" : "Chruthaigh tú féilire {calendar}",
+ "{actor} deleted calendar {calendar}" : "Scriosadh {actor} féilire {calendar}",
+ "You deleted calendar {calendar}" : "Scrios tú an féilire {calendar}",
+ "{actor} updated calendar {calendar}" : "{actor} féilire nuashonraithe {calendar}",
+ "You updated calendar {calendar}" : "Nuashonraigh tú an féilire {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} féilire athchóirithe {calendar}",
+ "You restored calendar {calendar}" : "D'athchóirigh tú an féilire {calendar}",
+ "You shared calendar {calendar} as public link" : "Roinn tú féilire {calendar} mar nasc poiblí",
+ "You removed public link for calendar {calendar}" : "Bhain tú nasc poiblí don fhéilire {calendar}",
+ "{actor} shared calendar {calendar} with you" : "Roinn {actor} féilire {calendar} leat",
+ "You shared calendar {calendar} with {user}" : "Roinn tú féilire {calendar} le {user} ",
+ "{actor} shared calendar {calendar} with {user}" : "Roinn {actor} féilire {calendar} le {user}",
+ "{actor} unshared calendar {calendar} from you" : "{actor} féilire {calendar} neamhroinnte uait",
+ "You unshared calendar {calendar} from {user}" : "Dhíroinn tú {calendar} ó {user}",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor} féilire {calendar} neamhroinnte ó {user}",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} féilire {calendar} neamhroinnte astu féin",
+ "You shared calendar {calendar} with group {group}" : "Roinn tú féilire {calendar} le grúpa {group}",
+ "{actor} shared calendar {calendar} with group {group}" : "Roinn {actor} féilire {calendar} le grúpa {group}",
+ "You unshared calendar {calendar} from group {group}" : "Dhíroinn tú féilire {calendar} ón ngrúpa {group}",
+ "{actor} unshared calendar {calendar} from group {group}" : "{actor} féilire {calendar} neamhroinnte ó ghrúpa {group}",
+ "Untitled event" : "Imeacht gan teideal",
+ "{actor} created event {event} in calendar {calendar}" : "Chruthaigh {actor} imeacht {event} san fhéilire {calendar}",
+ "You created event {event} in calendar {calendar}" : "Chruthaigh tú {event} imeacht san fhéilire {calendar}",
+ "{actor} deleted event {event} from calendar {calendar}" : "Scrios {actor} imeacht {event} ón bhféilire {calendar}",
+ "You deleted event {event} from calendar {calendar}" : "Scrios tú {event} imeacht ón bhféilire {calendar}",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} imeacht nuashonraithe {event} san fhéilire {calendar}",
+ "You updated event {event} in calendar {calendar}" : "Nuashonraigh tú {event} imeacht san fhéilire {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Bhog {actor} imeacht {event} ó fhéilire {sourceCalendar} go féilire {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Bhog tú {event} imeacht ó fhéilire {sourceCalendar} go féilire {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "D'aischuir {actor} imeacht {event} den fhéilire {calendar}",
+ "You restored event {event} of calendar {calendar}" : "D'aischuir tú imeacht {event} den fhéilire {calendar}",
+ "Busy" : "Gnóthach",
+ "{actor} created to-do {todo} in list {calendar}" : "Chruthaigh {actor} to-do {todo} sa liosta {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Chruthaigh tú to-do {todo} sa liosta {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "Scriosadh {actor} to-do {todo} ón liosta {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Scrios tú to-do {todo} ón liosta {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "Nuashonraigh {actor} to-do {todo} sa liosta {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Nuashonraigh tú to-do {todo} sa liosta {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "Réitigh {actor} to-do {todo} sa liosta {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Réitigh tú le déanamh {todo} sa liosta {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "D'athoscail {actor} to-do {todo} sa liosta {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "D'oscail tú to-do {todo} sa liosta {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Bhog {actor} {todo} ó liosta {sourceCalendar} chun {targetCalendar} a liostú",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Bhog tú chun {todo} a dhéanamh ón liosta {sourceCalendar} go dtí an liosta {targetCalendar}",
+ "Calendar, contacts and tasks" : "Féilire, teagmhálacha agus tascanna",
+ "A <strong>calendar</strong> was modified" : "Athraíodh <strong>calendar</strong> ",
+ "A calendar <strong>event</strong> was modified" : "Athraíodh féilire <strong>event</strong> ",
+ "A calendar <strong>to-do</strong> was modified" : "Athraíodh féilire<strong>to-do</strong> ",
+ "Contact birthdays" : "Déan teagmháil le breithlaethanta",
+ "Death of %s" : "Bás%s",
+ "Untitled calendar" : "Féilire gan teideal",
+ "Calendar:" : "Féilire:",
+ "Date:" : "Dáta:",
+ "Where:" : "Cá háit:",
+ "Description:" : "Cur síos:",
+ "_%n year_::_%n years_" : ["%n bhliain","%n bliain","%n bliain","%n bliain","%n bliain"],
+ "_%n month_::_%n months_" : ["%n mí","%n míonna","%n míonna","%n míonna","%n míonna"],
+ "_%n day_::_%n days_" : ["%n lá","%n laethanta","%n laethanta","%n laethanta","%n laethanta"],
+ "_%n hour_::_%n hours_" : ["%n uair an chloig","%n uair an chloig","%n uair an chloig","%n uair an chloig","%n uair an chloig"],
+ "_%n minute_::_%n minutes_" : ["%n nóiméad","%n nóiméad","%n nóiméad","%n nóiméad","%n nóiméad"],
+ "%s (in %s)" : "%s (i %s)",
+ "%s (%s ago)" : "%s (%s ó shin)",
+ "Calendar: %s" : "Féilire: %s",
+ "Date: %s" : "Dáta: %s",
+ "Description: %s" : "Cur síos: %s",
+ "Where: %s" : "Cá: %s",
+ "%1$s via %2$s" : "%1$s trí %2$s",
+ "In the past on %1$s for the entire day" : "San am atá thart ar %1$s don lá ar fad",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["I gceann nóiméid ar%1$s don lá ar fad","I gceann %n nóiméad ar %1$s ar feadh an lae ar fad","I gceann %n nóiméad ar %1$s ar feadh an lae ar fad","I gceann %n nóiméad ar %1$s ar feadh an lae ar fad","I gceann %n nóiméad ar %1$s ar feadh an lae ar fad"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["I gceann uair an chloig ar %1$s don lá ar fad","I gceann %n uair an chloig ar %1$s don lá ar fad","I gceann %n uair an chloig ar %1$s don lá ar fad","I gceann %n uair an chloig ar %1$s don lá ar fad","I gceann %n uair an chloig ar %1$s don lá ar fad"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["I lá ar %1$s don lá ar fad","I gceann %n lá ar %1$s don lá ar fad","I gceann %n lá ar %1$s don lá ar fad","I gceann %n lá ar %1$s don lá ar fad","I gceann %n lá ar %1$s don lá ar fad"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["I gceann seachtaine ar %1$s don lá ar fad","I gceann %n seachtain ar %1$s don lá ar fad","I gceann %n seachtain ar %1$s don lá ar fad","I gceann %n seachtain ar %1$s don lá ar fad","I gceann %n seachtain ar %1$s don lá ar fad"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["I gceann míosa ar %1$s don lá ar fad","I gceann %n mí ar %1$s don lá ar fad","I gceann %n mí ar %1$s don lá ar fad","I gceann %n mí ar %1$s don lá ar fad","I gceann %n mí ar %1$s don lá ar fad"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["I gceann bliana ar %1$s don lá ar fad","I gceann %n bliain ar %1$s don lá ar fad","I gceann %n bliain ar %1$s don lá ar fad","I gceann %n bliain ar %1$s don lá ar fad","I gceann %n bliain ar %1$s don lá ar fad"],
+ "In the past on %1$s between %2$s - %3$s" : "San am a chuaigh thart ar %1$s idir %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["I gceann nóiméid ar %1$s idir %2$s - %3$s","I %n nóiméad ar %1$s idir %2$s - %3$s","I %n nóiméad ar %1$s idir %2$s - %3$s","I %n nóiméad ar %1$s idir %2$s - %3$s","I %n nóiméad ar %1$s idir %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["I gceann uair an chloig ar %1$s idir %2$s - %3$s","I %n uair an chloig ar %1$s idir %2$s - %3$s","I %n uair an chloig ar %1$s idir %2$s - %3$s","I %n uair an chloig ar %1$s idir %2$s - %3$s","I %n uair an chloig ar %1$s idir %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["I lá ar %1$s idir %2$s - %3$s","I %n lá ar %1$s idir %2$s - %3$s","I %n lá ar %1$s idir %2$s - %3$s","I %n lá ar %1$s idir %2$s - %3$s","I %n lá ar %1$s idir %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["I gceann seachtaine ar %1$s idir %2$s - %3$s","I %n seachtain ar %1$s idir %2$s - %3$s","I %n seachtain ar %1$s idir %2$s - %3$s","I %n seachtain ar %1$s idir %2$s - %3$s","I %n seachtain ar %1$s idir %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["I gceann míosa ar %1$s idir %2$s - %3$s","I %n mí ar %1$s idir %2$s - %3$s","I %n mí ar %1$s idir %2$s - %3$s","I %n mí ar %1$s idir %2$s - %3$s","I %n mí ar %1$s idir %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["I mbliain ar %1$s idir %2$s - %3$s","I gceann %n bliain ar %1$s idir %2$s - %3$s","I gceann %n bliain ar %1$s idir %2$s - %3$s","I gceann %n bliain ar %1$s idir %2$s - %3$s","I gceann %n bliain ar %1$s idir %2$s - %3$s"],
+ "Could not generate when statement" : "Níorbh fhéidir a ghiniúint nuair a ráiteas",
+ "Every Day for the entire day" : "Gach Lá don lá ar fad",
+ "Every Day for the entire day until %1$s" : "Gach Lá ar feadh an lae ar fad go dtí %1$s",
+ "Every Day between %1$s - %2$s" : "Gach Lá idir %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Gach Lá idir %1$s - %2$s go %3$s",
+ "Every %1$d Days for the entire day" : "Gach %1$d lá don lá ar fad",
+ "Every %1$d Days for the entire day until %2$s" : "Gach %1$d Laethanta don lá iomlán go dtí %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Gach %1$d Laethanta idir %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Gach %1$d Laethanta idir %2$s - %3$s go %4$s",
+ "Could not generate event recurrence statement" : "Níorbh fhéidir ráiteas atarlaithe teagmhais a ghiniúint",
+ "Every Week on %1$s for the entire day" : "Gach seachtain ar %1$s don lá ar fad",
+ "Every Week on %1$s for the entire day until %2$s" : "Gach seachtain ar %1$s don lá ar fad go dtí %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Gach Seachtain ar %1$s idir %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Gach Seachtain ar %1$s idir %2$s - %3$s go %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Gach %1$d seachtain ar %2$s don lá ar fad",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Gach %1$d seachtain ar %2$s ar feadh an lae ar fad go dtí %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Gach %1$d Seachtain ar %2$s idir %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Gach %1$d Seachtain ar %2$s idir %3$s - %4$s go %5$s",
+ "Every Month on the %1$s for the entire day" : "Gach Mí ar an %1$s don lá ar fad",
+ "Every Month on the %1$s for the entire day until %2$s" : "Gach Mí ar an %1$s don lá iomlán go dtí %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Gach Mí ar an %1$s idir %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Gach Mí ar an %1$s idir %2$s - %3$s go %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Gach %1$d Míonna ar an %2$s ar feadh an lae ar fad",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Gach %1$d Míonna ar an %2$s ar feadh an lae ar fad go dtí %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Gach %1$d Mí ar an %2$s idir %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Gach %1$d Mí ar an %2$s idir %3$s - %4$s go %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Gach Bliain i %1$s ar an %2$s ar feadh an lae ar fad",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Gach Bliain i %1$s ar an %2$s ar feadh an lae ar fad go dtí %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Gach Bliain i %1$s ar an %2$s idir %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Gach Bliain i %1$s ar an %2$s idir %3$s - %4$s go %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Gach %1$d Bliain i %2$s ar an %3$s don lá ar fad",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Gach %1$d Bliain i %2$s ar an %3$s don lá ar fad go dtí %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Gach %1$d Bliain i %2$s ar an %3$s idir %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Gach %1$d Bliain i %2$s ar an %3$s idir %4$s - %5$s go %6$s",
+ "On specific dates for the entire day until %1$s" : "Ar dhátaí ar leith don lá iomlán go dtí %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "Ar dhátaí sonracha idir %1$s - %2$s go %3$s",
+ "In the past on %1$s" : "San am a chuaigh thart ar %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["I gceann nóiméid ar %1$s","I gceann %n nóiméad ar %1$s","I gceann %n nóiméad ar %1$s","I gceann %n nóiméad ar %1$s","I gceann %n nóiméad ar %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["I gceann uair an chloig ar %1$s","I gceann %n uair ar %1$s","I gceann %n uair ar %1$s","I gceann %n uair ar %1$s","I gceann %n uair ar %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["I gceann lá ar %1$s","I gceann %n lá ar %1$s","I gceann %n lá ar %1$s","I gceann %n lá ar %1$s","I gceann %n lá ar %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["I gceann seachtaine ar %1$s","I gceann %n seachtain ar %1$s","I gceann %n seachtain ar %1$s","I gceann %n seachtain ar %1$s","I gceann %n seachtain ar %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["I gceann míosa ar %1$s","I gceann %n mí ar %1$s","I gceann %n mí ar %1$s","I gceann %n mí ar %1$s","I gceann %n mí ar %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["I gceann bliana ar %1$s","I gceann %n bliain ar %1$s","I gceann %n bliain ar %1$s","I gceann %n bliain ar %1$s","I gceann %n bliain ar %1$s"],
+ "In the past on %1$s then on %2$s" : "San am a chuaigh thart ar %1$s agus ansin ar %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["I gceann nóiméid ar %1$s agus ansin ar %2$s","I gceann %n nóiméad ar %1$s ansin ar %2$s","I gceann %n nóiméad ar %1$s ansin ar %2$s","I gceann %n nóiméad ar %1$s ansin ar %2$s","I gceann %n nóiméad ar %1$s ansin ar %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["I gceann uair an chloig ar %1$s agus ansin ar %2$s","I gceann %n uair an chloig ar %1$s ansin ar %2$s","I gceann %n uair an chloig ar %1$s ansin ar %2$s","I gceann %n uair an chloig ar %1$s ansin ar %2$s","I gceann %n uair an chloig ar %1$s ansin ar %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["I gceann lá ar %1$s ansin ar %2$s","I gceann %n lá ar %1$s ansin ar %2$s","I gceann %n lá ar %1$s ansin ar %2$s","I gceann %n lá ar %1$s ansin ar %2$s","I gceann %n lá ar %1$s ansin ar %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["I gceann seachtaine ar %1$s ansin ar %2$s","I gceann %n seachtain ar %1$s ansin ar %2$s","I gceann %n seachtain ar %1$s ansin ar %2$s","I gceann %n seachtain ar %1$s ansin ar %2$s","I gceann %n seachtain ar %1$s ansin ar %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["I gceann míosa ar %1$s ansin ar %2$s","I gceann %n mí ar %1$s ansin ar %2$s","I gceann %n mí ar %1$s ansin ar %2$s","I gceann %n mí ar %1$s ansin ar %2$s","I gceann %n mí ar %1$s ansin ar %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["I mbliain ar %1$s ansin ar %2$s","I gceann %n bliain ar %1$s ansin ar %2$s","I gceann %n bliain ar %1$s ansin ar %2$s","I gceann %n bliain ar %1$s ansin ar %2$s","I gceann %n bliain ar %1$s ansin ar %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "San am a chuaigh thart ar %1$s ansin ar %2$s agus %3$s",
+ "_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_" : ["I gceann nóiméid ar %1$s ansin ar %2$s agus %3$s","I gceann %n nóiméad ar %1$s ansin ar %2$s agus %3$s","I gceann %n nóiméad ar %1$s ansin ar %2$s agus %3$s","I gceann %n nóiméad ar %1$s ansin ar %2$s agus %3$s","I gceann %n nóiméad ar %1$s ansin ar %2$s agus %3$s"],
+ "_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_" : ["I gceann uair an chloig ar %1$s ansin ar %2$s agus %3$s","I gceann %n uair an chloig ar %1$s ansin ar %2$s agus %3$s","I gceann %n uair an chloig ar %1$s ansin ar %2$s agus %3$s","I gceann %n uair an chloig ar %1$s ansin ar %2$s agus %3$s","I gceann %n uair an chloig ar %1$s ansin ar %2$s agus %3$s"],
+ "_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_" : ["I gceann lá ar %1$s ansin ar %2$s agus %3$s","I gceann %n lá ar %1$s ansin ar %2$s agus %3$s","I gceann %n lá ar %1$s ansin ar %2$s agus %3$s","I gceann %n lá ar %1$s ansin ar %2$s agus %3$s","I gceann %n lá ar %1$s ansin ar %2$s agus %3$s"],
+ "_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_" : ["I gceann seachtaine ar %1$s ansin ar %2$s agus %3$s","I gceann %n seachtain ar %1$s ansin ar %2$s agus %3$s","I gceann %n seachtain ar %1$s ansin ar %2$s agus %3$s","I gceann %n seachtain ar %1$s ansin ar %2$s agus %3$s","I gceann %n seachtain ar %1$s ansin ar %2$s agus %3$s"],
+ "_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_" : ["I gceann míosa ar %1$s ansin ar %2$s agus %3$s","I gceann %n mí ar %1$s ansin ar %2$s agus %3$s","I gceann %n mí ar %1$s ansin ar %2$s agus %3$s","I gceann %n mí ar %1$s ansin ar %2$s agus %3$s","I gceann %n mí ar %1$s ansin ar %2$s agus %3$s"],
+ "_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_" : ["I mbliain ar %1$s ansin ar %2$s agus %3$s","I gceann %n bliain ar %1$s ansin ar %2$s agus %3$s","I gceann %n bliain ar %1$s ansin ar %2$s agus %3$s","I gceann %n bliain ar %1$s ansin ar %2$s agus %3$s","I gceann %n bliain ar %1$s ansin ar %2$s agus %3$s"],
+ "Could not generate next recurrence statement" : "Níorbh fhéidir an chéad ráiteas atarlaithe eile a ghiniúint",
+ "Cancelled: %1$s" : "Ar ceal: %1$s",
+ "\"%1$s\" has been canceled" : "\"%1$s\" curtha ar ceal",
+ "Re: %1$s" : "Maidir le: %1$s",
+ "%1$s has accepted your invitation" : "%1$s ghlac sé le do chuireadh",
+ "%1$s has tentatively accepted your invitation" : "Ghlac %1$s sé go sealadach le do chuireadh",
+ "%1$s has declined your invitation" : "Dhiúltaigh %1$s do chuireadh",
+ "%1$s has responded to your invitation" : "D'fhreagair %1$s do chuireadh",
+ "Invitation updated: %1$s" : "Nuashonraíodh an cuireadh: %1$s",
+ "%1$s updated the event \"%2$s\"" : "Nuashonraigh %1$s imeacht \"%2$s\"",
+ "Invitation: %1$s" : "Cuireadh: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "Ba mhaith le %1$s cuireadh a thabhairt duit chuig \"%2$s\"",
+ "Organizer:" : "Eagraí:",
+ "Attendees:" : "Lucht freastail:",
+ "Title:" : "Teideal:",
+ "When:" : "Cathain:",
+ "Location:" : "Suíomh:",
+ "Link:" : "Nasc:",
+ "Occurring:" : "Ag tarlú:",
+ "Accept" : "Glac",
+ "Decline" : "Meath",
+ "More options …" : "Tuilleadh roghanna…",
+ "More options at %s" : "Tuilleadh roghanna ag %s",
+ "Monday" : "Luan",
+ "Tuesday" : "Máirt",
+ "Wednesday" : "Céadaoin",
+ "Thursday" : "Déardaoin",
+ "Friday" : "Aoine",
+ "Saturday" : "Sathairn",
+ "Sunday" : "Domhnach",
+ "January" : "Eanáir",
+ "February" : "Feabhra",
+ "March" : "Márta",
+ "April" : "Aibreán",
+ "May" : "Bealtaine",
+ "June" : "Meitheamh",
+ "July" : "Iúil",
+ "August" : "Lúnasa",
+ "September" : "Meán Fómhair",
+ "October" : "Deireadh Fómhair",
+ "November" : "Samhain",
+ "December" : "Nollaig",
+ "First" : "Ar dtús",
+ "Second" : "Dara",
+ "Third" : "Tríú",
+ "Fourth" : "Ceathrú",
+ "Fifth" : "Cúigiú",
+ "Last" : "Seo caite",
+ "Second Last" : "Dara Deireanach",
+ "Third Last" : "An Tríú Deireanach",
+ "Fourth Last" : "Ceathrú Deireanach",
+ "Fifth Last" : "An Cúigiú Deireanach",
+ "Contacts" : "Teagmhálaithe",
+ "{actor} created address book {addressbook}" : "chruthaigh {actor} leabhar seoltaí {addressbook}",
+ "You created address book {addressbook}" : "Chruthaigh tú leabhar seoltaí {addressbook}",
+ "{actor} deleted address book {addressbook}" : "Scrios {actor} leabhar seoltaí {addressbook}",
+ "You deleted address book {addressbook}" : "Scrios tú leabhar seoltaí {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} leabhar seoltaí nuashonraithe {addressbook}",
+ "You updated address book {addressbook}" : "Nuashonraigh tú leabhar seoltaí {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "Roinn {actor} leabhar seoltaí {addressbook} leat",
+ "You shared address book {addressbook} with {user}" : "Roinn tú leabhar seoltaí {addressbook} le {user}",
+ "{actor} shared address book {addressbook} with {user}" : "Roinn {actor} leabhar seoltaí {addressbook} le {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} leabhar seoltaí neamhroinnte {addressbook} uait",
+ "You unshared address book {addressbook} from {user}" : "Dhíroinn tú leabhar seoltaí {addressbook} ó {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} leabhar seoltaí neamhroinnte {addressbook} ó {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} leabhar seoltaí neamhroinnte {addressbook} uathu féin",
+ "You shared address book {addressbook} with group {group}" : "Roinn tú leabhar seoltaí {addressbook} le grúpa {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "Roinn {actor} leabhar seoltaí {addressbook} le grúpa {group}",
+ "You unshared address book {addressbook} from group {group}" : "Dhíroinn tú leabhar seoltaí {addressbook} ó ghrúpa {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} leabhar seoltaí neamhroinnte {addressbook} ó ghrúpa {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "Chruthaigh {actor} teagmhálaí {card} sa leabhar seoltaí {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Chruthaigh tú teagmhálaí {card} sa leabhar seoltaí {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "Scrios {actor} teagmhálaí {card} ón leabhar seoltaí {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Scrios tú teagmhálaí {card} ón leabhar seoltaí {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "Nuashonraigh {actor} teagmhálaí {card} sa leabhar seoltaí {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Nuashonraigh tú teagmhálaí {card} sa leabhar seoltaí {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Athraíodh <strong>contact</strong> nó <strong>address book</strong> seoltaí",
+ "Accounts" : "Cuntais",
+ "System address book which holds all accounts" : "Leabhar seoltaí córais ina bhfuil gach cuntas",
+ "File is not updatable: %1$s" : "Ní féidir an comhad a nuashonrú: %1$s",
+ "Failed to get storage for file" : "Theip ar stóras a fháil don chomhad",
+ "Could not write to final file, canceled by hook" : "Níorbh fhéidir scríobh chuig an gcomhad deiridh, cealaithe le hook",
+ "Could not write file contents" : "Níorbh fhéidir inneachar an chomhaid a scríobh",
+ "_%n byte_::_%n bytes_" : ["%n beart ","%n bearta ","%n bearta","%n bearta","%n bearta"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Earráid agus an comhad á chóipeáil go dtí an suíomh sprice (cóipeáladh: %1$s, bhíothas ag súil le méid comhaid: %2$s)",
+ "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." : "Bhíothas ag súil le méid comhaid de %1$s ach léite (ó chliant Nextcloud) agus scríobh (go stóras Nextcloud) %2$s. D'fhéadfadh fadhb líonra a bheith ann ar an taobh seolta nó fadhb ag scríobh chuig an stóras ar thaobh an fhreastalaí.",
+ "Could not rename part file to final file, canceled by hook" : "Níorbh fhéidir páirtchomhad a athainmniú go comhad deiridh, curtha ar ceal le hook",
+ "Could not rename part file to final file" : "Níorbh fhéidir páirtchomhad a athainmniú go dtí an comhad deiridh",
+ "Failed to check file size: %1$s" : "Níorbh fhéidir méid an chomhaid a sheiceáil: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "Níorbh fhéidir comhad a oscailt: %1$s, is cosúil go bhfuil an comhad ann",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Níorbh fhéidir comhad a oscailt: %1$s, is cosúil nach bhfuil an comhad ann",
+ "Encryption not ready: %1$s" : "Níl an criptiúchán réidh: %1$s",
+ "Failed to open file: %1$s" : "Níorbh fhéidir an comhad a oscailt: %1$s",
+ "Failed to unlink: %1$s" : "Theip ar dhínascadh: %1$s",
+ "Failed to write file contents: %1$s" : "Níorbh fhéidir inneachar an chomhaid a scríobh: %1$s",
+ "File not found: %1$s" : "Comhad gan aimsiú: %1$s",
+ "Invalid target path" : "Conair sprice neamhbhailí",
+ "System is in maintenance mode." : "Tá an córas i mód cothabhála.",
+ "Upgrade needed" : "Uasghrádú ag teastáil",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Ní mór do %s a chumrú le HTTPS a úsáid chun CalDAV agus CardDAV a úsáid le iOS/macOS.",
+ "Configures a CalDAV account" : "Cumraíonn sé cuntas CalDAV",
+ "Configures a CardDAV account" : "Cumraíonn sé cuntas CardDAV",
+ "Events" : "Imeachtaí",
+ "Untitled task" : "Tasc gan teideal",
+ "Completed on %s" : "Críochnaithe ar %s",
+ "Due on %s by %s" : "Dlite ar %s faoi %s",
+ "Due on %s" : "Dlite ar %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Fáilte go Féilire Nextcloud!\n\nSeo sampla imeachta - déan iniúchadh ar sholúbthacht na pleanála le Féilire Nextcloud trí aon eagarthóireacht is mian leat a dhéanamh!\n\nLe Féilire Nextcloud, is féidir leat:\n- Imeachtaí a chruthú, a chur in eagar agus a bhainistiú gan stró.\n- Ilfhéilirí a chruthú agus iad a roinnt le comhghleacaithe foirne, cairde nó teaghlach.\n- Infhaighteacht a sheiceáil agus do chuid amanna gnóthacha a thaispeáint do dhaoine eile.\n- Comhtháthú gan uaim le haipeanna agus gléasanna trí CalDAV.\n- Do thaithí a shaincheapadh: imeachtaí athfhillteacha a sceidealú, fógraí agus socruithe eile a choigeartú.",
+ "Example event - open me!" : "Imeacht shamplach - oscail mé!",
+ "System Address Book" : "Leabhar Seoltaí Córais",
+ "The system address book contains contact information for all users in your instance." : "Tá faisnéis teagmhála i leabhar seoltaí an chórais do gach úsáideoir i do chás.",
+ "Enable System Address Book" : "Cumasaigh Leabhar Seoltaí an Chórais",
+ "DAV system address book" : "Leabhar seoltaí córas DAV",
+ "No outstanding DAV system address book sync." : "Níl sioncronú leabhar seoltaí córais DAV gan íoc.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Níor rith sioncronú leabhar seoltaí an chórais DAV fós toisc go bhfuil níos mó ná 1000 úsáideoir ag do chás nó toisc gur tharla earráid. Rith de láimh é le do thoil trí ghlao a chur ar \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "Críochphointe WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Níorbh fhéidir a sheiceáil go bhfuil do fhreastalaí gréasáin socraithe i gceart chun sioncrónú comhad thar WebDAV a cheadú. Seiceáil le do thoil de láimh.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Níl do fhreastalaí gréasáin socraithe i gceart fós chun sioncrónú comhad a cheadú, mar is cosúil go bhfuil comhéadan WebDAV briste.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Tá do fhreastalaí gréasáin socraithe i gceart chun sioncrónú comhad thar WebDAV a cheadú.",
+ "Migrated calendar (%1$s)" : "Féilire aistrithe (%1$s)",
+ "Calendars including events, details and attendees" : "Féilirí lena n-áirítear imeachtaí, sonraí agus lucht freastail",
+ "Contacts and groups" : "Teagmhálaithe agus grúpaí",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "Neamhláithreacht shábháil",
+ "Failed to save your absence settings" : "Theip ar do shocruithe asláithreachta a shábháil",
+ "Absence cleared" : "Neamhláithreacht glanta",
+ "Failed to clear your absence settings" : "Theip ar do shocruithe asláithreachta a ghlanadh",
+ "First day" : "Céad lá",
+ "Last day (inclusive)" : "Lá deiridh (san áireamh)",
+ "Out of office replacement (optional)" : "Athsholáthar as oifig (roghnach)",
+ "Name of the replacement" : "Ainm an athsholáthair",
+ "No results." : "Gan torthaí.",
+ "Start typing." : "Tosaigh ag clóscríobh.",
+ "Short absence status" : "Stádas asláithreachta gearr",
+ "Long absence Message" : "Neamhláithreacht fada Teachtaireacht",
+ "Save" : "Sábháil",
+ "Disable absence" : "Díchumasaigh neamhláithreacht",
+ "Failed to load availability" : "Theip ar infhaighteacht a lódáil",
+ "Saved availability" : "Infhaighteacht shábháilte",
+ "Failed to save availability" : "Theip ar infhaighteacht a shábháil",
+ "Time zone:" : "Crios ama:",
+ "to" : "chun",
+ "Delete slot" : "Scrios sliotán",
+ "No working hours set" : "Níl aon uaireanta oibre socraithe",
+ "Add slot" : "Cuir sliotán",
+ "Weekdays" : "Laethanta na seachtaine",
+ "Pick a start time for {dayName}" : "Roghnaigh am tosaithe le haghaidh {dayName}",
+ "Pick a end time for {dayName}" : "Roghnaigh am críochnaithe le haghaidh {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Socraigh stádas úsáideora go huathoibríoch mar \"Ná cuir isteach\" taobh amuigh den infhaighteacht chun gach fógra a bhalbhú.",
+ "Cancel" : "Cealaigh",
+ "Import" : "Iompórtáil",
+ "Error while saving settings" : "Earráid agus na socruithe á sábháil",
+ "Contact reset successfully" : "D'éirigh le hathshocrú teagmhála",
+ "Error while resetting contact" : "Earráid agus an teagmhálaí á athshocrú",
+ "Contact imported successfully" : "D'éirigh le hiompórtáil an teagmhálaí",
+ "Error while importing contact" : "Earráid agus an teagmhálaí á iompórtáil",
+ "Import contact" : "Iompórtáil teagmhála",
+ "Reset to default" : "Athshocraigh go réamhshocrú",
+ "Import contacts" : "Teagmhálaithe a allmhairiú",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Má dhéantar comhad .vcf nua a iompórtáil, scriosfar an teagmhálaí réamhshocraithe atá ann cheana féin agus cuirfear an ceann nua ina ionad. Ar mhaith leat leanúint ar aghaidh?",
+ "Failed to save example event creation setting" : "Theip ar shocrú cruthaithe imeachta samplach a shábháil",
+ "Failed to upload the example event" : "Theip ar an imeacht samplach a uaslódáil",
+ "Custom example event was saved successfully" : "Sábháladh imeacht samplach saincheaptha go rathúil",
+ "Failed to delete the custom example event" : "Theip ar an imeacht samplach saincheaptha a scriosadh",
+ "Custom example event was deleted successfully" : "Scriosadh imeacht samplach saincheaptha go rathúil",
+ "Import calendar event" : "Imeacht féilire a allmhairiú",
+ "Uploading a new event will overwrite the existing one." : "Scríobhfar an ceann atá ann cheana má uaslódálann tú imeacht nua.",
+ "Upload event" : "Uaslódáil imeacht",
+ "Availability" : "Infhaighteacht",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Má dhéanann tú do chuid uaireanta oibre a chumrú, feicfidh daoine eile nuair a bhíonn tú as oifig nuair a chuireann siad cruinniú in áirithe.",
+ "Absence" : "Neamhláithreacht",
+ "Configure your next absence period." : "Cumraigh do chéad tréimhse neamhláithreachta eile.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Suiteáil an {calendarappstoreopen}Féilire aip{linkclose} freisin, nó {calendardocopen}ceangail do dheasc & do ghutháin phóca le haghaidh sioncronaithe ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Cinntigh le do thoil go socróidh tú {emailopen}an freastalaí ríomhphoist{linkclose} i gceart.",
+ "Calendar server" : "Freastalaí féilire",
+ "Send invitations to attendees" : "Seol cuirí chuig an lucht freastail",
+ "Automatically generate a birthday calendar" : "Gin féilire lá breithe go huathoibríoch",
+ "Birthday calendars will be generated by a background job." : "Ginfear féilirí lá breithe le post cúlra.",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "Mar sin ní bheidh siad ar fáil díreach tar éis iad a chumasú ach taispeánfar iad tar éis roinnt ama.",
+ "Send notifications for events" : "Seol fógraí le haghaidh imeachtaí",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Seoltar fógraí trí phoist chúlra, mar sin caithfidh siad tarlú minic go leor.",
+ "Send reminder notifications to calendar sharees as well" : "Seol fógraí meabhrúcháin chuig scaireanna féilire freisin",
+ "Reminders are always sent to organizers and attendees." : "Seoltar meabhrúcháin chuig na heagraithe agus an lucht freastail i gcónaí.",
+ "Enable notifications for events via push" : "Cumasaigh fógraí le haghaidh imeachtaí trí bhrú",
+ "Example content" : "Ábhar samplach",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Feidhmíonn ábhar samplach chun gnéithe Nextcloud a thaispeáint. Seoltar ábhar réamhshocraithe le Nextcloud, agus is féidir ábhar saincheaptha a chur ina áit.",
+ "There was an error updating your attendance status." : "Tharla earráid agus do stádas freastail á nuashonrú.",
+ "Please contact the organizer directly." : "Téigh i dteagmháil leis an eagraí go díreach le do thoil.",
+ "Are you accepting the invitation?" : "An bhfuil tú ag glacadh leis an gcuireadh?",
+ "Tentative" : "Sealadach",
+ "Your attendance was updated successfully." : "D'éirigh le do thinreamh a nuashonrú."
+},"pluralForm" :"nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4);"
+} \ No newline at end of file
diff --git a/apps/dav/l10n/gl.js b/apps/dav/l10n/gl.js
index b45bdc5f8a1..2dcf4a4d100 100644
--- a/apps/dav/l10n/gl.js
+++ b/apps/dav/l10n/gl.js
@@ -2,54 +2,64 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Calendario",
- "Todos" : "Asuntos pendentes",
+ "Tasks" : "Tarefas",
"Personal" : "Persoal",
"{actor} created calendar {calendar}" : "{actor} creou o calendario {calendar}",
- "You created calendar {calendar}" : "Vostede creou o calendario {calendar}",
+ "You created calendar {calendar}" : "Creou o calendario {calendar}",
"{actor} deleted calendar {calendar}" : "{actor} eliminou o calendario {calendar}",
- "You deleted calendar {calendar}" : "Vostede eliminou o calendario {calendar}",
+ "You deleted calendar {calendar}" : "Eliminou o calendario {calendar}",
"{actor} updated calendar {calendar}" : "{actor} actualizou o calendario {calendar}",
- "You updated calendar {calendar}" : "Vostede actualizou o calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Vostede compartiu o calendario {calendar} como ligazón pública",
- "You removed public link for calendar {calendar}" : "Vostede retirou a ligazón pública do calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} compartiu o calendario {calendar} con vostede",
- "You shared calendar {calendar} with {user}" : "Vostede compartiu o calendario {calendar} con {user}",
+ "You updated calendar {calendar}" : "Vde. actualizou o calendario {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} restaurou o calendario {calendar}",
+ "You restored calendar {calendar}" : "Vde. restaurou o calendario {calendar}",
+ "You shared calendar {calendar} as public link" : "Vde. compartiu o calendario {calendar} como ligazón pública",
+ "You removed public link for calendar {calendar}" : "Vde. retirou a ligazón pública do calendario {calendar}",
+ "{actor} shared calendar {calendar} with you" : "{actor} compartiu o calendario {calendar} con Vde.",
+ "You shared calendar {calendar} with {user}" : "Vde. compartiu o calendario {calendar} con {user}",
"{actor} shared calendar {calendar} with {user}" : "{actor} compartiu o calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} deixou de compartir o calendario {calendar} de vostede",
- "You unshared calendar {calendar} from {user}" : "Vostede deixou de compartir o calendario {calendar} de {user}",
+ "{actor} unshared calendar {calendar} from you" : "{actor} deixou de compartir o calendario {calendar} de Vde.",
+ "You unshared calendar {calendar} from {user}" : "Vde. deixou de compartir o calendario {calendar} de {user}",
"{actor} unshared calendar {calendar} from {user}" : "{actor} deixou de compartir o calendario {calendar} de {user}",
"{actor} unshared calendar {calendar} from themselves" : "{actor} deixou de compartir o seu propio calendario {calendar}",
- "You shared calendar {calendar} with group {group}" : "Vostede compartiu o calendario {calendar} co grupo {group}",
+ "You shared calendar {calendar} with group {group}" : "Vde. compartiu o calendario {calendar} co grupo {group}",
"{actor} shared calendar {calendar} with group {group}" : "{actor} compartiu o calendario {calendar} co grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Vostede deixou de compartir o calendario {calendar} do grupo {group}",
+ "You unshared calendar {calendar} from group {group}" : "Vde. deixou de compartir o calendario {calendar} do grupo {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} deixou de compartir o calendario {calendar} do grupo {group}",
+ "Untitled event" : "Evento sen título",
"{actor} created event {event} in calendar {calendar}" : "{actor} creou o evento {event} no calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Vostede creou o evento {event} no calendario {calendar}",
+ "You created event {event} in calendar {calendar}" : "Vde. creou o evento {event} no calendario {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} eliminou o evento {event} do calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Vostede eliminou o evento {event} do calendario {calendar}",
+ "You deleted event {event} from calendar {calendar}" : "Vde. eliminou o evento {event} do calendario {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} actualizou o evento {event} no calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Vostede actualizou o evento {event} no calendario {calendar}",
+ "You updated event {event} in calendar {calendar}" : "Vde. actualizou o evento {event} no calendario {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} moveu o evento {event} do calendario {sourceCalendar} ao calendario {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Moveu o evento {event} do calendario {sourceCalendar} ao calendario {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} restaurou o evento {event} do calendario {calendar}",
+ "You restored event {event} of calendar {calendar}" : "Restaurou o evento {event} do calendario {calendar}",
"Busy" : "Ocupado",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creou os asuntos pendentes {todo} na lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Vostede creou os asuntos pendentes {todo} na lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} eliminou os asuntos pendentes {todo} da lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Vostede eliminou os asuntos pendentes {todo} da lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizou os asuntos pendentes {todo} na lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Vostede actualizou os asuntos pendentes {todo} na lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolveu os asuntos pendentes {todo} na lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Vostede resolveu os asuntos pendentes {todo} na lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} volveu abrir os asuntos pendentes {todo} na lista {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Vostede volveu abrir os asuntos pendentes {todo} na lista {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} creou a tarefa pendente {todo} na lista {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Creou a tarefa pendente {todo} na lista {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} eliminou a tarefa pendente {todo} da lista {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Eliminou a tarefa pendente {todo} da lista {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} actualizou a tarefa pendente {todo} na lista {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Actualizou a tarefa pendente {todo} na lista {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} resolveu a tarefa pendente {todo} na lista {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Resolveu a tarefa pendente {todo} na lista {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} reabriu a tarefa pendente {todo} na lista {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Reabriu a tarefa pendente {todo} na lista {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} moveu a tarefa pendente {todo} da lista {sourceCalendar} á lista {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Moveu a tarefa pendente {todo} da lista {sourceCalendar} á lista {targetCalendar}",
+ "Calendar, contacts and tasks" : "Calendario, contactos e tarefas",
"A <strong>calendar</strong> was modified" : "Foi modificado un <strong>calendario</strong>",
"A calendar <strong>event</strong> was modified" : "Foi modificado un <strong>evento</strong> do calendario",
- "A calendar <strong>todo</strong> was modified" : "Foi modificado un <strong>asunto pendente</strong> do calendario",
+ "A calendar <strong>to-do</strong> was modified" : "Modificouse unha <strong>tarefa pendente</strong> do calendario",
"Contact birthdays" : "Aniversario do contacto",
"Death of %s" : "Falecemento de %s",
+ "Untitled calendar" : "Calendario sen título",
"Calendar:" : "Calendario:",
"Date:" : "Data:",
"Where:" : "Onde:",
"Description:" : "Descrición:",
- "Untitled event" : "Evento sen título",
"_%n year_::_%n years_" : ["%n ano","%n anos"],
"_%n month_::_%n months_" : ["%n mes","%n meses"],
"_%n day_::_%n days_" : ["%n día","%n días"],
@@ -62,42 +72,231 @@ OC.L10N.register(
"Description: %s" : "Descrición: %s",
"Where: %s" : "Onde: %s",
"%1$s via %2$s" : "%1$s mediante %2$s",
- "Invitation canceled" : "Convite cancelado",
- "Invitation updated" : "Convite actualizado",
- "Invitation" : "Convite",
+ "In the past on %1$s for the entire day" : "No pasado o %1$s durante todo o día",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Dentro dun minuto o %1$s durante todo o día","Dentro de %n minutos o %1$s durante todo o día"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Dentro dunha hora o %1$s durante todo o día","Dentro de %n horas o %1$s durante todo o día"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Dentro dun día o %1$s durante todo o día","Dentro de %n días o %1$s durante todo o día"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Dentro dunha semana o %1$s durante todo o día","Dentro de %n semanas o %1$s durante todo o día"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Dentro dun mes o %1$s durante todo o día","Dentro de %n meses o %1$s durante todo o día"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Dentro dun ano o %1$s durante todo o día","Dentro de %n anos o %1$s durante todo o día"],
+ "In the past on %1$s between %2$s - %3$s" : "No pasado o %1$s entre as %2$s e as %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Dentro dun minuto o %1$s entre as %2$s e as %3$s","Dentro de %n minutos o %1$s entre as %2$s e as %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Dentro dunha hora o %1$s entre as %2$s e as %3$s","Dentro de %n horas o %1$s entre as %2$s e as %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Dentro dun día o %1$s entre as %2$s e as %3$s","Dentro de %n días o %1$s entre as %2$s e as %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Dentro dunha semana o %1$s entre as %2$s e as %3$s","Dentro de %n semanas o %1$s entre as %2$s e as %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Dentro dun mes o %1$s entre as %2$s e as %3$s","Dentro de %n meses o %1$s entre as %2$s e as %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Dentro dun ano o %1$s entre as %2$s e as %3$s","Dentro de %n anos o %1$s entre as %2$s e as %3$s"],
+ "Could not generate when statement" : "Non foi posíbel xerar a declaración when (cando)",
+ "Every Day for the entire day" : "Todos os días durante todo o día",
+ "Every Day for the entire day until %1$s" : "Todos os días durante todo o día ata o %1$s",
+ "Every Day between %1$s - %2$s" : "Todos os días entre as %1$s e as %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : " Todos os días entre as %1$s e as %2$s ata o %3$s",
+ "Every %1$d Days for the entire day" : "Cada %1$d días durante todo o día",
+ "Every %1$d Days for the entire day until %2$s" : "Cada %1$d días durante todo o día ata o %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Cada %1$d días entre as %2$s e as %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Cada %1$d días entre as %2$s e as %3$s ata o %4$s",
+ "Could not generate event recurrence statement" : "Non foi posíbel xerar a declaración de recorrencia do evento",
+ "Every Week on %1$s for the entire day" : "Todas as semanas o %1$s durante todo o día",
+ "Every Week on %1$s for the entire day until %2$s" : "Todas as semanas o %1$s durante todo o día ata o %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Todas as semanas o %1$s entre as %2$s e as %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Todas as semanas o %1$s entre as %2$s e as %3$s ata o %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Cada %1$d semanas o %2$s durante todo o día",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Cada %1$d semanas o %2$s durante todo o día ata o %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Cada %1$d semanas o %2$s entre as %3$s e as %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Cada %1$d semanas o %2$s entre as %3$s e as %4$s ata o %5$s",
+ "Every Month on the %1$s for the entire day" : "Todos os meses o %1$s durante todo o día",
+ "Every Month on the %1$s for the entire day until %2$s" : "Todos os meses o %1$s durante todo o día ata o %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Todos os meses o %1$s entre as %2$s e as %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Todos os meses o %1$s entre as %2$s e as %3$s ata o %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Cada %1$d meses o %2$s durante todo o día",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Cada %1$d meses o %2$s durante todo o día ata o %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Cada %1$d meses o %2$s entre as %3$s e as %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Cada %1$d meses o %2$s entre as %3$s e as %4$s ata o %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Todos os anos en %1$s, o %2$s durante todo o día",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Todos os anos en %1$s, o %2$s durante todo o día ata o %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Todos os anos en %1$s, o %2$s entre as %3$s e as %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Todos os anos en %1$s, o %2$s entre as %3$s e as %4$s ata o %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Cada %1$d anos en %2$s, o %3$s durante todo o día",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Cada %1$d anos en %2$s, o %3$s durante todo o día ata o %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Cada %1$d anos en %2$s, o %3$s entre as %4$s e as %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Cada %1$d anos en %2$s, o %3$s entre as %4$s e as %5$s ata o %6$s",
+ "On specific dates for the entire day until %1$s" : "En datas concretas durante todo o día ata o %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "En datas concretas entre as %1$s e as %2$s ata o %3$s",
+ "In the past on %1$s" : "No pasado o %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Dentro dun minuto o %1$s","Dentro de %n minutos o %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Dentro dunha hora o %1$s","Dentro de %n horas o %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Dentro dun día o %1$s","Dentro de %n días o %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Dentro dunha semana o %1$s","Dentro de %n semanas o %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Dentro dun mes o %1$s","Dentro de %n meses o %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Dentro dun ano o %1$s","Dentro de %n anos o %1$s"],
+ "In the past on %1$s then on %2$s" : "No pasado o %1$s e após o %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Dentro dun minuto o %1$s e após o %2$s","Dentro de %n minutos o %1$s e após o %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Dentro dunha hora o %1$s e após o %2$s","Dentro de %n horas o %1$s e após o %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Dentro dun día o %1$s e após o %2$s","Dentro de %n días o %1$s e após o %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Dentro dunha semana o %1$s e após o %2$s","Dentro de %n semanas o %1$s e após o %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Dentro dun mes o %1$s e após o %2$s","Dentro de %n meses o %1$s e após o %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Dentro dun ano o %1$s e após o %2$s","Dentro de %n anos o %1$s e após o %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "No pasado o %1$s e após o %2$s e o %3$s",
+ "_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_" : ["Dentro dun minuto o %1$s e após o %2$s e o %3$s","Dentro de %n minutos o %1$s e após o %2$s e o %3$s"],
+ "_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_" : ["Dentro dunha hora o %1$s e após o %2$s e o %3$s","Dentro de %n horas o %1$s e após o %2$s e o %3$s"],
+ "_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_" : ["Dentro dun dia o %1$s e após o %2$s e o %3$s","Dentro de %n dias o %1$s e após o %2$s e o %3$s"],
+ "_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_" : ["Dentro dunha semana o %1$s e após o %2$s e o %3$s","Dentro de %n semanas o %1$s e após o %2$s e o %3$s"],
+ "_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_" : ["Dentro dun mes o %1$s e após o %2$s e o %3$s","Dentro de %n meses o %1$s e após o %2$s e o %3$s"],
+ "_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_" : ["Dentro dun ano o %1$s e após o %2$s e o %3$s","Dentro de %n anos o %1$s e após o %2$s e o %3$s"],
+ "Could not generate next recurrence statement" : "Non foi posíbel xerar a seguinte declaración de recorrencia",
+ "Cancelled: %1$s" : "Cancelado: %1$s",
+ "\"%1$s\" has been canceled" : "Cancelouse «%1$s»",
+ "Re: %1$s" : "Re: %1$s",
+ "%1$s has accepted your invitation" : "%1$s aceptou o seu convite",
+ "%1$s has tentatively accepted your invitation" : "%1$s aceptou provisionalmente o seu convite",
+ "%1$s has declined your invitation" : "%1$s declinou o seu convite",
+ "%1$s has responded to your invitation" : "%1$s respondeu ao seu convite",
+ "Invitation updated: %1$s" : "Convite actualizado: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s actualizou o evento «%2$s»",
+ "Invitation: %1$s" : "Convite: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s quere convidalo a «%2$s»",
+ "Organizer:" : "Organizador:",
+ "Attendees:" : "Asistentes:",
"Title:" : "Título:",
- "Time:" : "Hora:",
+ "When:" : "Cando:",
"Location:" : "Lugar:",
"Link:" : "Ligazón:",
- "Organizer:" : "Organizador:",
- "Attendees:" : "Asistentes:",
+ "Occurring:" : "Acaecendo:",
"Accept" : "Aceptar",
"Decline" : "Declinar",
"More options …" : "Máis opcións…",
"More options at %s" : "Máis opcións en %s",
+ "Monday" : "luns",
+ "Tuesday" : "martes",
+ "Wednesday" : "mércores",
+ "Thursday" : "xoves",
+ "Friday" : "venres",
+ "Saturday" : "sábado",
+ "Sunday" : "domingo",
+ "January" : "xaneiro",
+ "February" : "febreiro",
+ "March" : "marzo",
+ "April" : "abril",
+ "May" : "maio",
+ "June" : "xuño",
+ "July" : "xullo",
+ "August" : "agosto",
+ "September" : "setembro",
+ "October" : "outubro",
+ "November" : "novembro",
+ "December" : "decembro",
+ "First" : "Primeiro",
+ "Second" : "Segundo",
+ "Third" : "Terceiro",
+ "Fourth" : "Cuarto",
+ "Fifth" : "Quinto",
+ "Last" : "Último",
+ "Second Last" : "Penúltimo",
+ "Third Last" : "Antepenúltimo",
+ "Fourth Last" : "Trasantepenúltimo",
+ "Fifth Last" : "Quinto polo final",
"Contacts" : "Contactos",
- "Upgrade needed" : "É necesario anovar actualizar",
+ "{actor} created address book {addressbook}" : "{actor} creou o caderno de enderezos {addressbook}",
+ "You created address book {addressbook}" : "Vde. creou o caderno de enderezos {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} eliminou o caderno de enderezos {addressbook}",
+ "You deleted address book {addressbook}" : "Vde. eliminou o caderno de enderezos {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} actualizou o caderno de enderezos {addressbook}",
+ "You updated address book {addressbook}" : "Vde. actualizou o caderno de enderezos {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} compartiu o caderno de enderezos {addressbook} con Vde.",
+ "You shared address book {addressbook} with {user}" : "Vde. compartiu o caderno de enderezos {addressbook} con {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} compartiu o caderno de enderezos {addressbook} con {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} deixou de compartir o seu caderno de enderezos {addressbook}",
+ "You unshared address book {addressbook} from {user}" : "Vde. deixou de compartir o caderno de enderezos {addressbook} de {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} deixou de compartir o caderno de enderezos {addressbook} de {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} deixaron de compartir o seu caderno de enderezos {addressbook}",
+ "You shared address book {addressbook} with group {group}" : "Vde. compartiu o caderno de enderezos {addressbook} co grupo {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} compartiu o caderno de enderezos {addressbook} co grupo {group}",
+ "You unshared address book {addressbook} from group {group}" : "Vde. deixou de compartir o caderno de enderezos {addressbook} do grupo {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} deixou de compartir o caderno de enderezos {addressbook} do grupo {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} creou o contacto {card} no caderno de enderezos {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Vde. creou o contacto {card} no caderno de enderezos {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} eliminou o contacto {card} do caderno de enderezos {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Vde. eliminou o contacto {card} do caderno de enderezos {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} actualizou o contacto {card} no caderno de enderezos {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Vde. actualizou o contacto {card} no caderno de enderezos {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Foi modificado un <strong>contacto</strong> ou <strong>caderno de enderezos</strong>",
+ "Accounts" : "Contas",
+ "System address book which holds all accounts" : "Caderno de enderezos do sistema que contén todas as contas",
+ "File is not updatable: %1$s" : "Non é posíbel actualizar o ficheiro: %1$s",
+ "Failed to get storage for file" : "Produciuse un fallo ao obter o almacenamento para o ficheiro",
+ "Could not write to final file, canceled by hook" : "Non foi posíbel escribir no ficheiro final, foi cancelado polo sistema",
+ "Could not write file contents" : "Non foi posíbel escribir o contido do ficheiro",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Produciuse un erro ao copiar o ficheiro na localización de destino (copiado: %1$s, tamaño agardado do ficheiro: %2$s)",
+ "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." : "Tamaño agardado do ficheiro %1$s mais lido (do cliente de Nextcloud) e escrito (no almacenamento de Nextcloud) %2$s. Pode ser un problema de rede no lado do envío ou un problema ao escribir no almacenamento no lado do servidor.",
+ "Could not rename part file to final file, canceled by hook" : "Non foi posíbel cambiar o nome do ficheiro parcial ao ficheiro final, foi cancelado polo sistema",
+ "Could not rename part file to final file" : "Non foi posíbel cambiar o nome do ficheiro parcial ao ficheiro final",
+ "Failed to check file size: %1$s" : "Produciuse un erro ao comprobar o tamaño do ficheiro: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "Non foi posíbel abrir o ficheiro: %1$s, semella o ficheiro existe",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Non foi posíbel abrir o ficheiro: %1$s, semella o ficheiro non existe",
+ "Encryption not ready: %1$s" : "O cifrado non está preparado: %1$s",
+ "Failed to open file: %1$s" : "Produciuse un erro ao abrir o ficheiro: %1$s",
+ "Failed to unlink: %1$s" : "Produciuse un erro ao desligar: %1$s",
+ "Failed to write file contents: %1$s" : "Produciuse un erro ao escribir o contido do ficheiro: %1$s",
+ "File not found: %1$s" : "Non se atopou o ficheiro: %1$s",
+ "Invalid target path" : "A ruta de destino non é correcta",
+ "System is in maintenance mode." : "O sistema está en modo de mantemento.",
+ "Upgrade needed" : "É necesario anovar",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "É preciso configurar o seu %s para que empregue HTTPS para poder usar CalDAV e CardDAV con iOS / macOS.",
"Configures a CalDAV account" : "Configurar unha conta de CalDAV",
"Configures a CardDAV account" : "Configurar unha conta de CardDAV",
"Events" : "Eventos",
- "Tasks" : "Tarefas",
"Untitled task" : "Tarefa sen título",
"Completed on %s" : "Rematado o %s",
"Due on %s by %s" : "Caduca o %s por %s",
"Due on %s" : "Caduca o %s",
+ "DAV system address book" : "Caderno de enderezos do sistema DAV",
+ "No outstanding DAV system address book sync." : "Non hai sincronización pendente do caderno de enderezos do sistema DAV.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "A sincronización do caderno de enderezos do sistema DAV aínda non foi executada aínda porque a súa instancia ten máis de 1000 usuarios ou porque se produciu un erro. Execútea manualmente con occ dav:sync-system-addressbook.",
+ "WebDAV endpoint" : "Punto final WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Non foi posíbel comprobar que o seu servidor web estea configurado correctamente para permitir a sincronización de ficheiros a través de WebDAV. Compróbeo manualmente.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "O servidor non foi configurado correctamente para permitir a sincronización de ficheiros, semella que a interface WebDAV non está a funcionar.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "O seu servidor web está configurado correctamente para permitir a sincronización de ficheiros a través de WebDAV.",
+ "Migrated calendar (%1$s)" : "Calendario migrado (%1$s)",
+ "Calendars including events, details and attendees" : "Calendarios incluíndo eventos, detalles e asistentes",
"Contacts and groups" : "Contactos e grupos",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "Terminación WebDAV",
- "to" : "para",
- "Monday" : "luns",
- "Tuesday" : "martes",
- "Wednesday" : "mércores",
- "Thursday" : "xoves",
- "Friday" : "venres",
- "Saturday" : "sábado",
- "Sunday" : "domingo",
+ "Absence saved" : "Ausencia gardada",
+ "Failed to save your absence settings" : "Produciuse un fallo ao gardar os axustes de ausencia",
+ "Absence cleared" : "Limpouse a ausencia",
+ "Failed to clear your absence settings" : "Produciuse un fallo ao limpar os axustes de ausencia",
+ "First day" : "Primeiro día",
+ "Last day (inclusive)" : "Último día (inclusive)",
+ "Out of office replacement (optional)" : "Substitución fóra da oficina (opcional)",
+ "Name of the replacement" : "Nome do substituto",
+ "No results." : "Sen resultados",
+ "Start typing." : "Comece a escribir.",
+ "Short absence status" : "Estado de ausencia breve",
+ "Long absence Message" : "Mensaxe de ausencia prolongada",
"Save" : "Gardar",
+ "Disable absence" : "Desactivar a ausencia",
+ "Failed to load availability" : "Produciuse un erro ao cargar a dispoñibilidade",
+ "Saved availability" : "Dispoñibilidade gardada",
+ "Failed to save availability" : "Produciuse un erro ao gardar a dispoñibilidade",
+ "Time zone:" : "Fuso horario:",
+ "to" : "para",
+ "Delete slot" : "Eliminar franxa horaria",
+ "No working hours set" : "Sen horario de traballo estabelecido",
+ "Add slot" : "Engadir franxa horaria",
+ "Weekdays" : "Días laborábeis",
+ "Pick a start time for {dayName}" : "Escolla unha hora de inicio para {dayName}",
+ "Pick a end time for {dayName}" : "Escolla unha hora de finalización para {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Definir automaticamente o estado do usuario en «Non molestar» fóra de dispoñibilidade para enmudecer todas as notificacións.",
+ "Cancel" : "Cancelar",
+ "Import" : "Importar",
+ "Error while saving settings" : "Produciuse un erro ao gardar os axustes",
+ "Reset to default" : "Restabelecer os valores predeterminados",
+ "Availability" : "Dispoñibilidade",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Se configura o seu horario de traballo, outras persoas verán cando está fóra da oficina cando reserven unha xuntanza.",
+ "Absence" : "Ausencia",
+ "Configure your next absence period." : "Configurar o próximo período de ausencia.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instale tamén a {calendarappstoreopen}aplicación do Calendario{linkclose} ou {calendardocopen}conecte os seus escritorio e móbil para sincronizalos ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Asegúrese de ter configurado correctamente {emailopen}o servidor de correo-e{linkclose}.",
"Calendar server" : "Servidor do calendario",
"Send invitations to attendees" : "Enviar convites aos asistentes",
"Automatically generate a birthday calendar" : "Xerar automaticamente o calendario de aniversarios",
@@ -105,15 +304,13 @@ OC.L10N.register(
"Hence they will not be available immediately after enabling but will show up after some time." : "Por isto, non estarán dispoñíbeis inmediatamente tras activalos, senón que aparecerán após certo tempo",
"Send notifications for events" : "Enviar notificacións para eventos",
"Notifications are sent via background jobs, so these must occur often enough." : "As notificacións enviaranse mediante procesos en segundo plano, polo que estes teñen que suceder con frecuencia.",
- "Enable notifications for events via push" : "Activar o envío de notificacións do servidor para eventos",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instale tamén a {calendarappstoreopen}aplicación do Calendario{linkclose} ou {calendardocopen}conecte os seus escritorio e móbil para sincronizalos ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Asegúrese de ter configurado correctamente {emailopen}o servidor de correo-e{linkclose}.",
+ "Send reminder notifications to calendar sharees as well" : "Enviar notificacións de lembrete tamén aos que comparten calendario",
+ "Reminders are always sent to organizers and attendees." : "Os lembretes envíanselle sempre aos organizadores e aos asistentes.",
+ "Enable notifications for events via push" : "Activar o envío de notificacións emerxentes para eventos",
"There was an error updating your attendance status." : "Produciuse un erro ao actualizar o seu estado de asistencia.",
"Please contact the organizer directly." : "Contacte directamente co organizador.",
- "Are you accepting the invitation?" : "Acepta vostede o convite?",
- "Tentative" : "Tentativa",
- "Comment" : "Comentario",
- "Your attendance was updated successfully." : "A súa asistencia foi actualizada satisfactoriamente.",
- "Calendar and tasks" : "Calendario e tarefas"
+ "Are you accepting the invitation?" : "Acepta Vde. o convite?",
+ "Tentative" : "Provisional",
+ "Your attendance was updated successfully." : "A súa asistencia foi actualizada satisfactoriamente."
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/gl.json b/apps/dav/l10n/gl.json
index 3259cc641e7..777fd270c00 100644
--- a/apps/dav/l10n/gl.json
+++ b/apps/dav/l10n/gl.json
@@ -1,53 +1,63 @@
{ "translations": {
"Calendar" : "Calendario",
- "Todos" : "Asuntos pendentes",
+ "Tasks" : "Tarefas",
"Personal" : "Persoal",
"{actor} created calendar {calendar}" : "{actor} creou o calendario {calendar}",
- "You created calendar {calendar}" : "Vostede creou o calendario {calendar}",
+ "You created calendar {calendar}" : "Creou o calendario {calendar}",
"{actor} deleted calendar {calendar}" : "{actor} eliminou o calendario {calendar}",
- "You deleted calendar {calendar}" : "Vostede eliminou o calendario {calendar}",
+ "You deleted calendar {calendar}" : "Eliminou o calendario {calendar}",
"{actor} updated calendar {calendar}" : "{actor} actualizou o calendario {calendar}",
- "You updated calendar {calendar}" : "Vostede actualizou o calendario {calendar}",
- "You shared calendar {calendar} as public link" : "Vostede compartiu o calendario {calendar} como ligazón pública",
- "You removed public link for calendar {calendar}" : "Vostede retirou a ligazón pública do calendario {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} compartiu o calendario {calendar} con vostede",
- "You shared calendar {calendar} with {user}" : "Vostede compartiu o calendario {calendar} con {user}",
+ "You updated calendar {calendar}" : "Vde. actualizou o calendario {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} restaurou o calendario {calendar}",
+ "You restored calendar {calendar}" : "Vde. restaurou o calendario {calendar}",
+ "You shared calendar {calendar} as public link" : "Vde. compartiu o calendario {calendar} como ligazón pública",
+ "You removed public link for calendar {calendar}" : "Vde. retirou a ligazón pública do calendario {calendar}",
+ "{actor} shared calendar {calendar} with you" : "{actor} compartiu o calendario {calendar} con Vde.",
+ "You shared calendar {calendar} with {user}" : "Vde. compartiu o calendario {calendar} con {user}",
"{actor} shared calendar {calendar} with {user}" : "{actor} compartiu o calendario {calendar} con {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} deixou de compartir o calendario {calendar} de vostede",
- "You unshared calendar {calendar} from {user}" : "Vostede deixou de compartir o calendario {calendar} de {user}",
+ "{actor} unshared calendar {calendar} from you" : "{actor} deixou de compartir o calendario {calendar} de Vde.",
+ "You unshared calendar {calendar} from {user}" : "Vde. deixou de compartir o calendario {calendar} de {user}",
"{actor} unshared calendar {calendar} from {user}" : "{actor} deixou de compartir o calendario {calendar} de {user}",
"{actor} unshared calendar {calendar} from themselves" : "{actor} deixou de compartir o seu propio calendario {calendar}",
- "You shared calendar {calendar} with group {group}" : "Vostede compartiu o calendario {calendar} co grupo {group}",
+ "You shared calendar {calendar} with group {group}" : "Vde. compartiu o calendario {calendar} co grupo {group}",
"{actor} shared calendar {calendar} with group {group}" : "{actor} compartiu o calendario {calendar} co grupo {group}",
- "You unshared calendar {calendar} from group {group}" : "Vostede deixou de compartir o calendario {calendar} do grupo {group}",
+ "You unshared calendar {calendar} from group {group}" : "Vde. deixou de compartir o calendario {calendar} do grupo {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} deixou de compartir o calendario {calendar} do grupo {group}",
+ "Untitled event" : "Evento sen título",
"{actor} created event {event} in calendar {calendar}" : "{actor} creou o evento {event} no calendario {calendar}",
- "You created event {event} in calendar {calendar}" : "Vostede creou o evento {event} no calendario {calendar}",
+ "You created event {event} in calendar {calendar}" : "Vde. creou o evento {event} no calendario {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} eliminou o evento {event} do calendario {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Vostede eliminou o evento {event} do calendario {calendar}",
+ "You deleted event {event} from calendar {calendar}" : "Vde. eliminou o evento {event} do calendario {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} actualizou o evento {event} no calendario {calendar}",
- "You updated event {event} in calendar {calendar}" : "Vostede actualizou o evento {event} no calendario {calendar}",
+ "You updated event {event} in calendar {calendar}" : "Vde. actualizou o evento {event} no calendario {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} moveu o evento {event} do calendario {sourceCalendar} ao calendario {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Moveu o evento {event} do calendario {sourceCalendar} ao calendario {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} restaurou o evento {event} do calendario {calendar}",
+ "You restored event {event} of calendar {calendar}" : "Restaurou o evento {event} do calendario {calendar}",
"Busy" : "Ocupado",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creou os asuntos pendentes {todo} na lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Vostede creou os asuntos pendentes {todo} na lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} eliminou os asuntos pendentes {todo} da lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Vostede eliminou os asuntos pendentes {todo} da lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} actualizou os asuntos pendentes {todo} na lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Vostede actualizou os asuntos pendentes {todo} na lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} resolveu os asuntos pendentes {todo} na lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Vostede resolveu os asuntos pendentes {todo} na lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} volveu abrir os asuntos pendentes {todo} na lista {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Vostede volveu abrir os asuntos pendentes {todo} na lista {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} creou a tarefa pendente {todo} na lista {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Creou a tarefa pendente {todo} na lista {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} eliminou a tarefa pendente {todo} da lista {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Eliminou a tarefa pendente {todo} da lista {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} actualizou a tarefa pendente {todo} na lista {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Actualizou a tarefa pendente {todo} na lista {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} resolveu a tarefa pendente {todo} na lista {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Resolveu a tarefa pendente {todo} na lista {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} reabriu a tarefa pendente {todo} na lista {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Reabriu a tarefa pendente {todo} na lista {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} moveu a tarefa pendente {todo} da lista {sourceCalendar} á lista {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Moveu a tarefa pendente {todo} da lista {sourceCalendar} á lista {targetCalendar}",
+ "Calendar, contacts and tasks" : "Calendario, contactos e tarefas",
"A <strong>calendar</strong> was modified" : "Foi modificado un <strong>calendario</strong>",
"A calendar <strong>event</strong> was modified" : "Foi modificado un <strong>evento</strong> do calendario",
- "A calendar <strong>todo</strong> was modified" : "Foi modificado un <strong>asunto pendente</strong> do calendario",
+ "A calendar <strong>to-do</strong> was modified" : "Modificouse unha <strong>tarefa pendente</strong> do calendario",
"Contact birthdays" : "Aniversario do contacto",
"Death of %s" : "Falecemento de %s",
+ "Untitled calendar" : "Calendario sen título",
"Calendar:" : "Calendario:",
"Date:" : "Data:",
"Where:" : "Onde:",
"Description:" : "Descrición:",
- "Untitled event" : "Evento sen título",
"_%n year_::_%n years_" : ["%n ano","%n anos"],
"_%n month_::_%n months_" : ["%n mes","%n meses"],
"_%n day_::_%n days_" : ["%n día","%n días"],
@@ -60,42 +70,231 @@
"Description: %s" : "Descrición: %s",
"Where: %s" : "Onde: %s",
"%1$s via %2$s" : "%1$s mediante %2$s",
- "Invitation canceled" : "Convite cancelado",
- "Invitation updated" : "Convite actualizado",
- "Invitation" : "Convite",
+ "In the past on %1$s for the entire day" : "No pasado o %1$s durante todo o día",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Dentro dun minuto o %1$s durante todo o día","Dentro de %n minutos o %1$s durante todo o día"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Dentro dunha hora o %1$s durante todo o día","Dentro de %n horas o %1$s durante todo o día"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Dentro dun día o %1$s durante todo o día","Dentro de %n días o %1$s durante todo o día"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Dentro dunha semana o %1$s durante todo o día","Dentro de %n semanas o %1$s durante todo o día"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Dentro dun mes o %1$s durante todo o día","Dentro de %n meses o %1$s durante todo o día"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Dentro dun ano o %1$s durante todo o día","Dentro de %n anos o %1$s durante todo o día"],
+ "In the past on %1$s between %2$s - %3$s" : "No pasado o %1$s entre as %2$s e as %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Dentro dun minuto o %1$s entre as %2$s e as %3$s","Dentro de %n minutos o %1$s entre as %2$s e as %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Dentro dunha hora o %1$s entre as %2$s e as %3$s","Dentro de %n horas o %1$s entre as %2$s e as %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Dentro dun día o %1$s entre as %2$s e as %3$s","Dentro de %n días o %1$s entre as %2$s e as %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Dentro dunha semana o %1$s entre as %2$s e as %3$s","Dentro de %n semanas o %1$s entre as %2$s e as %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Dentro dun mes o %1$s entre as %2$s e as %3$s","Dentro de %n meses o %1$s entre as %2$s e as %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Dentro dun ano o %1$s entre as %2$s e as %3$s","Dentro de %n anos o %1$s entre as %2$s e as %3$s"],
+ "Could not generate when statement" : "Non foi posíbel xerar a declaración when (cando)",
+ "Every Day for the entire day" : "Todos os días durante todo o día",
+ "Every Day for the entire day until %1$s" : "Todos os días durante todo o día ata o %1$s",
+ "Every Day between %1$s - %2$s" : "Todos os días entre as %1$s e as %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : " Todos os días entre as %1$s e as %2$s ata o %3$s",
+ "Every %1$d Days for the entire day" : "Cada %1$d días durante todo o día",
+ "Every %1$d Days for the entire day until %2$s" : "Cada %1$d días durante todo o día ata o %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Cada %1$d días entre as %2$s e as %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Cada %1$d días entre as %2$s e as %3$s ata o %4$s",
+ "Could not generate event recurrence statement" : "Non foi posíbel xerar a declaración de recorrencia do evento",
+ "Every Week on %1$s for the entire day" : "Todas as semanas o %1$s durante todo o día",
+ "Every Week on %1$s for the entire day until %2$s" : "Todas as semanas o %1$s durante todo o día ata o %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Todas as semanas o %1$s entre as %2$s e as %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Todas as semanas o %1$s entre as %2$s e as %3$s ata o %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Cada %1$d semanas o %2$s durante todo o día",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Cada %1$d semanas o %2$s durante todo o día ata o %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Cada %1$d semanas o %2$s entre as %3$s e as %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Cada %1$d semanas o %2$s entre as %3$s e as %4$s ata o %5$s",
+ "Every Month on the %1$s for the entire day" : "Todos os meses o %1$s durante todo o día",
+ "Every Month on the %1$s for the entire day until %2$s" : "Todos os meses o %1$s durante todo o día ata o %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Todos os meses o %1$s entre as %2$s e as %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Todos os meses o %1$s entre as %2$s e as %3$s ata o %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Cada %1$d meses o %2$s durante todo o día",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Cada %1$d meses o %2$s durante todo o día ata o %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Cada %1$d meses o %2$s entre as %3$s e as %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Cada %1$d meses o %2$s entre as %3$s e as %4$s ata o %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Todos os anos en %1$s, o %2$s durante todo o día",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Todos os anos en %1$s, o %2$s durante todo o día ata o %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Todos os anos en %1$s, o %2$s entre as %3$s e as %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Todos os anos en %1$s, o %2$s entre as %3$s e as %4$s ata o %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Cada %1$d anos en %2$s, o %3$s durante todo o día",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Cada %1$d anos en %2$s, o %3$s durante todo o día ata o %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Cada %1$d anos en %2$s, o %3$s entre as %4$s e as %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Cada %1$d anos en %2$s, o %3$s entre as %4$s e as %5$s ata o %6$s",
+ "On specific dates for the entire day until %1$s" : "En datas concretas durante todo o día ata o %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "En datas concretas entre as %1$s e as %2$s ata o %3$s",
+ "In the past on %1$s" : "No pasado o %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Dentro dun minuto o %1$s","Dentro de %n minutos o %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Dentro dunha hora o %1$s","Dentro de %n horas o %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Dentro dun día o %1$s","Dentro de %n días o %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Dentro dunha semana o %1$s","Dentro de %n semanas o %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Dentro dun mes o %1$s","Dentro de %n meses o %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Dentro dun ano o %1$s","Dentro de %n anos o %1$s"],
+ "In the past on %1$s then on %2$s" : "No pasado o %1$s e após o %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Dentro dun minuto o %1$s e após o %2$s","Dentro de %n minutos o %1$s e após o %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Dentro dunha hora o %1$s e após o %2$s","Dentro de %n horas o %1$s e após o %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Dentro dun día o %1$s e após o %2$s","Dentro de %n días o %1$s e após o %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Dentro dunha semana o %1$s e após o %2$s","Dentro de %n semanas o %1$s e após o %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Dentro dun mes o %1$s e após o %2$s","Dentro de %n meses o %1$s e após o %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Dentro dun ano o %1$s e após o %2$s","Dentro de %n anos o %1$s e após o %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "No pasado o %1$s e após o %2$s e o %3$s",
+ "_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_" : ["Dentro dun minuto o %1$s e após o %2$s e o %3$s","Dentro de %n minutos o %1$s e após o %2$s e o %3$s"],
+ "_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_" : ["Dentro dunha hora o %1$s e após o %2$s e o %3$s","Dentro de %n horas o %1$s e após o %2$s e o %3$s"],
+ "_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_" : ["Dentro dun dia o %1$s e após o %2$s e o %3$s","Dentro de %n dias o %1$s e após o %2$s e o %3$s"],
+ "_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_" : ["Dentro dunha semana o %1$s e após o %2$s e o %3$s","Dentro de %n semanas o %1$s e após o %2$s e o %3$s"],
+ "_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_" : ["Dentro dun mes o %1$s e após o %2$s e o %3$s","Dentro de %n meses o %1$s e após o %2$s e o %3$s"],
+ "_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_" : ["Dentro dun ano o %1$s e após o %2$s e o %3$s","Dentro de %n anos o %1$s e após o %2$s e o %3$s"],
+ "Could not generate next recurrence statement" : "Non foi posíbel xerar a seguinte declaración de recorrencia",
+ "Cancelled: %1$s" : "Cancelado: %1$s",
+ "\"%1$s\" has been canceled" : "Cancelouse «%1$s»",
+ "Re: %1$s" : "Re: %1$s",
+ "%1$s has accepted your invitation" : "%1$s aceptou o seu convite",
+ "%1$s has tentatively accepted your invitation" : "%1$s aceptou provisionalmente o seu convite",
+ "%1$s has declined your invitation" : "%1$s declinou o seu convite",
+ "%1$s has responded to your invitation" : "%1$s respondeu ao seu convite",
+ "Invitation updated: %1$s" : "Convite actualizado: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s actualizou o evento «%2$s»",
+ "Invitation: %1$s" : "Convite: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s quere convidalo a «%2$s»",
+ "Organizer:" : "Organizador:",
+ "Attendees:" : "Asistentes:",
"Title:" : "Título:",
- "Time:" : "Hora:",
+ "When:" : "Cando:",
"Location:" : "Lugar:",
"Link:" : "Ligazón:",
- "Organizer:" : "Organizador:",
- "Attendees:" : "Asistentes:",
+ "Occurring:" : "Acaecendo:",
"Accept" : "Aceptar",
"Decline" : "Declinar",
"More options …" : "Máis opcións…",
"More options at %s" : "Máis opcións en %s",
+ "Monday" : "luns",
+ "Tuesday" : "martes",
+ "Wednesday" : "mércores",
+ "Thursday" : "xoves",
+ "Friday" : "venres",
+ "Saturday" : "sábado",
+ "Sunday" : "domingo",
+ "January" : "xaneiro",
+ "February" : "febreiro",
+ "March" : "marzo",
+ "April" : "abril",
+ "May" : "maio",
+ "June" : "xuño",
+ "July" : "xullo",
+ "August" : "agosto",
+ "September" : "setembro",
+ "October" : "outubro",
+ "November" : "novembro",
+ "December" : "decembro",
+ "First" : "Primeiro",
+ "Second" : "Segundo",
+ "Third" : "Terceiro",
+ "Fourth" : "Cuarto",
+ "Fifth" : "Quinto",
+ "Last" : "Último",
+ "Second Last" : "Penúltimo",
+ "Third Last" : "Antepenúltimo",
+ "Fourth Last" : "Trasantepenúltimo",
+ "Fifth Last" : "Quinto polo final",
"Contacts" : "Contactos",
- "Upgrade needed" : "É necesario anovar actualizar",
+ "{actor} created address book {addressbook}" : "{actor} creou o caderno de enderezos {addressbook}",
+ "You created address book {addressbook}" : "Vde. creou o caderno de enderezos {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} eliminou o caderno de enderezos {addressbook}",
+ "You deleted address book {addressbook}" : "Vde. eliminou o caderno de enderezos {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} actualizou o caderno de enderezos {addressbook}",
+ "You updated address book {addressbook}" : "Vde. actualizou o caderno de enderezos {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} compartiu o caderno de enderezos {addressbook} con Vde.",
+ "You shared address book {addressbook} with {user}" : "Vde. compartiu o caderno de enderezos {addressbook} con {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} compartiu o caderno de enderezos {addressbook} con {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} deixou de compartir o seu caderno de enderezos {addressbook}",
+ "You unshared address book {addressbook} from {user}" : "Vde. deixou de compartir o caderno de enderezos {addressbook} de {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} deixou de compartir o caderno de enderezos {addressbook} de {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} deixaron de compartir o seu caderno de enderezos {addressbook}",
+ "You shared address book {addressbook} with group {group}" : "Vde. compartiu o caderno de enderezos {addressbook} co grupo {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} compartiu o caderno de enderezos {addressbook} co grupo {group}",
+ "You unshared address book {addressbook} from group {group}" : "Vde. deixou de compartir o caderno de enderezos {addressbook} do grupo {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} deixou de compartir o caderno de enderezos {addressbook} do grupo {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} creou o contacto {card} no caderno de enderezos {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Vde. creou o contacto {card} no caderno de enderezos {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} eliminou o contacto {card} do caderno de enderezos {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Vde. eliminou o contacto {card} do caderno de enderezos {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} actualizou o contacto {card} no caderno de enderezos {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Vde. actualizou o contacto {card} no caderno de enderezos {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Foi modificado un <strong>contacto</strong> ou <strong>caderno de enderezos</strong>",
+ "Accounts" : "Contas",
+ "System address book which holds all accounts" : "Caderno de enderezos do sistema que contén todas as contas",
+ "File is not updatable: %1$s" : "Non é posíbel actualizar o ficheiro: %1$s",
+ "Failed to get storage for file" : "Produciuse un fallo ao obter o almacenamento para o ficheiro",
+ "Could not write to final file, canceled by hook" : "Non foi posíbel escribir no ficheiro final, foi cancelado polo sistema",
+ "Could not write file contents" : "Non foi posíbel escribir o contido do ficheiro",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Produciuse un erro ao copiar o ficheiro na localización de destino (copiado: %1$s, tamaño agardado do ficheiro: %2$s)",
+ "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." : "Tamaño agardado do ficheiro %1$s mais lido (do cliente de Nextcloud) e escrito (no almacenamento de Nextcloud) %2$s. Pode ser un problema de rede no lado do envío ou un problema ao escribir no almacenamento no lado do servidor.",
+ "Could not rename part file to final file, canceled by hook" : "Non foi posíbel cambiar o nome do ficheiro parcial ao ficheiro final, foi cancelado polo sistema",
+ "Could not rename part file to final file" : "Non foi posíbel cambiar o nome do ficheiro parcial ao ficheiro final",
+ "Failed to check file size: %1$s" : "Produciuse un erro ao comprobar o tamaño do ficheiro: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "Non foi posíbel abrir o ficheiro: %1$s, semella o ficheiro existe",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Non foi posíbel abrir o ficheiro: %1$s, semella o ficheiro non existe",
+ "Encryption not ready: %1$s" : "O cifrado non está preparado: %1$s",
+ "Failed to open file: %1$s" : "Produciuse un erro ao abrir o ficheiro: %1$s",
+ "Failed to unlink: %1$s" : "Produciuse un erro ao desligar: %1$s",
+ "Failed to write file contents: %1$s" : "Produciuse un erro ao escribir o contido do ficheiro: %1$s",
+ "File not found: %1$s" : "Non se atopou o ficheiro: %1$s",
+ "Invalid target path" : "A ruta de destino non é correcta",
+ "System is in maintenance mode." : "O sistema está en modo de mantemento.",
+ "Upgrade needed" : "É necesario anovar",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "É preciso configurar o seu %s para que empregue HTTPS para poder usar CalDAV e CardDAV con iOS / macOS.",
"Configures a CalDAV account" : "Configurar unha conta de CalDAV",
"Configures a CardDAV account" : "Configurar unha conta de CardDAV",
"Events" : "Eventos",
- "Tasks" : "Tarefas",
"Untitled task" : "Tarefa sen título",
"Completed on %s" : "Rematado o %s",
"Due on %s by %s" : "Caduca o %s por %s",
"Due on %s" : "Caduca o %s",
+ "DAV system address book" : "Caderno de enderezos do sistema DAV",
+ "No outstanding DAV system address book sync." : "Non hai sincronización pendente do caderno de enderezos do sistema DAV.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "A sincronización do caderno de enderezos do sistema DAV aínda non foi executada aínda porque a súa instancia ten máis de 1000 usuarios ou porque se produciu un erro. Execútea manualmente con occ dav:sync-system-addressbook.",
+ "WebDAV endpoint" : "Punto final WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Non foi posíbel comprobar que o seu servidor web estea configurado correctamente para permitir a sincronización de ficheiros a través de WebDAV. Compróbeo manualmente.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "O servidor non foi configurado correctamente para permitir a sincronización de ficheiros, semella que a interface WebDAV non está a funcionar.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "O seu servidor web está configurado correctamente para permitir a sincronización de ficheiros a través de WebDAV.",
+ "Migrated calendar (%1$s)" : "Calendario migrado (%1$s)",
+ "Calendars including events, details and attendees" : "Calendarios incluíndo eventos, detalles e asistentes",
"Contacts and groups" : "Contactos e grupos",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "Terminación WebDAV",
- "to" : "para",
- "Monday" : "luns",
- "Tuesday" : "martes",
- "Wednesday" : "mércores",
- "Thursday" : "xoves",
- "Friday" : "venres",
- "Saturday" : "sábado",
- "Sunday" : "domingo",
+ "Absence saved" : "Ausencia gardada",
+ "Failed to save your absence settings" : "Produciuse un fallo ao gardar os axustes de ausencia",
+ "Absence cleared" : "Limpouse a ausencia",
+ "Failed to clear your absence settings" : "Produciuse un fallo ao limpar os axustes de ausencia",
+ "First day" : "Primeiro día",
+ "Last day (inclusive)" : "Último día (inclusive)",
+ "Out of office replacement (optional)" : "Substitución fóra da oficina (opcional)",
+ "Name of the replacement" : "Nome do substituto",
+ "No results." : "Sen resultados",
+ "Start typing." : "Comece a escribir.",
+ "Short absence status" : "Estado de ausencia breve",
+ "Long absence Message" : "Mensaxe de ausencia prolongada",
"Save" : "Gardar",
+ "Disable absence" : "Desactivar a ausencia",
+ "Failed to load availability" : "Produciuse un erro ao cargar a dispoñibilidade",
+ "Saved availability" : "Dispoñibilidade gardada",
+ "Failed to save availability" : "Produciuse un erro ao gardar a dispoñibilidade",
+ "Time zone:" : "Fuso horario:",
+ "to" : "para",
+ "Delete slot" : "Eliminar franxa horaria",
+ "No working hours set" : "Sen horario de traballo estabelecido",
+ "Add slot" : "Engadir franxa horaria",
+ "Weekdays" : "Días laborábeis",
+ "Pick a start time for {dayName}" : "Escolla unha hora de inicio para {dayName}",
+ "Pick a end time for {dayName}" : "Escolla unha hora de finalización para {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Definir automaticamente o estado do usuario en «Non molestar» fóra de dispoñibilidade para enmudecer todas as notificacións.",
+ "Cancel" : "Cancelar",
+ "Import" : "Importar",
+ "Error while saving settings" : "Produciuse un erro ao gardar os axustes",
+ "Reset to default" : "Restabelecer os valores predeterminados",
+ "Availability" : "Dispoñibilidade",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Se configura o seu horario de traballo, outras persoas verán cando está fóra da oficina cando reserven unha xuntanza.",
+ "Absence" : "Ausencia",
+ "Configure your next absence period." : "Configurar o próximo período de ausencia.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instale tamén a {calendarappstoreopen}aplicación do Calendario{linkclose} ou {calendardocopen}conecte os seus escritorio e móbil para sincronizalos ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Asegúrese de ter configurado correctamente {emailopen}o servidor de correo-e{linkclose}.",
"Calendar server" : "Servidor do calendario",
"Send invitations to attendees" : "Enviar convites aos asistentes",
"Automatically generate a birthday calendar" : "Xerar automaticamente o calendario de aniversarios",
@@ -103,15 +302,13 @@
"Hence they will not be available immediately after enabling but will show up after some time." : "Por isto, non estarán dispoñíbeis inmediatamente tras activalos, senón que aparecerán após certo tempo",
"Send notifications for events" : "Enviar notificacións para eventos",
"Notifications are sent via background jobs, so these must occur often enough." : "As notificacións enviaranse mediante procesos en segundo plano, polo que estes teñen que suceder con frecuencia.",
- "Enable notifications for events via push" : "Activar o envío de notificacións do servidor para eventos",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instale tamén a {calendarappstoreopen}aplicación do Calendario{linkclose} ou {calendardocopen}conecte os seus escritorio e móbil para sincronizalos ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Asegúrese de ter configurado correctamente {emailopen}o servidor de correo-e{linkclose}.",
+ "Send reminder notifications to calendar sharees as well" : "Enviar notificacións de lembrete tamén aos que comparten calendario",
+ "Reminders are always sent to organizers and attendees." : "Os lembretes envíanselle sempre aos organizadores e aos asistentes.",
+ "Enable notifications for events via push" : "Activar o envío de notificacións emerxentes para eventos",
"There was an error updating your attendance status." : "Produciuse un erro ao actualizar o seu estado de asistencia.",
"Please contact the organizer directly." : "Contacte directamente co organizador.",
- "Are you accepting the invitation?" : "Acepta vostede o convite?",
- "Tentative" : "Tentativa",
- "Comment" : "Comentario",
- "Your attendance was updated successfully." : "A súa asistencia foi actualizada satisfactoriamente.",
- "Calendar and tasks" : "Calendario e tarefas"
+ "Are you accepting the invitation?" : "Acepta Vde. o convite?",
+ "Tentative" : "Provisional",
+ "Your attendance was updated successfully." : "A súa asistencia foi actualizada satisfactoriamente."
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/dav/l10n/he.js b/apps/dav/l10n/he.js
deleted file mode 100644
index 5c32f113ace..00000000000
--- a/apps/dav/l10n/he.js
+++ /dev/null
@@ -1,117 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "יומן",
- "Todos" : "משימות מטלות",
- "Personal" : "אישי",
- "{actor} created calendar {calendar}" : "היומן {calendar} נוצר על ידי {actor}",
- "You created calendar {calendar}" : "מחקת את היומן {calendar}",
- "{actor} deleted calendar {calendar}" : "היומן {calendar} נמחק על ידי {actor}",
- "You deleted calendar {calendar}" : "מחקת את היומן {calendar}",
- "{actor} updated calendar {calendar}" : "היומן {calendar} עודכן על ידי {actor}",
- "You updated calendar {calendar}" : "עדכנת את היומן {calendar}",
- "You shared calendar {calendar} as public link" : "שיתפת את היומן {calendar} כקישור ציבורי",
- "You removed public link for calendar {calendar}" : "הסרת את הקישור הציבורי ליומן {calendar}",
- "{actor} shared calendar {calendar} with you" : "שותף אתך לוח השנה {calendar} על ידי {actor}",
- "You shared calendar {calendar} with {user}" : "שיתפת לוח שנה {calendar} עם {user}",
- "{actor} shared calendar {calendar} with {user}" : "לוח השנה {calendar} שותף על ידי {actor} עם {user}",
- "{actor} unshared calendar {calendar} from you" : "השיתוף של לוח השנה {calendar} אתך הופסק על ידי {actor}",
- "You unshared calendar {calendar} from {user}" : "ביטלת את שיתוף לוח השנה {calendar} עם {user}",
- "{actor} unshared calendar {calendar} from {user}" : "השיתוף של לוח השנה {calendar} עם {user} הופסק על ידי {actor}",
- "{actor} unshared calendar {calendar} from themselves" : "השיתוף של לוח השנה {calendar} עם עצמם הופסק על ידי {actor}",
- "You shared calendar {calendar} with group {group}" : "שיתפת את לוח השנה {calendar} עם הקבוצה {group}",
- "{actor} shared calendar {calendar} with group {group}" : "לוח השנה {calendar} שותף עם הקבוצה {group} על ידי {actor}",
- "You unshared calendar {calendar} from group {group}" : "הפסקת את שיתוף לוח השנה {calendar} עם הקבוצה {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "השיתוף של לוח השנה {calendar} עם {group} הופסק על ידי {actor}",
- "{actor} created event {event} in calendar {calendar}" : "האירוע {event} נוצר בלוח השנה {calendar} על ידי {actor}",
- "You created event {event} in calendar {calendar}" : "יצרת אירוע {event} בלוח השנה {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "האירוע {event} נמחק מלוח השנה {calendar} על ידי {actor}",
- "You deleted event {event} from calendar {calendar}" : "מחקת אירוע {event} מלוח השנה {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "האירוע {event} עודכן בלוח השנה {calendar} על ידי {actor}",
- "You updated event {event} in calendar {calendar}" : "עדכנת את האירוע {event} בלוח השנה {calendar}",
- "Busy" : "עסוק",
- "{actor} created todo {todo} in list {calendar}" : "המשימה לביצוע {todo} ברשימה {calendar} נוצרה על ידי {actor}",
- "You created todo {todo} in list {calendar}" : "יצרת את המשימה לביצוע {todo} ברשימה {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "המשימה לביצוע {todo} מהרשימה {calendar} נמחקה על ידי {actor}",
- "You deleted todo {todo} from list {calendar}" : "מחקת את המשימה לביצוע {todo} מהרשימה {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "המשימה לביצוע {todo} ברשימה {calendar} עודכנה על ידי {actor}",
- "You updated todo {todo} in list {calendar}" : "עדכנת את המשימה לביצוע {todo} ברשימה {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "המשימה לביצוע {todo} ברשימה {calendar} נפתרה על ידי {actor}",
- "You solved todo {todo} in list {calendar}" : "פתרת משימה לביצוע {todo} ברשימה {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "המשימה לביצוע {todo} ברשימה {calendar} נפתחה מחדש על ידי {actor}",
- "You reopened todo {todo} in list {calendar}" : "פתחת מחדש את המשימה לביצוע {todo} ברשימה {calendar}",
- "A <strong>calendar</strong> was modified" : " <strong>יומן</strong> נערך",
- "A calendar <strong>event</strong> was modified" : "<strong>אירוע</strong> ביומן נערך",
- "A calendar <strong>todo</strong> was modified" : "נערכה <strong>מטלה</strong> בלוח שנה",
- "Contact birthdays" : "ימי הולדת של אנשי קשר",
- "Death of %s" : "הפטירה של %s",
- "Calendar:" : "לוח שנה:",
- "Date:" : "תאריך:",
- "Where:" : "איפה:",
- "Description:" : "תיאור:",
- "Untitled event" : "אירוע ללא כותרת",
- "_%n year_::_%n years_" : ["שנה","שנתיים","%n שנים","%n שנים"],
- "_%n month_::_%n months_" : ["חודש","חודשיים","%n חודשים","%n חודשים"],
- "_%n day_::_%n days_" : ["יום","יומיים","%n ימים","%n ימים"],
- "_%n hour_::_%n hours_" : ["שעה","שעתיים","%n שעות","%n שעות"],
- "_%n minute_::_%n minutes_" : ["דקה","%n דקות","%n דקות","%n דקות"],
- "%s (in %s)" : "%s (בתוך %s)",
- "%s (%s ago)" : "%s (לפני %s)",
- "Calendar: %s" : "לוח שנה: %s",
- "Date: %s" : "תאריך: %s",
- "Description: %s" : "תיאור: %s",
- "Where: %s" : "איפה: %s",
- "%1$s via %2$s" : "%1$s דרך %2$s",
- "Invitation canceled" : "ההזמנה בוטלה",
- "Invitation updated" : "ההזמנה עודכנה",
- "Invitation" : "הזמנה",
- "Title:" : "כותרת:",
- "Time:" : "שעה:",
- "Location:" : "מיקום:",
- "Link:" : "קישור:",
- "Organizer:" : "ארגון:",
- "Attendees:" : "משתתפים:",
- "Accept" : "קבלה",
- "Decline" : "דחייה",
- "More options …" : "אפשרויות נוספות…",
- "More options at %s" : "אפשרויות נוספים ב־%s",
- "Contacts" : "אנשי קשר",
- "Upgrade needed" : "נדרש עדכון",
- "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "עליך להגדיר את ה־%s שלך להשתמש ב־HTTPS כדי להשתמש ב־CalDAV וב־CardDAV עם iOS/macOS.",
- "Configures a CalDAV account" : "מגדיר חשבון CalDAV",
- "Configures a CardDAV account" : "מגדיר חשבון CardDAV",
- "Events" : "אירועים",
- "Tasks" : "משימות",
- "Untitled task" : "משימה ללא כותרת",
- "Completed on %s" : "הושלמה ב־%s",
- "Contacts and groups" : "אנשי קשר וקבוצות",
- "WebDAV" : "WebDAV",
- "WebDAV endpoint" : "נקודת קצה WebDAV",
- "to" : "אל",
- "Monday" : "יום שני",
- "Tuesday" : "יום שלישי",
- "Wednesday" : "יום רביעי",
- "Thursday" : "יום חמישי",
- "Friday" : "יום שישי",
- "Saturday" : "יום שבת",
- "Sunday" : "יום ראשון",
- "Save" : "שמירה",
- "Calendar server" : "שרת לוח שנה",
- "Send invitations to attendees" : "שליחת הזמנות למשתתפים",
- "Automatically generate a birthday calendar" : "יצירת יומן ימי הולדת אוטומטית",
- "Birthday calendars will be generated by a background job." : "יומני ימי הולדת ייווצרו על ידי משימה ברקע.",
- "Hence they will not be available immediately after enabling but will show up after some time." : "לכן הם לא יהיה זמינים מיד לאחר ההפעלה אלא הם יופיעו לאחר זמן מה.",
- "Send notifications for events" : "שליחת התראות לאירועים",
- "Notifications are sent via background jobs, so these must occur often enough." : "התראות נשלחות באמצעות משימות רקע, לכן הבדיקה תתבצע יחסית לעתים קרובות.",
- "Enable notifications for events via push" : "הפעלת התראות לאירועים בדחיפה",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "עליך גם להתקין את {calendarappstoreopen}יישומון לוח שנה{linkclose}, או {calendardocopen}לחבר את שולחן העבודה והמכשיר הנייד שלך לסנכרון ↖{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "נא לוודא שהגדרת את {emailopen}שרת הדוא״ל{linkclose} כראוי.",
- "There was an error updating your attendance status." : "אירעה שגיאה בעת עדכון מצב ההשתתפות שלך.",
- "Please contact the organizer directly." : "נא ליצור קשר עם הגוף מארגן ישירות.",
- "Are you accepting the invitation?" : "האם להיענות להזמנה?",
- "Tentative" : "טנטטיבית",
- "Comment" : "הערה",
- "Your attendance was updated successfully." : "ההשתתפות שלך עודכנה בהצלחה.",
- "Calendar and tasks" : "לוח שנה ומשימות"
-},
-"nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;");
diff --git a/apps/dav/l10n/he.json b/apps/dav/l10n/he.json
deleted file mode 100644
index 10fe8cad1bf..00000000000
--- a/apps/dav/l10n/he.json
+++ /dev/null
@@ -1,115 +0,0 @@
-{ "translations": {
- "Calendar" : "יומן",
- "Todos" : "משימות מטלות",
- "Personal" : "אישי",
- "{actor} created calendar {calendar}" : "היומן {calendar} נוצר על ידי {actor}",
- "You created calendar {calendar}" : "מחקת את היומן {calendar}",
- "{actor} deleted calendar {calendar}" : "היומן {calendar} נמחק על ידי {actor}",
- "You deleted calendar {calendar}" : "מחקת את היומן {calendar}",
- "{actor} updated calendar {calendar}" : "היומן {calendar} עודכן על ידי {actor}",
- "You updated calendar {calendar}" : "עדכנת את היומן {calendar}",
- "You shared calendar {calendar} as public link" : "שיתפת את היומן {calendar} כקישור ציבורי",
- "You removed public link for calendar {calendar}" : "הסרת את הקישור הציבורי ליומן {calendar}",
- "{actor} shared calendar {calendar} with you" : "שותף אתך לוח השנה {calendar} על ידי {actor}",
- "You shared calendar {calendar} with {user}" : "שיתפת לוח שנה {calendar} עם {user}",
- "{actor} shared calendar {calendar} with {user}" : "לוח השנה {calendar} שותף על ידי {actor} עם {user}",
- "{actor} unshared calendar {calendar} from you" : "השיתוף של לוח השנה {calendar} אתך הופסק על ידי {actor}",
- "You unshared calendar {calendar} from {user}" : "ביטלת את שיתוף לוח השנה {calendar} עם {user}",
- "{actor} unshared calendar {calendar} from {user}" : "השיתוף של לוח השנה {calendar} עם {user} הופסק על ידי {actor}",
- "{actor} unshared calendar {calendar} from themselves" : "השיתוף של לוח השנה {calendar} עם עצמם הופסק על ידי {actor}",
- "You shared calendar {calendar} with group {group}" : "שיתפת את לוח השנה {calendar} עם הקבוצה {group}",
- "{actor} shared calendar {calendar} with group {group}" : "לוח השנה {calendar} שותף עם הקבוצה {group} על ידי {actor}",
- "You unshared calendar {calendar} from group {group}" : "הפסקת את שיתוף לוח השנה {calendar} עם הקבוצה {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "השיתוף של לוח השנה {calendar} עם {group} הופסק על ידי {actor}",
- "{actor} created event {event} in calendar {calendar}" : "האירוע {event} נוצר בלוח השנה {calendar} על ידי {actor}",
- "You created event {event} in calendar {calendar}" : "יצרת אירוע {event} בלוח השנה {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "האירוע {event} נמחק מלוח השנה {calendar} על ידי {actor}",
- "You deleted event {event} from calendar {calendar}" : "מחקת אירוע {event} מלוח השנה {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "האירוע {event} עודכן בלוח השנה {calendar} על ידי {actor}",
- "You updated event {event} in calendar {calendar}" : "עדכנת את האירוע {event} בלוח השנה {calendar}",
- "Busy" : "עסוק",
- "{actor} created todo {todo} in list {calendar}" : "המשימה לביצוע {todo} ברשימה {calendar} נוצרה על ידי {actor}",
- "You created todo {todo} in list {calendar}" : "יצרת את המשימה לביצוע {todo} ברשימה {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "המשימה לביצוע {todo} מהרשימה {calendar} נמחקה על ידי {actor}",
- "You deleted todo {todo} from list {calendar}" : "מחקת את המשימה לביצוע {todo} מהרשימה {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "המשימה לביצוע {todo} ברשימה {calendar} עודכנה על ידי {actor}",
- "You updated todo {todo} in list {calendar}" : "עדכנת את המשימה לביצוע {todo} ברשימה {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "המשימה לביצוע {todo} ברשימה {calendar} נפתרה על ידי {actor}",
- "You solved todo {todo} in list {calendar}" : "פתרת משימה לביצוע {todo} ברשימה {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "המשימה לביצוע {todo} ברשימה {calendar} נפתחה מחדש על ידי {actor}",
- "You reopened todo {todo} in list {calendar}" : "פתחת מחדש את המשימה לביצוע {todo} ברשימה {calendar}",
- "A <strong>calendar</strong> was modified" : " <strong>יומן</strong> נערך",
- "A calendar <strong>event</strong> was modified" : "<strong>אירוע</strong> ביומן נערך",
- "A calendar <strong>todo</strong> was modified" : "נערכה <strong>מטלה</strong> בלוח שנה",
- "Contact birthdays" : "ימי הולדת של אנשי קשר",
- "Death of %s" : "הפטירה של %s",
- "Calendar:" : "לוח שנה:",
- "Date:" : "תאריך:",
- "Where:" : "איפה:",
- "Description:" : "תיאור:",
- "Untitled event" : "אירוע ללא כותרת",
- "_%n year_::_%n years_" : ["שנה","שנתיים","%n שנים","%n שנים"],
- "_%n month_::_%n months_" : ["חודש","חודשיים","%n חודשים","%n חודשים"],
- "_%n day_::_%n days_" : ["יום","יומיים","%n ימים","%n ימים"],
- "_%n hour_::_%n hours_" : ["שעה","שעתיים","%n שעות","%n שעות"],
- "_%n minute_::_%n minutes_" : ["דקה","%n דקות","%n דקות","%n דקות"],
- "%s (in %s)" : "%s (בתוך %s)",
- "%s (%s ago)" : "%s (לפני %s)",
- "Calendar: %s" : "לוח שנה: %s",
- "Date: %s" : "תאריך: %s",
- "Description: %s" : "תיאור: %s",
- "Where: %s" : "איפה: %s",
- "%1$s via %2$s" : "%1$s דרך %2$s",
- "Invitation canceled" : "ההזמנה בוטלה",
- "Invitation updated" : "ההזמנה עודכנה",
- "Invitation" : "הזמנה",
- "Title:" : "כותרת:",
- "Time:" : "שעה:",
- "Location:" : "מיקום:",
- "Link:" : "קישור:",
- "Organizer:" : "ארגון:",
- "Attendees:" : "משתתפים:",
- "Accept" : "קבלה",
- "Decline" : "דחייה",
- "More options …" : "אפשרויות נוספות…",
- "More options at %s" : "אפשרויות נוספים ב־%s",
- "Contacts" : "אנשי קשר",
- "Upgrade needed" : "נדרש עדכון",
- "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "עליך להגדיר את ה־%s שלך להשתמש ב־HTTPS כדי להשתמש ב־CalDAV וב־CardDAV עם iOS/macOS.",
- "Configures a CalDAV account" : "מגדיר חשבון CalDAV",
- "Configures a CardDAV account" : "מגדיר חשבון CardDAV",
- "Events" : "אירועים",
- "Tasks" : "משימות",
- "Untitled task" : "משימה ללא כותרת",
- "Completed on %s" : "הושלמה ב־%s",
- "Contacts and groups" : "אנשי קשר וקבוצות",
- "WebDAV" : "WebDAV",
- "WebDAV endpoint" : "נקודת קצה WebDAV",
- "to" : "אל",
- "Monday" : "יום שני",
- "Tuesday" : "יום שלישי",
- "Wednesday" : "יום רביעי",
- "Thursday" : "יום חמישי",
- "Friday" : "יום שישי",
- "Saturday" : "יום שבת",
- "Sunday" : "יום ראשון",
- "Save" : "שמירה",
- "Calendar server" : "שרת לוח שנה",
- "Send invitations to attendees" : "שליחת הזמנות למשתתפים",
- "Automatically generate a birthday calendar" : "יצירת יומן ימי הולדת אוטומטית",
- "Birthday calendars will be generated by a background job." : "יומני ימי הולדת ייווצרו על ידי משימה ברקע.",
- "Hence they will not be available immediately after enabling but will show up after some time." : "לכן הם לא יהיה זמינים מיד לאחר ההפעלה אלא הם יופיעו לאחר זמן מה.",
- "Send notifications for events" : "שליחת התראות לאירועים",
- "Notifications are sent via background jobs, so these must occur often enough." : "התראות נשלחות באמצעות משימות רקע, לכן הבדיקה תתבצע יחסית לעתים קרובות.",
- "Enable notifications for events via push" : "הפעלת התראות לאירועים בדחיפה",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "עליך גם להתקין את {calendarappstoreopen}יישומון לוח שנה{linkclose}, או {calendardocopen}לחבר את שולחן העבודה והמכשיר הנייד שלך לסנכרון ↖{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "נא לוודא שהגדרת את {emailopen}שרת הדוא״ל{linkclose} כראוי.",
- "There was an error updating your attendance status." : "אירעה שגיאה בעת עדכון מצב ההשתתפות שלך.",
- "Please contact the organizer directly." : "נא ליצור קשר עם הגוף מארגן ישירות.",
- "Are you accepting the invitation?" : "האם להיענות להזמנה?",
- "Tentative" : "טנטטיבית",
- "Comment" : "הערה",
- "Your attendance was updated successfully." : "ההשתתפות שלך עודכנה בהצלחה.",
- "Calendar and tasks" : "לוח שנה ומשימות"
-},"pluralForm" :"nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/hr.js b/apps/dav/l10n/hr.js
deleted file mode 100644
index ad034f9b972..00000000000
--- a/apps/dav/l10n/hr.js
+++ /dev/null
@@ -1,157 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Kalendar",
- "Todos" : "Zadaci",
- "Personal" : "Osobno",
- "{actor} created calendar {calendar}" : "{actor} je stvorio kalendar {calendar}",
- "You created calendar {calendar}" : "Stvorili ste kalendar {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} je izbrisao kalendar {calendar}",
- "You deleted calendar {calendar}" : "Izbrisali ste kalendar {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} je ažurirao kalendar {calendar}",
- "You updated calendar {calendar}" : "Ažurirali ste kalendar {calendar}",
- "{actor} restored calendar {calendar}" : "{actor} je vratio kalendar {calendar}",
- "You restored calendar {calendar}" : "Vratili ste kalendar {calendar}",
- "You shared calendar {calendar} as public link" : "Podijelili ste kalendar {calendar} putem javne poveznice",
- "You removed public link for calendar {calendar}" : "Uklonili ste javnu poveznicu na kalendar {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} dijeli kalendar {calendar} s vama",
- "You shared calendar {calendar} with {user}" : "Podijelili ste kalendar {calendar} s {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} dijeli kalendar {calendar} s {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} više ne dijeli kalendar {calendar} s vama",
- "You unshared calendar {calendar} from {user}" : "Više ne dijelite kalendar {calendar} s {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} više ne dijeli kalendar {calendar} s {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} više ne dijeli kalendar {calendar} sam sa sobom",
- "You shared calendar {calendar} with group {group}" : "Podijelili ste kalendar {kalendar} s grupom {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} dijeli kalendar {calendar} s grupom {group}",
- "You unshared calendar {calendar} from group {group}" : "Više ne dijelite kalendar {calendar} s grupom {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} više ne dijeli kalendar {calendar} s grupom {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} je stvorio događaj {event} u kalendaru {calendar}",
- "You created event {event} in calendar {calendar}" : "Stvorili ste događaj {event} u kalendaru {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} je izbrisao događaj {event} iz kalendara {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Izbrisali ste događaj {event} iz kalendara {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} je ažurirao događaj {event} u kalendaru {calendar}",
- "You updated event {event} in calendar {calendar}" : "Ažurirali ste događaj {event} u kalendaru {calendar}",
- "{actor} restored event {event} of calendar {calendar}" : "{actor} je vratio događaj {event} kalendara {calendar}",
- "You restored event {event} of calendar {calendar}" : "Vratili ste događaj {event} kalendara {calendar}",
- "Busy" : "Zauzeto",
- "{actor} created todo {todo} in list {calendar}" : "{actor} je stvorio zadatak {todo} u popisu {calendar}",
- "You created todo {todo} in list {calendar}" : "Stvorili ste zadatak {todo} u popisu {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} je izbrisao zadatak {todo} s popisa {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Izbrisali ste zadatak {todo} s popisa {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} je ažurirao zadatak {todo} u popisu {calendar}",
- "You updated todo {todo} in list {calendar}" : "Ažurirali ste zadatak {todo} u popisu {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} je izvršio zadatak {todo} u popisu {calendar}",
- "You solved todo {todo} in list {calendar}" : "Izvršili ste zadatak {todo} u popisu {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} je ponovno otvorio zadatak {todo} u popisu {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Ponovno ste otvorili zadatak {todo} u popisu {calendar}",
- "Calendar, contacts and tasks" : "Kalendar, kontakti i zadaci",
- "A <strong>calendar</strong> was modified" : "Izmijenjen je <strong>kalendar</strong>",
- "A calendar <strong>event</strong> was modified" : "Izmijenjen je <strong>događaj</strong> u kalendaru",
- "A calendar <strong>todo</strong> was modified" : "Izmijenjen je <strong>zadatak</strong> u kalendaru",
- "Contact birthdays" : "Rođendani kontakata",
- "Death of %s" : "Smrt %s",
- "Calendar:" : "Kalendar:",
- "Date:" : "Datum:",
- "Where:" : "Gdje:",
- "Description:" : "Opis:",
- "Untitled event" : "Događaj bez naslova",
- "_%n year_::_%n years_" : ["%n godina","%n godina","%n godina"],
- "_%n month_::_%n months_" : ["%n mjesec","%n mjeseci","%n mjeseci"],
- "_%n day_::_%n days_" : ["%n dan","%n dana","%n dana"],
- "_%n hour_::_%n hours_" : ["%n sat","%n sati","%n sati"],
- "_%n minute_::_%n minutes_" : ["%n minute/minuta","%n minuta","%n minuta"],
- "%s (in %s)" : "%s (u %s)",
- "%s (%s ago)" : "%s (prije %s)",
- "Calendar: %s" : "Kalendar: %s",
- "Date: %s" : "Datum: %s",
- "Description: %s" : "Opis: %s",
- "Where: %s" : "Gdje: %s",
- "%1$s via %2$s" : "%1$s putem %2$s",
- "Cancelled: %1$s" : "Otkazano: %1$s",
- "Invitation canceled" : "Poziv je otkazan",
- "Re: %1$s" : "Odgovor: %1 $ s",
- "Invitation updated" : "Poziv je ažuriran",
- "Invitation: %1$s" : "Pozivnica: %1$s",
- "Invitation" : "Pozivnica",
- "Title:" : "Naslov:",
- "Time:" : "Vrijeme:",
- "Location:" : "Lokacija:",
- "Link:" : "Poveznica:",
- "Organizer:" : "Organizator:",
- "Attendees:" : "Polaznici:",
- "Accept" : "Prihvati",
- "Decline" : "Odbij",
- "More options …" : "Više mogućnosti…",
- "More options at %s" : "Više mogućnosti na %s",
- "Contacts" : "Kontakti",
- "{actor} created address book {addressbook}" : "{actor} je stvorio adresar {addressbook}",
- "You created address book {addressbook}" : "Stvorili ste adresar {addressbook}",
- "{actor} deleted address book {addressbook}" : "{actor} je izbrisao adresar {addressbook}",
- "You deleted address book {addressbook}" : "Izbrisali ste adresar {addressbook}",
- "{actor} updated address book {addressbook}" : "{actor} je ažurirao adresar {addressbook}",
- "You updated address book {addressbook}" : "Ažurirali ste adresar {addressbook}",
- "{actor} shared address book {addressbook} with you" : "{actor} dijeli adresar {addressbook} s vama",
- "You shared address book {addressbook} with {user}" : "Dijelite adresar {addressbook} s {user}",
- "{actor} shared address book {addressbook} with {user}" : "{actor} dijeli adresar {addressbook} s {user}",
- "{actor} unshared address book {addressbook} from you" : "{actor} je prestao dijeliti adresar {addressbook} s vama",
- "You unshared address book {addressbook} from {user}" : "Prestali ste dijeliti adresar {addressbook} s {user}",
- "{actor} unshared address book {addressbook} from {user}" : "{actor} je prestao dijeliti adresar {addressbook} s {user}",
- "{actor} unshared address book {addressbook} from themselves" : "{actor} je prestao dijeliti adresar {addressbook} sa sobom",
- "You shared address book {addressbook} with group {group}" : "Dijelite adresar {addressbook} s grupom {group}",
- "{actor} shared address book {addressbook} with group {group}" : "{actor} dijeli adresar {addressbook} s grupom {group}",
- "You unshared address book {addressbook} from group {group}" : "Prestali ste dijeliti adresar {addressbook} s grupom {group}",
- "{actor} unshared address book {addressbook} from group {group}" : "{actor} je prestao dijeliti adresar {addressbook} s grupom {group}",
- "{actor} created contact {card} in address book {addressbook}" : "{actor} je stvorio kontakt {card} u adresaru {addressbook}",
- "You created contact {card} in address book {addressbook}" : "Stvorili ste kontakt {card} u adresaru {addressbook}",
- "{actor} deleted contact {card} from address book {addressbook}" : "{actor} je izbrisao kontakt {card} iz adresara {addressbook}",
- "You deleted contact {card} from address book {addressbook}" : "Izbrisali ste kontakt {card} iz adresara {addressbook}",
- "{actor} updated contact {card} in address book {addressbook}" : "{actor} je ažurirao kontakt {card} u adresaru {addressbook}",
- "You updated contact {card} in address book {addressbook}" : "Ažurirali ste kontakt {card} u adresaru {addressbook}",
- "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Izmijenjen je <strong>kontakt</strong> ili <strong>adresar</strong>",
- "System is in maintenance mode." : "Sustav je u načinu održavanja.",
- "Upgrade needed" : "Potrebno nadograditi",
- "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Vaš %s treba konfigurirati za korištenje HTTPS-a kako bi se mogli upotrebljavati CalDAV i CardDAV s operacijskim sustavom iOS/macOS.",
- "Configures a CalDAV account" : "Konfigurira CalDAV račun",
- "Configures a CardDAV account" : "Konfigurira CardDAV račun",
- "Events" : "Događaji",
- "Tasks" : "Zadaci",
- "Untitled task" : "Zadatak bez naslova",
- "Completed on %s" : "Završeno na %s",
- "Due on %s by %s" : "%s treba završiti do %s",
- "Due on %s" : "Treba završiti do %s",
- "Contacts and groups" : "Kontakti i grupe",
- "WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV krajnja točka",
- "Availability" : "Raspoloživost",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Ako konfigurirate svoje radno vrijeme, drugi korisnici moći će vidjeti jeste li izvan ureda kada rezerviraju sastanak.",
- "to" : "do",
- "Delete slot" : "Izbriši mjesto",
- "Add slot" : "Dodaj mjesto",
- "Monday" : "Ponedjeljak",
- "Tuesday" : "Utorak",
- "Wednesday" : "Srijeda",
- "Thursday" : "Četvrtak",
- "Friday" : "Petak",
- "Saturday" : "Subota",
- "Sunday" : "Nedjelja",
- "Save" : "Spremi",
- "Calendar server" : "Poslužitelj kalendara",
- "Send invitations to attendees" : "Pošaljite pozive sudionicima",
- "Automatically generate a birthday calendar" : "Automatski generiraj kalendar rođendana",
- "Birthday calendars will be generated by a background job." : "Kalendari rođendana generirat će se u pozadini.",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Stoga neće biti dostupni odmah nakon omogućivanja, ali će se pojaviti nakon nekog vremena.",
- "Send notifications for events" : "Šalji obavijesti o događajima",
- "Notifications are sent via background jobs, so these must occur often enough." : "Obavijesti se šalju putem pozadinskih zadataka koji se moraju dovoljno često izvoditi.",
- "Enable notifications for events via push" : "Omogući slanje obavijesti o događajima putem push obavijesti",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Također instalirajte {calendarappstoreopen}aplikaciju Kalendar{linkclose} ili {calendardocopen}povežite računalo i mobilni uređaj radi sinkroniziranja ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Provjerite jeste li ispravno postavili {emailopen}poslužitelj e-pošte{linkclose}.",
- "There was an error updating your attendance status." : "Došlo je do pogreške prilikom ažuriranja statusa prisutnosti.",
- "Please contact the organizer directly." : "Izravno se obratite organizatoru.",
- "Are you accepting the invitation?" : "Prihvaćate li poziv?",
- "Tentative" : "Uvjetno",
- "Number of guests" : "Broj gostiju",
- "Comment" : "Komentar",
- "Your attendance was updated successfully." : "Vaša je prisutnost uspješno ažurirana.",
- "Calendar and tasks" : "Kalendar i zadaci"
-},
-"nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;");
diff --git a/apps/dav/l10n/hr.json b/apps/dav/l10n/hr.json
deleted file mode 100644
index 95c0c9e6a1b..00000000000
--- a/apps/dav/l10n/hr.json
+++ /dev/null
@@ -1,155 +0,0 @@
-{ "translations": {
- "Calendar" : "Kalendar",
- "Todos" : "Zadaci",
- "Personal" : "Osobno",
- "{actor} created calendar {calendar}" : "{actor} je stvorio kalendar {calendar}",
- "You created calendar {calendar}" : "Stvorili ste kalendar {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} je izbrisao kalendar {calendar}",
- "You deleted calendar {calendar}" : "Izbrisali ste kalendar {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} je ažurirao kalendar {calendar}",
- "You updated calendar {calendar}" : "Ažurirali ste kalendar {calendar}",
- "{actor} restored calendar {calendar}" : "{actor} je vratio kalendar {calendar}",
- "You restored calendar {calendar}" : "Vratili ste kalendar {calendar}",
- "You shared calendar {calendar} as public link" : "Podijelili ste kalendar {calendar} putem javne poveznice",
- "You removed public link for calendar {calendar}" : "Uklonili ste javnu poveznicu na kalendar {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} dijeli kalendar {calendar} s vama",
- "You shared calendar {calendar} with {user}" : "Podijelili ste kalendar {calendar} s {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} dijeli kalendar {calendar} s {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} više ne dijeli kalendar {calendar} s vama",
- "You unshared calendar {calendar} from {user}" : "Više ne dijelite kalendar {calendar} s {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} više ne dijeli kalendar {calendar} s {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} više ne dijeli kalendar {calendar} sam sa sobom",
- "You shared calendar {calendar} with group {group}" : "Podijelili ste kalendar {kalendar} s grupom {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} dijeli kalendar {calendar} s grupom {group}",
- "You unshared calendar {calendar} from group {group}" : "Više ne dijelite kalendar {calendar} s grupom {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} više ne dijeli kalendar {calendar} s grupom {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} je stvorio događaj {event} u kalendaru {calendar}",
- "You created event {event} in calendar {calendar}" : "Stvorili ste događaj {event} u kalendaru {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} je izbrisao događaj {event} iz kalendara {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Izbrisali ste događaj {event} iz kalendara {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} je ažurirao događaj {event} u kalendaru {calendar}",
- "You updated event {event} in calendar {calendar}" : "Ažurirali ste događaj {event} u kalendaru {calendar}",
- "{actor} restored event {event} of calendar {calendar}" : "{actor} je vratio događaj {event} kalendara {calendar}",
- "You restored event {event} of calendar {calendar}" : "Vratili ste događaj {event} kalendara {calendar}",
- "Busy" : "Zauzeto",
- "{actor} created todo {todo} in list {calendar}" : "{actor} je stvorio zadatak {todo} u popisu {calendar}",
- "You created todo {todo} in list {calendar}" : "Stvorili ste zadatak {todo} u popisu {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} je izbrisao zadatak {todo} s popisa {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Izbrisali ste zadatak {todo} s popisa {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} je ažurirao zadatak {todo} u popisu {calendar}",
- "You updated todo {todo} in list {calendar}" : "Ažurirali ste zadatak {todo} u popisu {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} je izvršio zadatak {todo} u popisu {calendar}",
- "You solved todo {todo} in list {calendar}" : "Izvršili ste zadatak {todo} u popisu {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} je ponovno otvorio zadatak {todo} u popisu {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Ponovno ste otvorili zadatak {todo} u popisu {calendar}",
- "Calendar, contacts and tasks" : "Kalendar, kontakti i zadaci",
- "A <strong>calendar</strong> was modified" : "Izmijenjen je <strong>kalendar</strong>",
- "A calendar <strong>event</strong> was modified" : "Izmijenjen je <strong>događaj</strong> u kalendaru",
- "A calendar <strong>todo</strong> was modified" : "Izmijenjen je <strong>zadatak</strong> u kalendaru",
- "Contact birthdays" : "Rođendani kontakata",
- "Death of %s" : "Smrt %s",
- "Calendar:" : "Kalendar:",
- "Date:" : "Datum:",
- "Where:" : "Gdje:",
- "Description:" : "Opis:",
- "Untitled event" : "Događaj bez naslova",
- "_%n year_::_%n years_" : ["%n godina","%n godina","%n godina"],
- "_%n month_::_%n months_" : ["%n mjesec","%n mjeseci","%n mjeseci"],
- "_%n day_::_%n days_" : ["%n dan","%n dana","%n dana"],
- "_%n hour_::_%n hours_" : ["%n sat","%n sati","%n sati"],
- "_%n minute_::_%n minutes_" : ["%n minute/minuta","%n minuta","%n minuta"],
- "%s (in %s)" : "%s (u %s)",
- "%s (%s ago)" : "%s (prije %s)",
- "Calendar: %s" : "Kalendar: %s",
- "Date: %s" : "Datum: %s",
- "Description: %s" : "Opis: %s",
- "Where: %s" : "Gdje: %s",
- "%1$s via %2$s" : "%1$s putem %2$s",
- "Cancelled: %1$s" : "Otkazano: %1$s",
- "Invitation canceled" : "Poziv je otkazan",
- "Re: %1$s" : "Odgovor: %1 $ s",
- "Invitation updated" : "Poziv je ažuriran",
- "Invitation: %1$s" : "Pozivnica: %1$s",
- "Invitation" : "Pozivnica",
- "Title:" : "Naslov:",
- "Time:" : "Vrijeme:",
- "Location:" : "Lokacija:",
- "Link:" : "Poveznica:",
- "Organizer:" : "Organizator:",
- "Attendees:" : "Polaznici:",
- "Accept" : "Prihvati",
- "Decline" : "Odbij",
- "More options …" : "Više mogućnosti…",
- "More options at %s" : "Više mogućnosti na %s",
- "Contacts" : "Kontakti",
- "{actor} created address book {addressbook}" : "{actor} je stvorio adresar {addressbook}",
- "You created address book {addressbook}" : "Stvorili ste adresar {addressbook}",
- "{actor} deleted address book {addressbook}" : "{actor} je izbrisao adresar {addressbook}",
- "You deleted address book {addressbook}" : "Izbrisali ste adresar {addressbook}",
- "{actor} updated address book {addressbook}" : "{actor} je ažurirao adresar {addressbook}",
- "You updated address book {addressbook}" : "Ažurirali ste adresar {addressbook}",
- "{actor} shared address book {addressbook} with you" : "{actor} dijeli adresar {addressbook} s vama",
- "You shared address book {addressbook} with {user}" : "Dijelite adresar {addressbook} s {user}",
- "{actor} shared address book {addressbook} with {user}" : "{actor} dijeli adresar {addressbook} s {user}",
- "{actor} unshared address book {addressbook} from you" : "{actor} je prestao dijeliti adresar {addressbook} s vama",
- "You unshared address book {addressbook} from {user}" : "Prestali ste dijeliti adresar {addressbook} s {user}",
- "{actor} unshared address book {addressbook} from {user}" : "{actor} je prestao dijeliti adresar {addressbook} s {user}",
- "{actor} unshared address book {addressbook} from themselves" : "{actor} je prestao dijeliti adresar {addressbook} sa sobom",
- "You shared address book {addressbook} with group {group}" : "Dijelite adresar {addressbook} s grupom {group}",
- "{actor} shared address book {addressbook} with group {group}" : "{actor} dijeli adresar {addressbook} s grupom {group}",
- "You unshared address book {addressbook} from group {group}" : "Prestali ste dijeliti adresar {addressbook} s grupom {group}",
- "{actor} unshared address book {addressbook} from group {group}" : "{actor} je prestao dijeliti adresar {addressbook} s grupom {group}",
- "{actor} created contact {card} in address book {addressbook}" : "{actor} je stvorio kontakt {card} u adresaru {addressbook}",
- "You created contact {card} in address book {addressbook}" : "Stvorili ste kontakt {card} u adresaru {addressbook}",
- "{actor} deleted contact {card} from address book {addressbook}" : "{actor} je izbrisao kontakt {card} iz adresara {addressbook}",
- "You deleted contact {card} from address book {addressbook}" : "Izbrisali ste kontakt {card} iz adresara {addressbook}",
- "{actor} updated contact {card} in address book {addressbook}" : "{actor} je ažurirao kontakt {card} u adresaru {addressbook}",
- "You updated contact {card} in address book {addressbook}" : "Ažurirali ste kontakt {card} u adresaru {addressbook}",
- "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Izmijenjen je <strong>kontakt</strong> ili <strong>adresar</strong>",
- "System is in maintenance mode." : "Sustav je u načinu održavanja.",
- "Upgrade needed" : "Potrebno nadograditi",
- "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Vaš %s treba konfigurirati za korištenje HTTPS-a kako bi se mogli upotrebljavati CalDAV i CardDAV s operacijskim sustavom iOS/macOS.",
- "Configures a CalDAV account" : "Konfigurira CalDAV račun",
- "Configures a CardDAV account" : "Konfigurira CardDAV račun",
- "Events" : "Događaji",
- "Tasks" : "Zadaci",
- "Untitled task" : "Zadatak bez naslova",
- "Completed on %s" : "Završeno na %s",
- "Due on %s by %s" : "%s treba završiti do %s",
- "Due on %s" : "Treba završiti do %s",
- "Contacts and groups" : "Kontakti i grupe",
- "WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV krajnja točka",
- "Availability" : "Raspoloživost",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Ako konfigurirate svoje radno vrijeme, drugi korisnici moći će vidjeti jeste li izvan ureda kada rezerviraju sastanak.",
- "to" : "do",
- "Delete slot" : "Izbriši mjesto",
- "Add slot" : "Dodaj mjesto",
- "Monday" : "Ponedjeljak",
- "Tuesday" : "Utorak",
- "Wednesday" : "Srijeda",
- "Thursday" : "Četvrtak",
- "Friday" : "Petak",
- "Saturday" : "Subota",
- "Sunday" : "Nedjelja",
- "Save" : "Spremi",
- "Calendar server" : "Poslužitelj kalendara",
- "Send invitations to attendees" : "Pošaljite pozive sudionicima",
- "Automatically generate a birthday calendar" : "Automatski generiraj kalendar rođendana",
- "Birthday calendars will be generated by a background job." : "Kalendari rođendana generirat će se u pozadini.",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Stoga neće biti dostupni odmah nakon omogućivanja, ali će se pojaviti nakon nekog vremena.",
- "Send notifications for events" : "Šalji obavijesti o događajima",
- "Notifications are sent via background jobs, so these must occur often enough." : "Obavijesti se šalju putem pozadinskih zadataka koji se moraju dovoljno često izvoditi.",
- "Enable notifications for events via push" : "Omogući slanje obavijesti o događajima putem push obavijesti",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Također instalirajte {calendarappstoreopen}aplikaciju Kalendar{linkclose} ili {calendardocopen}povežite računalo i mobilni uređaj radi sinkroniziranja ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Provjerite jeste li ispravno postavili {emailopen}poslužitelj e-pošte{linkclose}.",
- "There was an error updating your attendance status." : "Došlo je do pogreške prilikom ažuriranja statusa prisutnosti.",
- "Please contact the organizer directly." : "Izravno se obratite organizatoru.",
- "Are you accepting the invitation?" : "Prihvaćate li poziv?",
- "Tentative" : "Uvjetno",
- "Number of guests" : "Broj gostiju",
- "Comment" : "Komentar",
- "Your attendance was updated successfully." : "Vaša je prisutnost uspješno ažurirana.",
- "Calendar and tasks" : "Kalendar i zadaci"
-},"pluralForm" :"nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/hu.js b/apps/dav/l10n/hu.js
index bd65bb758ef..d45c4d340dd 100644
--- a/apps/dav/l10n/hu.js
+++ b/apps/dav/l10n/hu.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Naptár",
- "Todos" : "Teendők",
+ "Tasks" : "Feladatok",
"Personal" : "Személyes",
"{actor} created calendar {calendar}" : "{actor} létrehozta a naptárt: {calendar}",
"You created calendar {calendar}" : "Létrehozta a naptárt: {calendar}",
@@ -25,36 +25,41 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} megosztotta a(z) {calendar} naptárt a következő csoporttal: {group}",
"You unshared calendar {calendar} from group {group}" : "Visszavonta a(z) {calendar} naptár magosztását a következő csoporttól: {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} visszavonta a(z) {calendar} naptár megosztását a következő csoporttól: {group}",
+ "Untitled event" : "Névtelen esemény",
"{actor} created event {event} in calendar {calendar}" : "{actor} létrehozta a(z) {event} eseményt a következő naptárban: {calendar}",
"You created event {event} in calendar {calendar}" : "Létrehozta a(z) {event} eseményt a következő naptárban: {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} törölte a(z) {event} eseményt a következő naptárból: {calendar}",
"You deleted event {event} from calendar {calendar}" : "Törölte a(z) {event} eseményt a következő naptárból: {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} frissítette a(z) {event} eseményt a következő naptárban: {calendar}",
"You updated event {event} in calendar {calendar}" : "Frissítette a(z) {event} eseményt a következő naptárban: {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} áthelyezte a(z) {event} eseményt a(z) {sourceCalendar} naptárból a(z) {targetCalendar} naptárba",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Áthelyezte a(z) {event} eseményt a(z) {sourceCalendar} naptárból a(z) {targetCalendar} naptárba",
"{actor} restored event {event} of calendar {calendar}" : "{actor} helyreállította a(z) {calendar} naptár következő eseményét: {event}",
"You restored event {event} of calendar {calendar}" : "Helyreállította a(z) {calendar} naptár következő eseményét: {event}",
"Busy" : "Foglalt",
- "{actor} created todo {todo} in list {calendar}" : "{actor} létrehozta a(z) {todo} teendőt a következő listában: {calendar}",
- "You created todo {todo} in list {calendar}" : "Létrehozta a(z) {todo} teendőt a következő listában: {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} törölte a(z) {todo} teendőt a következő listából: {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Törölte a(z) {todo} teendőt a következő listából: {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} frissítette a(z) {todo} teendőt a következő listában: {calendar}",
- "You updated todo {todo} in list {calendar}" : "Frissítette a(z) {todo} teendőt a következő listában: {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} elintézte a(z) {todo} teendőt a következő listában: {calendar}",
- "You solved todo {todo} in list {calendar}" : "Elintézte a(z) {todo} teendőt a következő listában: {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} újranyitotta a(z) {todo} teendőt a következő listában: {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Újranyitotta a(z) {todo} teendőt a következő listában: {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} létrehozta a(z) {todo} teendőt a következő listában: {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Létrehozta a(z) {todo} teendőt a következő listában: {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} törölte a(z) {todo} teendőt a következő listából: {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Törölte a(z) {todo} teendőt a következő listából: {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} frissítette a(z) {todo} teendőt a következő listában: {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Frissítette a(z) {todo} teendőt a következő listában: {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} elintézte a(z) {todo} teendőt a következő listában: {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Elintézte a(z) {todo} teendőt a következő listában: {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} újranyitotta a(z) {todo} teendőt a következő listában: {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Újranyitotta a(z) {todo} teendőt a következő listában: {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} áthelyezte a(z) {todo} teendőt a(z) {sourceCalendar} listából a(z) {targetCalendar} listába",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Áthelyezte a(z) {todo} teendőt a(z) {sourceCalendar} listából a(z) {targetCalendar} listába",
"Calendar, contacts and tasks" : "Naptár, címjegyzék és feladatok",
"A <strong>calendar</strong> was modified" : "Egy <strong>naptár</strong> megváltozott",
"A calendar <strong>event</strong> was modified" : "Egy <strong>naptáresemény</strong> megváltozott",
- "A calendar <strong>todo</strong> was modified" : "Egy <strong>naptárteendő</strong> megváltozott",
+ "A calendar <strong>to-do</strong> was modified" : "Egy <strong>naptárteendő</strong> megváltozott",
"Contact birthdays" : "Névjegyek születésnapjai",
"Death of %s" : "%s halála",
+ "Untitled calendar" : "Névtelen naptár",
"Calendar:" : "Naptár:",
"Date:" : "Dátum:",
"Where:" : "Hol:",
"Description:" : "Leírás:",
- "Untitled event" : "Névtelen esemény",
"_%n year_::_%n years_" : ["%n év","%n év"],
"_%n month_::_%n months_" : ["%n hónap","%n hónap"],
"_%n day_::_%n days_" : ["%n nap","%n nap"],
@@ -67,22 +72,89 @@ OC.L10N.register(
"Description: %s" : "Leírás: %s",
"Where: %s" : "Hely: %s",
"%1$s via %2$s" : "%1$s – %2$s",
+ "Could not generate when statement" : "Nem sikerült létrehozni amikor az állítást",
+ "Every Day for the entire day" : "Minden Nap a teljes napra",
+ "Every Day for the entire day until %1$s" : "Minden Nap a teljes napon eddig %1$s",
+ "Every Day between %1$s - %2$s" : "Minden Nap %1$s - %2$sközött",
+ "Every Day between %1$s - %2$s until %3$s" : "Minden Nap %1$s - %2$s között %3$s-ig",
+ "Every %1$d Days for the entire day" : "Minden %1$d Nap az teljes napra",
+ "Every %1$d Days for the entire day until %2$s" : "Minden%1$d Nap a teljes napra %2$s-ig",
+ "Every %1$d Days between %2$s - %3$s" : "Minden%1$d Nap %2$s - %3$s között",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Minden%1$d Napon %2$s - %3$s között %4$s-ig",
+ "Could not generate event recurrence statement" : "Nem sikerült megújuló eseményt létrehozni",
+ "Every Week on %1$s for the entire day" : "Minden hét %1$s napon a teljes napja",
+ "Every Week on %1$s for the entire day until %2$s" : "Minden Héten %1$s a teljes napra %2$s-ig",
+ "Every Week on %1$s between %2$s - %3$s" : "Minden Hét %1$s napján %2$s - %3$sközött",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Minden Hét %1$s napján %2$s - %3$s között %4$s-ig",
+ "Every %1$d Weeks on %2$s for the entire day" : "Minden%1$d héten %2$s napon a teljes napra",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Minden%1$d Héten %2$s napján a teljes napon %3$s-ig",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Minden%1$d hét %2$s napján %3$s - %4$sközött",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Minden%1$d héten %2$s napján %3$s - %4$s között eddig %5$s",
+ "Every Month on the %1$s for the entire day" : "Minden hónap %1$s napján a teljes napon",
+ "Every Month on the %1$s for the entire day until %2$s" : "Minden hónap %1$s napján a teljes napon %2$s-ig",
+ "Every Month on the %1$s between %2$s - %3$s" : "Minden hónap %1$s napján %2$sés %3$s között",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Minden hónap %1$s napján %2$sés %3$s között %4$s-ig",
+ "Every %1$d Months on the %2$s for the entire day" : "Minden%1$d Hónap %2$s napján a teljes napon",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Minden%1$d Hónap %2$s napján a teljes napon %3$s-ig",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Minden%1$d Hónap %2$s napján %3$s és %4$s között",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Minden%1$d Hónap %2$s napján %3$s és %4$s között %5$s-ig",
+ "Every Year in %1$s on the %2$s for the entire day" : "Minden év %1$s hónap %2$s napján a teljes napra",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Minden év %1$s hónapjában %2$s napján a teljes napon %3$s-ig",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Minden év %1$s hónapjában %2$s napján %3$s - %4$sközött.",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Minden év %1$s hónapjában %2$s napján %3$s - %4$s között eddig %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Minden%1$d év %2$s hónapján %3$s napján a teljes napra",
+ "On specific dates for the entire day until %1$s" : "Egy megadott időpontban a teljes napon %1$s-ig ",
+ "On specific dates between %1$s - %2$s until %3$s" : "Egy megadott időpontban %1$s és %2$s %3$s-ig",
+ "Could not generate next recurrence statement" : "Nem sikerült a következő megújuló eseményt létrehozni",
"Cancelled: %1$s" : "Lemondva: %1$s",
- "Invitation canceled" : "Meghívás lemondva",
+ "\"%1$s\" has been canceled" : "A következőt le lett mondva: „%1$s”",
"Re: %1$s" : "Vá: %1$s",
- "Invitation updated" : "Meghívó frissítve",
+ "%1$s has accepted your invitation" : "%1$s elfogadta a meghívását",
+ "%1$s has tentatively accepted your invitation" : "%1$s feltételesen elfogadta a meghívását",
+ "%1$s has declined your invitation" : "%1$s elutasította a meghívását",
+ "%1$s has responded to your invitation" : "%1$s válaszolt a meghívására",
+ "Invitation updated: %1$s" : "Meghívó frissítve: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s frissítette a következő eseményt: %2$s",
"Invitation: %1$s" : "Meghívó: %1$s",
- "Invitation" : "Meghívó",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s meg szeretné hívni a következőre: „%2$s”",
+ "Organizer:" : "Szervező:",
+ "Attendees:" : "Résztvevők:",
"Title:" : "Cím:",
- "Time:" : "Idő:",
+ "When:" : "Mikor:",
"Location:" : "Hely:",
"Link:" : "Hivatkozás:",
- "Organizer:" : "Szervező:",
- "Attendees:" : "Résztvevők:",
+ "Occurring:" : "Előforduló",
"Accept" : "Elfogadás",
"Decline" : "Elutasítás",
"More options …" : "További lehetőségek…",
"More options at %s" : "További lehetőségek itt: %s",
+ "Monday" : "Hétfő",
+ "Tuesday" : "Kedd",
+ "Wednesday" : "Szerda",
+ "Thursday" : "Csütörtök",
+ "Friday" : "Péntek",
+ "Saturday" : "Szombat",
+ "Sunday" : "Vasárnap",
+ "January" : "Január",
+ "February" : "Február",
+ "March" : "Március",
+ "April" : "Április",
+ "May" : "Május",
+ "June" : "Június",
+ "July" : "Július",
+ "August" : "Augusztus",
+ "September" : "Szeptember",
+ "October" : "Október",
+ "November" : "November",
+ "December" : "December",
+ "First" : "Első",
+ "Second" : "Másodperc",
+ "Third" : "Harmadik",
+ "Fourth" : "Negyedik",
+ "Last" : "Utolsó",
+ "Second Last" : "Második Utolsó",
+ "Third Last" : "Harmadik Utolsó",
+ "Fourth Last" : "Negyedik Utolsó",
"Contacts" : "Névjegyek",
"{actor} created address book {addressbook}" : "{actor} létrehozta a következő címjegyzéket: {addressbook}",
"You created address book {addressbook}" : "Létrehozta a következő címjegyzéket: {addressbook}",
@@ -108,7 +180,10 @@ OC.L10N.register(
"{actor} updated contact {card} in address book {addressbook}" : "{actor} aktualizálta a(z) {card} névjegyet a következő címjegyzékben: {addressbook}",
"You updated contact {card} in address book {addressbook}" : "Aktualizálta a(z) {card} névjegyet a következő címjegyzékben: {addressbook}",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "Egy <strong>névjegy</strong> vagy <strong>címjegyzék</strong> módosítva lett",
+ "Accounts" : "Fiókok",
+ "System address book which holds all accounts" : "A rendszer címjegyzéke, amely az összes fiókot tartalmazza",
"File is not updatable: %1$s" : "A fájl nem frissíthető: %1$s",
+ "Failed to get storage for file" : "Nem sikerlt a tárhely lekérése a fájl számára",
"Could not write to final file, canceled by hook" : "A végleges fájl nem írható, a hurok megszakította",
"Could not write file contents" : "A fájl tartalma nem írható",
"_%n byte_::_%n bytes_" : ["%n bájt","%n bájt"],
@@ -117,45 +192,68 @@ OC.L10N.register(
"Could not rename part file to final file, canceled by hook" : "A részleges fájl nem nevezhető át a végleges fájllá, a hurok megszakította",
"Could not rename part file to final file" : "A részleges fájl nem nevezhető át a végleges fájllá",
"Failed to check file size: %1$s" : "A fájlméret nem ellenőrizhető: %1$s",
- "Could not open file" : "A fájl nem nyitható meg",
"Encryption not ready: %1$s" : "A titkosítás nincs kész: %1$s",
"Failed to open file: %1$s" : "A fájl megnyitása sikertelen: %1$s",
"Failed to unlink: %1$s" : "A hivatkozás eltávolítása sikertelen: %1$s",
- "Invalid chunk name" : "Érvénytelen darabnév",
- "Could not rename part file assembled from chunks" : "Nem lehet átnevezni a darabokból összeállított részleges fájlt",
"Failed to write file contents: %1$s" : "A fájl tartalmának kiírása sikertelen: %1$s",
"File not found: %1$s" : "A fájl nem található: %1$s",
+ "Invalid target path" : "Érvénytelen elérési útvonal",
"System is in maintenance mode." : "A rendszer karbantartási módban van.",
"Upgrade needed" : "Frissítés szükséges",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "A %s kiszolgálót HTTPS használatára kell beállítani, hogy iOS-szel/macOS-szel használhassa CalDAV-ot és a CardDAV-ot.",
"Configures a CalDAV account" : "Beállítja a CalDAV-fiókot",
"Configures a CardDAV account" : "Beállítja a CardDAV-fiókot",
"Events" : "Események",
- "Tasks" : "Feladatok",
"Untitled task" : "Névtelen feladat",
"Completed on %s" : "Befejezve: %s",
"Due on %s by %s" : "Esedékesség: %s, %s által",
"Due on %s" : "Esedékesség: %s",
+ "DAV system address book" : "DAV rendszer címjegyzék",
+ "No outstanding DAV system address book sync." : "Nincs kiemelkedő DAV rendszer címjegyzék szinkronizálás.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "A DAV rendszer címjegyzék szinkronizációja még nem futott le, mert több, mint 1000 felhasználó található vagy hiba történt. Kérem futtassa manuálisan a következő paranccsal: \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "WebDAV végpont",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "A webkiszolgáló nincs megfelelően beállítva a fájlok szinkronizálásához, mert a WebDAV interfész hibásnak tűnik.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "A webkisolgáló megfelelően van konfigurálva a WebDAVon keresztüli fájl szinkronizáció engedélyezéséhez.",
"Migrated calendar (%1$s)" : "Átköltöztetett naptár (%1$s)",
"Calendars including events, details and attendees" : "Naptárak eseményekkel, részletekkel és résztvevőkkel",
"Contacts and groups" : "Névjegyek és csoportok",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV végpont",
- "Availability" : "Elérhetőség",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Ha beállítja a munkaidejét, akkor más felhasználók a megbeszélések létrehozásakor látni fogják, hogy Ön mikor nem érhető el.",
+ "Absence saved" : "Távollét mentve",
+ "Failed to save your absence settings" : "Nem sikerlt a távolléti beállítások mentése",
+ "Absence cleared" : "Távollét törölve",
+ "Failed to clear your absence settings" : "Nem sikerült a távolléti beállítások törlése",
+ "First day" : "Első nap",
+ "Last day (inclusive)" : "Utolsó nap (beleértve)",
+ "Out of office replacement (optional)" : "Távolléti helyettesítő (nem kötelező)",
+ "Name of the replacement" : "Helyettesítő neve",
+ "No results." : "Nincs eredmény.",
+ "Start typing." : "Kezdjen gépelni.",
+ "Short absence status" : "Rövid távolléti állapot",
+ "Long absence Message" : "Hosszú távolléti állapot",
+ "Save" : "Mentés",
+ "Disable absence" : "Távollét tiltása",
+ "Failed to load availability" : "Az elérhetőség betöltése sikertelen",
+ "Saved availability" : "Elérhetőség mentve",
+ "Failed to save availability" : "Az elérhetőség mentése sikertelen",
"Time zone:" : "Időzóna:",
"to" : "–",
"Delete slot" : "Idősáv törlése",
"No working hours set" : "Nincs munkaidő beállítva",
"Add slot" : "Idősáv hozzáadása",
- "Monday" : "Hétfő",
- "Tuesday" : "Kedd",
- "Wednesday" : "Szerda",
- "Thursday" : "Csütörtök",
- "Friday" : "Péntek",
- "Saturday" : "Szombat",
- "Sunday" : "Vasárnap",
- "Save" : "Mentés",
+ "Weekdays" : "Hétköznapok",
+ "Pick a start time for {dayName}" : "Válassz kezdő dátumot a {dayName}-hoz",
+ "Pick a end time for {dayName}" : "Válassz vége dátumot a {dayName}-hoz",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Az elérhetőségi időn túl, a felhasználói állapot automatikus beállítása „Ne zavarjanak” módba az összes értesítés némításához.",
+ "Cancel" : "Mégse",
+ "Import" : "Importálás",
+ "Error while saving settings" : "Hiba a beállítások mentése során",
+ "Reset to default" : "Visszaállítás alapértelmezettre",
+ "Availability" : "Elérhetőség",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Ha beállítja a munkaidejét, akkor más emberek a megbeszélések létrehozásakor látni fogják, hogy Ön mikor nem érhető el.",
+ "Absence" : "Távollét",
+ "Configure your next absence period." : "A követkeő távolléti periódus beállítása.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Telepítse a {calendarappstoreopen}Naptár alkalmazást{linkclose}, vagy {calendardocopen}csatlakoztassa asztali számítógépét és mobilját a szinkronizáláshoz ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Ne felejtse el megfelelően beállítani az {emailopen}e-mail kiszolgálót{linkclose}.",
"Calendar server" : "Naptárkiszolgáló",
"Send invitations to attendees" : "Meghívó küldése a résztvevőknek",
"Automatically generate a birthday calendar" : "Születésnapokat tartalmazó naptár automatikus létrehozása",
@@ -166,15 +264,10 @@ OC.L10N.register(
"Send reminder notifications to calendar sharees as well" : "Emlékeztető értesítések küldése azoknak is, akikkel a naptár meg van osztva",
"Reminders are always sent to organizers and attendees." : "Az értesítések mindig a szervezőknek és a résztvevőknek lesznek elküldve.",
"Enable notifications for events via push" : "Leküldéses értesítések engedélyezése az eseményekhez",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Telepítse a {calendarappstoreopen}Naptár alkalmazást{linkclose}, vagy {calendardocopen}csatlakoztassa asztali számítógépét és mobilját a szinkronizáláshoz ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Ne felejtse el megfelelően beállítani az {emailopen}e-mail kiszolgálót{linkclose}.",
"There was an error updating your attendance status." : "Hiba történt a részvételi állapotának frissítése során.",
"Please contact the organizer directly." : "Vegye fel a kapcsolatot közvetlenül a szervezővel.",
"Are you accepting the invitation?" : "Elfogadja az meghívást?",
"Tentative" : "Feltételes",
- "Number of guests" : "Vendégek száma",
- "Comment" : "Megjegyzés",
- "Your attendance was updated successfully." : "A részvétele frissítése sikeres.",
- "Calendar and tasks" : "Naptár és feladatok"
+ "Your attendance was updated successfully." : "A részvétele frissítése sikeres."
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/hu.json b/apps/dav/l10n/hu.json
index 598af00a439..2e623866c60 100644
--- a/apps/dav/l10n/hu.json
+++ b/apps/dav/l10n/hu.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Naptár",
- "Todos" : "Teendők",
+ "Tasks" : "Feladatok",
"Personal" : "Személyes",
"{actor} created calendar {calendar}" : "{actor} létrehozta a naptárt: {calendar}",
"You created calendar {calendar}" : "Létrehozta a naptárt: {calendar}",
@@ -23,36 +23,41 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} megosztotta a(z) {calendar} naptárt a következő csoporttal: {group}",
"You unshared calendar {calendar} from group {group}" : "Visszavonta a(z) {calendar} naptár magosztását a következő csoporttól: {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} visszavonta a(z) {calendar} naptár megosztását a következő csoporttól: {group}",
+ "Untitled event" : "Névtelen esemény",
"{actor} created event {event} in calendar {calendar}" : "{actor} létrehozta a(z) {event} eseményt a következő naptárban: {calendar}",
"You created event {event} in calendar {calendar}" : "Létrehozta a(z) {event} eseményt a következő naptárban: {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} törölte a(z) {event} eseményt a következő naptárból: {calendar}",
"You deleted event {event} from calendar {calendar}" : "Törölte a(z) {event} eseményt a következő naptárból: {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} frissítette a(z) {event} eseményt a következő naptárban: {calendar}",
"You updated event {event} in calendar {calendar}" : "Frissítette a(z) {event} eseményt a következő naptárban: {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} áthelyezte a(z) {event} eseményt a(z) {sourceCalendar} naptárból a(z) {targetCalendar} naptárba",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Áthelyezte a(z) {event} eseményt a(z) {sourceCalendar} naptárból a(z) {targetCalendar} naptárba",
"{actor} restored event {event} of calendar {calendar}" : "{actor} helyreállította a(z) {calendar} naptár következő eseményét: {event}",
"You restored event {event} of calendar {calendar}" : "Helyreállította a(z) {calendar} naptár következő eseményét: {event}",
"Busy" : "Foglalt",
- "{actor} created todo {todo} in list {calendar}" : "{actor} létrehozta a(z) {todo} teendőt a következő listában: {calendar}",
- "You created todo {todo} in list {calendar}" : "Létrehozta a(z) {todo} teendőt a következő listában: {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} törölte a(z) {todo} teendőt a következő listából: {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Törölte a(z) {todo} teendőt a következő listából: {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} frissítette a(z) {todo} teendőt a következő listában: {calendar}",
- "You updated todo {todo} in list {calendar}" : "Frissítette a(z) {todo} teendőt a következő listában: {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} elintézte a(z) {todo} teendőt a következő listában: {calendar}",
- "You solved todo {todo} in list {calendar}" : "Elintézte a(z) {todo} teendőt a következő listában: {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} újranyitotta a(z) {todo} teendőt a következő listában: {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Újranyitotta a(z) {todo} teendőt a következő listában: {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} létrehozta a(z) {todo} teendőt a következő listában: {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Létrehozta a(z) {todo} teendőt a következő listában: {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} törölte a(z) {todo} teendőt a következő listából: {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Törölte a(z) {todo} teendőt a következő listából: {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} frissítette a(z) {todo} teendőt a következő listában: {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Frissítette a(z) {todo} teendőt a következő listában: {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} elintézte a(z) {todo} teendőt a következő listában: {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Elintézte a(z) {todo} teendőt a következő listában: {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} újranyitotta a(z) {todo} teendőt a következő listában: {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Újranyitotta a(z) {todo} teendőt a következő listában: {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} áthelyezte a(z) {todo} teendőt a(z) {sourceCalendar} listából a(z) {targetCalendar} listába",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Áthelyezte a(z) {todo} teendőt a(z) {sourceCalendar} listából a(z) {targetCalendar} listába",
"Calendar, contacts and tasks" : "Naptár, címjegyzék és feladatok",
"A <strong>calendar</strong> was modified" : "Egy <strong>naptár</strong> megváltozott",
"A calendar <strong>event</strong> was modified" : "Egy <strong>naptáresemény</strong> megváltozott",
- "A calendar <strong>todo</strong> was modified" : "Egy <strong>naptárteendő</strong> megváltozott",
+ "A calendar <strong>to-do</strong> was modified" : "Egy <strong>naptárteendő</strong> megváltozott",
"Contact birthdays" : "Névjegyek születésnapjai",
"Death of %s" : "%s halála",
+ "Untitled calendar" : "Névtelen naptár",
"Calendar:" : "Naptár:",
"Date:" : "Dátum:",
"Where:" : "Hol:",
"Description:" : "Leírás:",
- "Untitled event" : "Névtelen esemény",
"_%n year_::_%n years_" : ["%n év","%n év"],
"_%n month_::_%n months_" : ["%n hónap","%n hónap"],
"_%n day_::_%n days_" : ["%n nap","%n nap"],
@@ -65,22 +70,89 @@
"Description: %s" : "Leírás: %s",
"Where: %s" : "Hely: %s",
"%1$s via %2$s" : "%1$s – %2$s",
+ "Could not generate when statement" : "Nem sikerült létrehozni amikor az állítást",
+ "Every Day for the entire day" : "Minden Nap a teljes napra",
+ "Every Day for the entire day until %1$s" : "Minden Nap a teljes napon eddig %1$s",
+ "Every Day between %1$s - %2$s" : "Minden Nap %1$s - %2$sközött",
+ "Every Day between %1$s - %2$s until %3$s" : "Minden Nap %1$s - %2$s között %3$s-ig",
+ "Every %1$d Days for the entire day" : "Minden %1$d Nap az teljes napra",
+ "Every %1$d Days for the entire day until %2$s" : "Minden%1$d Nap a teljes napra %2$s-ig",
+ "Every %1$d Days between %2$s - %3$s" : "Minden%1$d Nap %2$s - %3$s között",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Minden%1$d Napon %2$s - %3$s között %4$s-ig",
+ "Could not generate event recurrence statement" : "Nem sikerült megújuló eseményt létrehozni",
+ "Every Week on %1$s for the entire day" : "Minden hét %1$s napon a teljes napja",
+ "Every Week on %1$s for the entire day until %2$s" : "Minden Héten %1$s a teljes napra %2$s-ig",
+ "Every Week on %1$s between %2$s - %3$s" : "Minden Hét %1$s napján %2$s - %3$sközött",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Minden Hét %1$s napján %2$s - %3$s között %4$s-ig",
+ "Every %1$d Weeks on %2$s for the entire day" : "Minden%1$d héten %2$s napon a teljes napra",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Minden%1$d Héten %2$s napján a teljes napon %3$s-ig",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Minden%1$d hét %2$s napján %3$s - %4$sközött",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Minden%1$d héten %2$s napján %3$s - %4$s között eddig %5$s",
+ "Every Month on the %1$s for the entire day" : "Minden hónap %1$s napján a teljes napon",
+ "Every Month on the %1$s for the entire day until %2$s" : "Minden hónap %1$s napján a teljes napon %2$s-ig",
+ "Every Month on the %1$s between %2$s - %3$s" : "Minden hónap %1$s napján %2$sés %3$s között",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Minden hónap %1$s napján %2$sés %3$s között %4$s-ig",
+ "Every %1$d Months on the %2$s for the entire day" : "Minden%1$d Hónap %2$s napján a teljes napon",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Minden%1$d Hónap %2$s napján a teljes napon %3$s-ig",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Minden%1$d Hónap %2$s napján %3$s és %4$s között",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Minden%1$d Hónap %2$s napján %3$s és %4$s között %5$s-ig",
+ "Every Year in %1$s on the %2$s for the entire day" : "Minden év %1$s hónap %2$s napján a teljes napra",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Minden év %1$s hónapjában %2$s napján a teljes napon %3$s-ig",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Minden év %1$s hónapjában %2$s napján %3$s - %4$sközött.",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Minden év %1$s hónapjában %2$s napján %3$s - %4$s között eddig %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Minden%1$d év %2$s hónapján %3$s napján a teljes napra",
+ "On specific dates for the entire day until %1$s" : "Egy megadott időpontban a teljes napon %1$s-ig ",
+ "On specific dates between %1$s - %2$s until %3$s" : "Egy megadott időpontban %1$s és %2$s %3$s-ig",
+ "Could not generate next recurrence statement" : "Nem sikerült a következő megújuló eseményt létrehozni",
"Cancelled: %1$s" : "Lemondva: %1$s",
- "Invitation canceled" : "Meghívás lemondva",
+ "\"%1$s\" has been canceled" : "A következőt le lett mondva: „%1$s”",
"Re: %1$s" : "Vá: %1$s",
- "Invitation updated" : "Meghívó frissítve",
+ "%1$s has accepted your invitation" : "%1$s elfogadta a meghívását",
+ "%1$s has tentatively accepted your invitation" : "%1$s feltételesen elfogadta a meghívását",
+ "%1$s has declined your invitation" : "%1$s elutasította a meghívását",
+ "%1$s has responded to your invitation" : "%1$s válaszolt a meghívására",
+ "Invitation updated: %1$s" : "Meghívó frissítve: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s frissítette a következő eseményt: %2$s",
"Invitation: %1$s" : "Meghívó: %1$s",
- "Invitation" : "Meghívó",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s meg szeretné hívni a következőre: „%2$s”",
+ "Organizer:" : "Szervező:",
+ "Attendees:" : "Résztvevők:",
"Title:" : "Cím:",
- "Time:" : "Idő:",
+ "When:" : "Mikor:",
"Location:" : "Hely:",
"Link:" : "Hivatkozás:",
- "Organizer:" : "Szervező:",
- "Attendees:" : "Résztvevők:",
+ "Occurring:" : "Előforduló",
"Accept" : "Elfogadás",
"Decline" : "Elutasítás",
"More options …" : "További lehetőségek…",
"More options at %s" : "További lehetőségek itt: %s",
+ "Monday" : "Hétfő",
+ "Tuesday" : "Kedd",
+ "Wednesday" : "Szerda",
+ "Thursday" : "Csütörtök",
+ "Friday" : "Péntek",
+ "Saturday" : "Szombat",
+ "Sunday" : "Vasárnap",
+ "January" : "Január",
+ "February" : "Február",
+ "March" : "Március",
+ "April" : "Április",
+ "May" : "Május",
+ "June" : "Június",
+ "July" : "Július",
+ "August" : "Augusztus",
+ "September" : "Szeptember",
+ "October" : "Október",
+ "November" : "November",
+ "December" : "December",
+ "First" : "Első",
+ "Second" : "Másodperc",
+ "Third" : "Harmadik",
+ "Fourth" : "Negyedik",
+ "Last" : "Utolsó",
+ "Second Last" : "Második Utolsó",
+ "Third Last" : "Harmadik Utolsó",
+ "Fourth Last" : "Negyedik Utolsó",
"Contacts" : "Névjegyek",
"{actor} created address book {addressbook}" : "{actor} létrehozta a következő címjegyzéket: {addressbook}",
"You created address book {addressbook}" : "Létrehozta a következő címjegyzéket: {addressbook}",
@@ -106,7 +178,10 @@
"{actor} updated contact {card} in address book {addressbook}" : "{actor} aktualizálta a(z) {card} névjegyet a következő címjegyzékben: {addressbook}",
"You updated contact {card} in address book {addressbook}" : "Aktualizálta a(z) {card} névjegyet a következő címjegyzékben: {addressbook}",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "Egy <strong>névjegy</strong> vagy <strong>címjegyzék</strong> módosítva lett",
+ "Accounts" : "Fiókok",
+ "System address book which holds all accounts" : "A rendszer címjegyzéke, amely az összes fiókot tartalmazza",
"File is not updatable: %1$s" : "A fájl nem frissíthető: %1$s",
+ "Failed to get storage for file" : "Nem sikerlt a tárhely lekérése a fájl számára",
"Could not write to final file, canceled by hook" : "A végleges fájl nem írható, a hurok megszakította",
"Could not write file contents" : "A fájl tartalma nem írható",
"_%n byte_::_%n bytes_" : ["%n bájt","%n bájt"],
@@ -115,45 +190,68 @@
"Could not rename part file to final file, canceled by hook" : "A részleges fájl nem nevezhető át a végleges fájllá, a hurok megszakította",
"Could not rename part file to final file" : "A részleges fájl nem nevezhető át a végleges fájllá",
"Failed to check file size: %1$s" : "A fájlméret nem ellenőrizhető: %1$s",
- "Could not open file" : "A fájl nem nyitható meg",
"Encryption not ready: %1$s" : "A titkosítás nincs kész: %1$s",
"Failed to open file: %1$s" : "A fájl megnyitása sikertelen: %1$s",
"Failed to unlink: %1$s" : "A hivatkozás eltávolítása sikertelen: %1$s",
- "Invalid chunk name" : "Érvénytelen darabnév",
- "Could not rename part file assembled from chunks" : "Nem lehet átnevezni a darabokból összeállított részleges fájlt",
"Failed to write file contents: %1$s" : "A fájl tartalmának kiírása sikertelen: %1$s",
"File not found: %1$s" : "A fájl nem található: %1$s",
+ "Invalid target path" : "Érvénytelen elérési útvonal",
"System is in maintenance mode." : "A rendszer karbantartási módban van.",
"Upgrade needed" : "Frissítés szükséges",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "A %s kiszolgálót HTTPS használatára kell beállítani, hogy iOS-szel/macOS-szel használhassa CalDAV-ot és a CardDAV-ot.",
"Configures a CalDAV account" : "Beállítja a CalDAV-fiókot",
"Configures a CardDAV account" : "Beállítja a CardDAV-fiókot",
"Events" : "Események",
- "Tasks" : "Feladatok",
"Untitled task" : "Névtelen feladat",
"Completed on %s" : "Befejezve: %s",
"Due on %s by %s" : "Esedékesség: %s, %s által",
"Due on %s" : "Esedékesség: %s",
+ "DAV system address book" : "DAV rendszer címjegyzék",
+ "No outstanding DAV system address book sync." : "Nincs kiemelkedő DAV rendszer címjegyzék szinkronizálás.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "A DAV rendszer címjegyzék szinkronizációja még nem futott le, mert több, mint 1000 felhasználó található vagy hiba történt. Kérem futtassa manuálisan a következő paranccsal: \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "WebDAV végpont",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "A webkiszolgáló nincs megfelelően beállítva a fájlok szinkronizálásához, mert a WebDAV interfész hibásnak tűnik.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "A webkisolgáló megfelelően van konfigurálva a WebDAVon keresztüli fájl szinkronizáció engedélyezéséhez.",
"Migrated calendar (%1$s)" : "Átköltöztetett naptár (%1$s)",
"Calendars including events, details and attendees" : "Naptárak eseményekkel, részletekkel és résztvevőkkel",
"Contacts and groups" : "Névjegyek és csoportok",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV végpont",
- "Availability" : "Elérhetőség",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Ha beállítja a munkaidejét, akkor más felhasználók a megbeszélések létrehozásakor látni fogják, hogy Ön mikor nem érhető el.",
+ "Absence saved" : "Távollét mentve",
+ "Failed to save your absence settings" : "Nem sikerlt a távolléti beállítások mentése",
+ "Absence cleared" : "Távollét törölve",
+ "Failed to clear your absence settings" : "Nem sikerült a távolléti beállítások törlése",
+ "First day" : "Első nap",
+ "Last day (inclusive)" : "Utolsó nap (beleértve)",
+ "Out of office replacement (optional)" : "Távolléti helyettesítő (nem kötelező)",
+ "Name of the replacement" : "Helyettesítő neve",
+ "No results." : "Nincs eredmény.",
+ "Start typing." : "Kezdjen gépelni.",
+ "Short absence status" : "Rövid távolléti állapot",
+ "Long absence Message" : "Hosszú távolléti állapot",
+ "Save" : "Mentés",
+ "Disable absence" : "Távollét tiltása",
+ "Failed to load availability" : "Az elérhetőség betöltése sikertelen",
+ "Saved availability" : "Elérhetőség mentve",
+ "Failed to save availability" : "Az elérhetőség mentése sikertelen",
"Time zone:" : "Időzóna:",
"to" : "–",
"Delete slot" : "Idősáv törlése",
"No working hours set" : "Nincs munkaidő beállítva",
"Add slot" : "Idősáv hozzáadása",
- "Monday" : "Hétfő",
- "Tuesday" : "Kedd",
- "Wednesday" : "Szerda",
- "Thursday" : "Csütörtök",
- "Friday" : "Péntek",
- "Saturday" : "Szombat",
- "Sunday" : "Vasárnap",
- "Save" : "Mentés",
+ "Weekdays" : "Hétköznapok",
+ "Pick a start time for {dayName}" : "Válassz kezdő dátumot a {dayName}-hoz",
+ "Pick a end time for {dayName}" : "Válassz vége dátumot a {dayName}-hoz",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Az elérhetőségi időn túl, a felhasználói állapot automatikus beállítása „Ne zavarjanak” módba az összes értesítés némításához.",
+ "Cancel" : "Mégse",
+ "Import" : "Importálás",
+ "Error while saving settings" : "Hiba a beállítások mentése során",
+ "Reset to default" : "Visszaállítás alapértelmezettre",
+ "Availability" : "Elérhetőség",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Ha beállítja a munkaidejét, akkor más emberek a megbeszélések létrehozásakor látni fogják, hogy Ön mikor nem érhető el.",
+ "Absence" : "Távollét",
+ "Configure your next absence period." : "A követkeő távolléti periódus beállítása.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Telepítse a {calendarappstoreopen}Naptár alkalmazást{linkclose}, vagy {calendardocopen}csatlakoztassa asztali számítógépét és mobilját a szinkronizáláshoz ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Ne felejtse el megfelelően beállítani az {emailopen}e-mail kiszolgálót{linkclose}.",
"Calendar server" : "Naptárkiszolgáló",
"Send invitations to attendees" : "Meghívó küldése a résztvevőknek",
"Automatically generate a birthday calendar" : "Születésnapokat tartalmazó naptár automatikus létrehozása",
@@ -164,15 +262,10 @@
"Send reminder notifications to calendar sharees as well" : "Emlékeztető értesítések küldése azoknak is, akikkel a naptár meg van osztva",
"Reminders are always sent to organizers and attendees." : "Az értesítések mindig a szervezőknek és a résztvevőknek lesznek elküldve.",
"Enable notifications for events via push" : "Leküldéses értesítések engedélyezése az eseményekhez",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Telepítse a {calendarappstoreopen}Naptár alkalmazást{linkclose}, vagy {calendardocopen}csatlakoztassa asztali számítógépét és mobilját a szinkronizáláshoz ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Ne felejtse el megfelelően beállítani az {emailopen}e-mail kiszolgálót{linkclose}.",
"There was an error updating your attendance status." : "Hiba történt a részvételi állapotának frissítése során.",
"Please contact the organizer directly." : "Vegye fel a kapcsolatot közvetlenül a szervezővel.",
"Are you accepting the invitation?" : "Elfogadja az meghívást?",
"Tentative" : "Feltételes",
- "Number of guests" : "Vendégek száma",
- "Comment" : "Megjegyzés",
- "Your attendance was updated successfully." : "A részvétele frissítése sikeres.",
- "Calendar and tasks" : "Naptár és feladatok"
+ "Your attendance was updated successfully." : "A részvétele frissítése sikeres."
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/dav/l10n/id.js b/apps/dav/l10n/id.js
deleted file mode 100644
index 0ab333b580f..00000000000
--- a/apps/dav/l10n/id.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Contact birthdays" : "Ulang tahun kontak",
- "Personal" : "Pribadi",
- "Contacts" : "Kontak",
- "Technical details" : "Rincian teknis",
- "Remote Address: %s" : "Alamat remote: %s",
- "Request ID: %s" : "ID Permintaan: %s"
-},
-"nplurals=1; plural=0;");
diff --git a/apps/dav/l10n/id.json b/apps/dav/l10n/id.json
deleted file mode 100644
index 44280c8125b..00000000000
--- a/apps/dav/l10n/id.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Contact birthdays" : "Ulang tahun kontak",
- "Personal" : "Pribadi",
- "Contacts" : "Kontak",
- "Technical details" : "Rincian teknis",
- "Remote Address: %s" : "Alamat remote: %s",
- "Request ID: %s" : "ID Permintaan: %s"
-},"pluralForm" :"nplurals=1; plural=0;"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/is.js b/apps/dav/l10n/is.js
index 2032bb7d5b3..80eb90d83db 100644
--- a/apps/dav/l10n/is.js
+++ b/apps/dav/l10n/is.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Dagatal",
- "Todos" : "Verkþættir",
+ "Tasks" : "Verkefni",
"Personal" : "Einka",
"{actor} created calendar {calendar}" : "{actor} bjó til dagatalið {calendar}",
"You created calendar {calendar}" : "Þú bjóst til dagatalið {calendar}",
@@ -10,6 +10,8 @@ OC.L10N.register(
"You deleted calendar {calendar}" : "Þú eyddir dagatalinu {calendar}",
"{actor} updated calendar {calendar}" : "{actor} uppfærði dagatalið {calendar}",
"You updated calendar {calendar}" : "Þú uppfærðir dagatalið {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} endurheimti dagatalið {calendar}",
+ "You restored calendar {calendar}" : "Þú endurheimtir dagatalið {calendar}",
"You shared calendar {calendar} as public link" : "Þú deildir dagatalinu {calendar} sem almenningstengli",
"You removed public link for calendar {calendar}" : "Þú fjarlægðir almenningstengilinn fyrir dagatalið {calendar}",
"{actor} shared calendar {calendar} with you" : "{actor} deildi dagatalinu {calendar} með þér",
@@ -23,32 +25,41 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} deildi dagatalinu {calendar} með hópnum {group}",
"You unshared calendar {calendar} from group {group}" : "Þú tókst af deilingu á dagatalinu {calendar} frá hópnum {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} tók af deilingu á dagatalinu {calendar} frá hópnum {group}",
+ "Untitled event" : "Ónefndur atburður",
"{actor} created event {event} in calendar {calendar}" : "{actor} bjó til atburðinn {event} í dagatalinu {calendar}",
"You created event {event} in calendar {calendar}" : "Þú bjóst til atburðinn {event} í dagatalinu {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} eyddi atburðinum {event} úr dagatalinu {calendar}",
"You deleted event {event} from calendar {calendar}" : "Þú eyddir atburðinum {event} úr dagatalinu {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} uppfærði atburðinn {event} í dagatalinu {calendar}",
"You updated event {event} in calendar {calendar}" : "Þú uppfærðir atburðinn {event} í dagatalinu {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} færði atburðinn {event} úr dagatalinu {sourceCalendar} yfir í dagatalið {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Þú færðir atburðinn {event} úr dagatalinu {sourceCalendar} yfir í dagatalið {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} endurheimti atburðinn {event} í dagatalinu {calendar}",
+ "You restored event {event} of calendar {calendar}" : "Þú endurheimtir atburðinn {event} í dagatalinu {calendar}",
"Busy" : "Upptekið",
- "{actor} created todo {todo} in list {calendar}" : "{actor} bjó til verkefnið {todo} á listanum {calendar}",
- "You created todo {todo} in list {calendar}" : "Þú bjóst til verkefnið {todo} á listanum {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} eyddi verkefninu {todo} af listanum {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Þú eyddir verkefninu {todo} af listanum {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} uppfærði verkefnið {todo} á listanum {calendar}",
- "You updated todo {todo} in list {calendar}" : "Þú uppfærðir verkefnið {todo} á listanum {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} leysti verkefnið {todo} á listanum {calendar}",
- "You solved todo {todo} in list {calendar}" : "Þú leystir verkefnið {todo} á listanum {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} opnaði aftur verkefnið {todo} á listanum {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Þú opnaðir aftur verkefnið {todo} á listanum {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} bjó til verkefnið {todo} á listanum {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Þú bjóst til verkefnið {todo} á listanum {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} eyddi verkefninu {todo} af listanum {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Þú eyddir verkefninu {todo} af listanum {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} uppfærði verkefnið {todo} á listanum {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Þú uppfærðir verkefnið {todo} á listanum {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} leysti verkefnið {todo} á listanum {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Þú leystirverkefnið {todo} á listanum {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} opnaði aftur verkefnið {todo} á listanum {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Þú opnaðir aftur verkefnið {todo} á listanum {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} færði verkefnið {todo} á listanum {sourceCalendar} yfir á listann {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Þú færðir verkefnið {todo} á listanum {sourceCalendar} yfir á listann {targetCalendar}",
+ "Calendar, contacts and tasks" : "Dagatal, tengiliðir og verkefni",
"A <strong>calendar</strong> was modified" : "<strong>Dagatali</strong> var breytt",
"A calendar <strong>event</strong> was modified" : "<strong>Atburði</strong> dagatals var breytt",
- "A calendar <strong>todo</strong> was modified" : "<strong>Verkefnalista</strong> dagatals var breytt",
+ "A calendar <strong>to-do</strong> was modified" : "<strong>Verkþætti</strong> dagatals var breytt",
"Contact birthdays" : "Afmælisdagar tengiliðar",
+ "Death of %s" : "Andlát %s",
+ "Untitled calendar" : "Ónefnt dagatal",
"Calendar:" : "Dagatal:",
"Date:" : "Dagsetning:",
"Where:" : "Hvar:",
"Description:" : "Lýsing:",
- "Untitled event" : "Ónefndur atburður",
"_%n year_::_%n years_" : ["%n ár","%n ár"],
"_%n month_::_%n months_" : ["%n mánuður","%n mánuðir"],
"_%n day_::_%n days_" : ["%n dagur","%n dagar"],
@@ -61,43 +72,245 @@ OC.L10N.register(
"Description: %s" : "Lýsing: %s",
"Where: %s" : "Hvar: %s",
"%1$s via %2$s" : "%1$s með %2$s",
- "Invitation canceled" : "Hætt við boð",
- "Invitation updated" : "Boð uppfært",
+ "In the past on %1$s for the entire day" : "Í fortíðinni þann %1$s allan daginn",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Eftir mínútu þann %1$s allan daginn","Eftir %n mínútur þann %1$s allan daginn"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Eftir klukkustund þann %1$s allan daginn","Eftir %n klukkustundir þann %1$s allan daginn"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Eftir dag þann %1$s allan daginn","Eftir %n daga þann %1$s allan daginn"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Eftir viku þann %1$s allan daginn","Eftir %n vikur þann %1$s allan daginn"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Eftir mánuð þann %1$s allan daginn","Eftir %n mánuði þann %1$s allan daginn"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Eftir ár þann %1$s allan daginn","Eftir %n ár þann %1$s allan daginn"],
+ "In the past on %1$s between %2$s - %3$s" : "Í fortíðinni þann %1$s milli %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Eftir mínútu þann %1$s milli %2$s - %3$s","Eftir %n mínútur þann %1$s milli %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Eftir klukkustund þann %1$s milli %2$s - %3$s","Eftir %n klukkustundir þann %1$s milli %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Eftir dag þann %1$s milli %2$s - %3$s","Eftir %n daga þann %1$s milli %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Eftir viku þann %1$s milli %2$s - %3$s","Eftir %n vikur þann %1$s milli %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Eftir mánuð þann %1$s milli %2$s - %3$s","Eftir %n mánuði þann %1$s milli %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Eftir ár þann %1$s milli %2$s - %3$s","Eftir %n ár þann %1$s milli %2$s - %3$s"],
+ "Could not generate when statement" : "Mistókst að útbúa yfirlýsingu fyrir hvenær",
+ "Every Day for the entire day" : "Á hverjum degi allan daginn",
+ "Every Day for the entire day until %1$s" : "Á hverjum degi allan daginn til %1$s",
+ "Every Day between %1$s - %2$s" : "Á hverjum degi milli %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Á hverjum degi milli %1$s - %2$s til %3$s",
+ "Every %1$d Days for the entire day" : "Á %1$d daga fresti allan daginn",
+ "Every %1$d Days for the entire day until %2$s" : "Á %1$d daga fresti allan daginn til %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Á %1$d daga fresti milli %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Á %1$d daga fresti milli %2$s - %3$s til %4$s",
+ "Could not generate event recurrence statement" : "Mistókst að útbúa yfirlýsingu fyrir næstu endurtekningu atburðar",
+ "Every Week on %1$s for the entire day" : "Í hverri viku á %1$s allan daginn",
+ "Every Week on %1$s for the entire day until %2$s" : "Í hverri viku á %1$s allan daginn til %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Í hverri viku á %1$s milli %3$s - %2$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Í hverri viku á %1$s milli %2$s - %3$s til %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Á %1$d vikna fresti á %2$s allan daginn",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Á %1$d vikna fresti á %2$s allan daginn til %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Á %1$d vikna fresti á %2$s milli %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Á %1$d vikna fresti á %2$s milli %3$s - %4$s til %5$s",
+ "Every Month on the %1$s for the entire day" : "Í hverjum mánuði á %1$s allan daginn",
+ "Every Month on the %1$s for the entire day until %2$s" : "Í hverjum mánuði á %1$s allan daginn til %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Í hverjum mánuði á %1$s milli %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Í hverjum mánuði á %1$s milli %2$s - %3$s til %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Á %1$d mánaða fresti á %2$s allan daginn",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Á %1$d mánaða fresti á %2$s allan daginn til %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Á %1$d mánaða fresti á %2$s milli %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Á %1$d mánaða fresti á %2$s milli %3$s - %4$s til %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Á hverju ári í %1$s þann %2$s allan daginn",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Á hverju ári í %1$s þann %2$s allan daginn til %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Á hverju ári í %1$s þann %2$s milli %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Á hverju ári í %1$s þann %2$s milli %3$s - %4$s til %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Á %1$d ára fresti í %2$s þann %3$s allan daginn",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Á %1$d ára fresti í %2$s þann %3$s allan daginn til %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Á %1$d ára fresti í %2$s þann %3$s milli %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Á %1$d ára fresti í %2$s þann %3$s milli %4$s - %5$s til %6$s",
+ "On specific dates for the entire day until %1$s" : "Á tilteknu dögum allan daginn til %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "Á tilteknum dögum frá %1$s - %2$s til %3$s",
+ "In the past on %1$s" : "Í fortíðinni þann %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Eftir mínútu þann %1$s","Eftir %n mínútur þann %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Eftir klukkustund þann %1$s","Eftir %n klukkustundir þann %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Eftir dag þann %1$s","Eftir %n daga þann %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Eftir viku þann %1$s","Eftir %n vikur þann %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Eftir mánuð þann %1$s","Eftir %n mánuði þann %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Eftir ár þann %1$s","Eftir %n ár þann %1$s"],
+ "In the past on %1$s then on %2$s" : "Í fortíðinni þann %1$s og síðan %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Eftir mínútu þann %1$s og síðan þann %2$s","Eftir %n mínútur þann %1$s og síðan þann %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Eftir klukkustund þann %1$s og síðan þann %2$s","Eftir %n klukkustundir þann %1$s og síðan þann %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Eftir dag þann %1$s og síðan þann %2$s","Eftir %n daga þann %1$s og síðan þann %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Eftir viku þann %1$s og síðan þann %2$s","Eftir %n vikur þann %1$s og síðan þann %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Eftir mánuð þann %1$s og síðan þann %2$s","Eftir %n mánuði þann %1$s og síðan þann %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Eftir ár þann %1$s og síðan þann %2$s","Eftir %n ár þann %1$s og síðan þann %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "Í fortíðinni þann %1$s og síðan %2$s og %3$s",
+ "_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_" : ["Eftir mínútu þann %1$s og síðan þann %2$s og %3$s","Eftir %n mínútur þann %1$s og síðan þann %2$s og %3$s"],
+ "_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_" : ["Eftir klukkustund þann %1$s og síðan þann %2$s og %3$s","Eftir %n klukkustundir þann %1$s og síðan þann %2$s og %3$s"],
+ "_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_" : ["Eftir dag þann %1$s og síðan þann %2$s og %3$s","Eftir %n daga þann %1$s og síðan þann %2$s og %3$s"],
+ "_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_" : ["Eftir viku þann %1$s og síðan þann %2$s og %3$s","Eftir %n vikur þann %1$s og síðan þann %2$s og %3$s"],
+ "_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_" : ["Eftir mánuð þann %1$s og síðan þann %2$s og %3$s","Eftir %n mánuði þann %1$s og síðan þann %2$s og %3$s"],
+ "_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_" : ["Eftir ár þann %1$s og síðan þann %2$s og %3$s","Eftir %n ár þann %1$s og síðan þann %2$s og %3$s"],
+ "Could not generate next recurrence statement" : "Mistókst að útbúa yfirlýsingu fyrir næstu endurtekningu",
+ "Cancelled: %1$s" : "Hætt við: %1$s",
+ "\"%1$s\" has been canceled" : "Hætt hefur verið við \"%1$s\"",
+ "Re: %1$s" : "Svar: %1s",
+ "%1$s has accepted your invitation" : "%1$s hefur samþykkt boð þitt",
+ "%1$s has tentatively accepted your invitation" : "%1$s hefur samþykkt boð þitt með fyrirvara",
+ "%1$s has declined your invitation" : "%1$s hafnaði boði þínu",
+ "%1$s has responded to your invitation" : "%1$s hefur svarað boði þínu",
+ "Invitation updated: %1$s" : "Boð uppfært: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s uppfærði atburðinn \"%2$s\"",
+ "Invitation: %1$s" : "Boð: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s vill fá að bjóða þér í \"%2$s\"",
+ "Organizer:" : "Skipuleggjandi:",
+ "Attendees:" : "Þáttakendur:",
+ "Title:" : "Titill:",
+ "When:" : "Hvenær:",
"Location:" : "Staðsetning:",
"Link:" : "Tengill:",
+ "Occurring:" : "Gerist:",
"Accept" : "Samþykkja",
"Decline" : "Hafna",
"More options …" : "Fleiri valkostir ...",
"More options at %s" : "Fleiri valkostir á %s",
+ "Monday" : "mánudag",
+ "Tuesday" : "þriðjudag",
+ "Wednesday" : "miðvikudag",
+ "Thursday" : "fimmtudag",
+ "Friday" : "föstudag",
+ "Saturday" : "laugardag",
+ "Sunday" : "sunnudag",
+ "January" : "Janúar",
+ "February" : "Febrúar",
+ "March" : "Mars",
+ "April" : "Apríl",
+ "May" : "Maí",
+ "June" : "Júní",
+ "July" : "Júlí",
+ "August" : "Ágúst",
+ "September" : "September",
+ "October" : "Október",
+ "November" : "Nóvember",
+ "December" : "Desember",
+ "First" : "fyrsta",
+ "Second" : "annan",
+ "Third" : "þriðja",
+ "Fourth" : "fjórða",
+ "Fifth" : "fimmta",
+ "Last" : "síðasta",
+ "Second Last" : "næstsíðasta",
+ "Third Last" : "þriðja síðasta",
+ "Fourth Last" : "fjórða síðasta",
+ "Fifth Last" : "fimmta síðasta",
"Contacts" : "Tengiliðir",
+ "{actor} created address book {addressbook}" : "{actor} útbjó nafnaskrána {addressbook}",
+ "You created address book {addressbook}" : "Þú bjóst til nafnaskrána {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} eyddi nafnaskránni {addressbook}",
+ "You deleted address book {addressbook}" : "Þú eyddir nafnaskránni {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} uppfærði nafnaskrána {addressbook}",
+ "You updated address book {addressbook}" : "Þú uppfærðir nafnaskrána {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} deildi nafnaskránni {addressbook} með þér",
+ "You shared address book {addressbook} with {user}" : "Þú deildir nafnaskránni {addressbook} með {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} deildi nafnaskránni {addressbook} með {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} hætti að deila nafnaskránni {addressbook} með þér",
+ "You unshared address book {addressbook} from {user}" : "Þú hættir að deila nafnaskránni {addressbook} með {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} hætti að deila nafnaskránni {addressbook} með {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} hætti að deila nafnaskránni {addressbook} með sjálfum sér",
+ "You shared address book {addressbook} with group {group}" : "Þú deildir nafnaskránni {addressbook} með hópnum {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} deildi nafnaskránni {addressbook} með hópnum {group}",
+ "You unshared address book {addressbook} from group {group}" : "Þú hættir að deila nafnaskránni {addressbook} með hópnum {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} hætti að deila nafnaskránni {addressbook} með hópnum {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} útbjó tengiliðinn {card} í nafnaskránni {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Þú útbjóst tengiliðinn {card} í nafnaskránni {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} eyddi tengiliðnum {card} úr nafnaskránni {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Þú eyddir tengiliðnum {card} úr nafnaskránni {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} uppfærði tengiliðinn {card} í nafnaskránni {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Þú uppfærðir tengiliðinn {card} í nafnaskránni {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>Tengilið</strong> eða <strong>nafnaskrá</strong> var breytt",
+ "Accounts" : "Notandaaðgangar",
+ "System address book which holds all accounts" : "Kerfisnafnaskrá sem inniheldur alla notendaaðganga",
+ "File is not updatable: %1$s" : "Skráin er ekki uppfæranleg: %1$s",
+ "Failed to get storage for file" : "Mistókst að fá geymslu fyrir skrá",
+ "Could not write to final file, canceled by hook" : "Gat ekki skrifað í endanlega skrá, hætt við af tengikrækju",
+ "Could not write file contents" : "Mistókst að skrifa innihald skrár",
+ "_%n byte_::_%n bytes_" : ["%n bæti","%n bæti"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Villa kom upp við að afrita skrá á áfangastað (afritað: %1$s, væntanleg skráarstærð: %2$s)",
+ "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." : "Átti von á skráarstærðinni %1$s en las (frá Nextcloud-biðlara) og skrifaði (í Nextcloud-geymslurými) %2$s. Gæti annað hvort verið vandamál í netkerfi varðandi sendingu eða vandamál við skrifun á netþjóni.",
+ "Could not rename part file to final file, canceled by hook" : "Gat ekki endurnefnt hlutaskrá sem endanlega skrá, hætt við af tengikrækju",
+ "Could not rename part file to final file" : "Gat ekki endurnefnt hlutaskrá sem endanlega skrá",
+ "Failed to check file size: %1$s" : "Mistókst að athuga skráarstærð: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "Gat ekki opnað skrá: %1$s, skráin virðist vera til.",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Gat ekki opnað skrá: %1$s, skráin virðist ekki vera til.",
+ "Encryption not ready: %1$s" : "Dulritun ekki tilbúin: %1$s",
+ "Failed to open file: %1$s" : "Mistókst að opna skrá: %1$s",
+ "Failed to unlink: %1$s" : "Mistókst að aftengja: %1$s",
+ "Failed to write file contents: %1$s" : "Mistókst að skrifa innihald skrár: %1$s",
+ "File not found: %1$s" : "Skrá fannst ekki: %1$s",
+ "Invalid target path" : "Ógild markslóð",
+ "System is in maintenance mode." : "Kerfið er í viðhaldsham.",
+ "Upgrade needed" : "Uppfærsla nauðsynleg",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "%s þarf að vera sett upp til að nota HTTPS til að CalDAV og CardDAV virki með iOS/macOS.",
"Configures a CalDAV account" : "Setur upp CalDAV aðgang",
"Configures a CardDAV account" : "Setur upp CardDAV aðgang",
- "Tasks" : "Verkefni",
+ "Events" : "Atburðir",
"Untitled task" : "Ónefnt verkefni",
- "WebDAV" : "WebDAV",
+ "Completed on %s" : "Lokið þann %s",
+ "Due on %s by %s" : "Á að ljúka %s af %s",
+ "Due on %s" : "Á að ljúka þann %s",
+ "DAV system address book" : "DAV-kerfisnafnaskrá",
+ "No outstanding DAV system address book sync." : "Engar eftirstandandi samstillingar við DAV-kerfisnafnaskrá.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Samstilling við DAV-nafnaskrá kerfisins hefur ekki enn verið keyrð, þar sem tilvikið þitt er með fleiri en 1000 notendur eða að villa hefur komið upp. Keyrðu samstillinguna handvirkt með því að kalla á occ dav:sync-system-addressbook\".",
"WebDAV endpoint" : "WebDAV-endapunktur",
- "to" : "til",
- "Monday" : "Mánudagur",
- "Tuesday" : "Þriðjudagur",
- "Wednesday" : "Miðvikudagur",
- "Thursday" : "Fimmtudagur",
- "Friday" : "Föstudagur",
- "Saturday" : "Laugardagur",
- "Sunday" : "Sunnudagur",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Tókst ekki að athuga hvort vefþjónninn sé sett upp á réttan hátt til að leyfa samstillingu skráa í gegnum WebDAV. Athugaðu hvort svo sé.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Vefþjónninn er ekki enn sett upp á réttan hátt til að leyfa skráasamstillingu því WebDAV viðmótið virðist vera skemmt.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Vefþjónninn þinn er settur upp á réttan hátt til að leyfa samstillingu skráa í gegnum WebDAV.",
+ "Migrated calendar (%1$s)" : "Yfirfært dagatal (%1$s)",
+ "Calendars including events, details and attendees" : "Dagatöl sem innihalda atburði, nánari upplýsingar og þátttakendur",
+ "Contacts and groups" : "Tengiliðir og hópar",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "Fjarvera vistuð",
+ "Failed to save your absence settings" : "Mistókst að vista fjarvistastillingar þínar",
+ "Absence cleared" : "Fjarvera hreinsuð",
+ "Failed to clear your absence settings" : "Mistókst að hreinsa fjarvistastillingar þínar",
+ "First day" : "Fyrsta dag",
+ "Last day (inclusive)" : "Síðasta dag (meðtalinn)",
+ "Out of office replacement (optional)" : "Afleysing við fjarveru utan fyrirtækis (valfrjálst)",
+ "Name of the replacement" : "Nafn þess sem tekur við í afleysingu",
+ "No results." : "Engar niðurstöður.",
+ "Start typing." : "Byrjaðu að skrifa.",
+ "Short absence status" : "Staða stuttrar fjarveru",
+ "Long absence Message" : "Skilaboð vegna langrar fjarveru",
"Save" : "Vista",
+ "Disable absence" : "Gera fjarveru óvirka",
+ "Failed to load availability" : "Mistókst að hlaða inn lausum tímum",
+ "Saved availability" : "Vistaði lausa tíma",
+ "Failed to save availability" : "Mistókst að hlaða vista lausa tíma",
+ "Time zone:" : "Tímabelti:",
+ "to" : "til",
+ "Delete slot" : "Eyða tímahólfi",
+ "No working hours set" : "Enginn vinnutími stilltur",
+ "Add slot" : "Bæta við tímahólfi",
+ "Weekdays" : "Virkir dagar",
+ "Pick a start time for {dayName}" : "Veldu upphafstíma fyrir {dayName}",
+ "Pick a end time for {dayName}" : "Veldu lokatíma fyrir {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Setja stöðu notenda sjálfkrafa á \"Ekki ónáða\" utan þess lausa tíma sem viðkomandi er tiltækur, til að þagga niður í öllum tilkynningum.",
+ "Cancel" : "Hætta við",
+ "Import" : "Flytja inn",
+ "Error while saving settings" : "Villa við að vista stillingar",
+ "Reset to default" : "Endurstilla á sjálfgefið",
+ "Availability" : "Aðgengileiki",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Ef þú stillir vinnutímana þína, geta aðrir séð hvenær þú sért fjarverandi þegar þeir bóka fundi með þér.",
+ "Absence" : "Fjarverandi",
+ "Configure your next absence period." : "Stilltu næsta fjarvistatímabilið þitt",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Settu líka upp {calendarappstoreopen}Calendar-forritið{linkclose}, eða {calendardocopen}-tengdu tölvuna þína & farsíma fyrir samstillingu ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Gakktu úr skugga um að {emailopen}póstþjóninn{linkclose} sé rétt uppsettur.",
"Calendar server" : "Dagatalaþjónn",
"Send invitations to attendees" : "Senda boð til þátttakenda",
"Automatically generate a birthday calendar" : "Útbúa fæðingardagatal sjálfvirkt",
"Birthday calendars will be generated by a background job." : "Fæðingardagatöl verða útbúin í bakvinnsluferli.",
"Hence they will not be available immediately after enabling but will show up after some time." : "Þar með verða þau ekki tilbúin strax eftir að þetta er virkjað, heldur birtast þau eftir nokkurn tíma.",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Settu líka upp {calendarappstoreopen}Calendar-forritið{linkclose}, eða {calendardocopen}-tengdu tölvuna þína & farsíma fyrir samstillingu ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Gakktu úr skugga um að {emailopen}póstþjóninn{linkclose} sé rétt uppsettur.",
+ "Send notifications for events" : "Senda tilkynningar fyrir atburði",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Tilkynningar eru sendar í gegnum bakgrunnsverk, þannig að þær verða að gerast nógu oft.",
+ "Send reminder notifications to calendar sharees as well" : "Senda einnig tilkynningar til þeirra sem deila dagatalinu",
+ "Reminders are always sent to organizers and attendees." : "Áminningar eru alltaf sendar til skipuleggjenda og þátttakenda.",
+ "Enable notifications for events via push" : "Senda tilkynningar fyrir atburði sem ýtitilkynningar",
"There was an error updating your attendance status." : "Það kom upp villa við að uppfæra mætingarstöðu þína.",
"Please contact the organizer directly." : "Hafðu samband beint við skipuleggjendurna.",
"Are you accepting the invitation?" : "Ætlar þú að samþykkja boðið?",
"Tentative" : "Bráðabirgða",
- "Comment" : "Athugasemd",
"Your attendance was updated successfully." : "Mætingarstaða þín var uppfærð."
},
"nplurals=2; plural=(n % 10 != 1 || n % 100 == 11);");
diff --git a/apps/dav/l10n/is.json b/apps/dav/l10n/is.json
index d17d83e207e..a89ef260f36 100644
--- a/apps/dav/l10n/is.json
+++ b/apps/dav/l10n/is.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Dagatal",
- "Todos" : "Verkþættir",
+ "Tasks" : "Verkefni",
"Personal" : "Einka",
"{actor} created calendar {calendar}" : "{actor} bjó til dagatalið {calendar}",
"You created calendar {calendar}" : "Þú bjóst til dagatalið {calendar}",
@@ -8,6 +8,8 @@
"You deleted calendar {calendar}" : "Þú eyddir dagatalinu {calendar}",
"{actor} updated calendar {calendar}" : "{actor} uppfærði dagatalið {calendar}",
"You updated calendar {calendar}" : "Þú uppfærðir dagatalið {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} endurheimti dagatalið {calendar}",
+ "You restored calendar {calendar}" : "Þú endurheimtir dagatalið {calendar}",
"You shared calendar {calendar} as public link" : "Þú deildir dagatalinu {calendar} sem almenningstengli",
"You removed public link for calendar {calendar}" : "Þú fjarlægðir almenningstengilinn fyrir dagatalið {calendar}",
"{actor} shared calendar {calendar} with you" : "{actor} deildi dagatalinu {calendar} með þér",
@@ -21,32 +23,41 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} deildi dagatalinu {calendar} með hópnum {group}",
"You unshared calendar {calendar} from group {group}" : "Þú tókst af deilingu á dagatalinu {calendar} frá hópnum {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} tók af deilingu á dagatalinu {calendar} frá hópnum {group}",
+ "Untitled event" : "Ónefndur atburður",
"{actor} created event {event} in calendar {calendar}" : "{actor} bjó til atburðinn {event} í dagatalinu {calendar}",
"You created event {event} in calendar {calendar}" : "Þú bjóst til atburðinn {event} í dagatalinu {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} eyddi atburðinum {event} úr dagatalinu {calendar}",
"You deleted event {event} from calendar {calendar}" : "Þú eyddir atburðinum {event} úr dagatalinu {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} uppfærði atburðinn {event} í dagatalinu {calendar}",
"You updated event {event} in calendar {calendar}" : "Þú uppfærðir atburðinn {event} í dagatalinu {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} færði atburðinn {event} úr dagatalinu {sourceCalendar} yfir í dagatalið {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Þú færðir atburðinn {event} úr dagatalinu {sourceCalendar} yfir í dagatalið {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} endurheimti atburðinn {event} í dagatalinu {calendar}",
+ "You restored event {event} of calendar {calendar}" : "Þú endurheimtir atburðinn {event} í dagatalinu {calendar}",
"Busy" : "Upptekið",
- "{actor} created todo {todo} in list {calendar}" : "{actor} bjó til verkefnið {todo} á listanum {calendar}",
- "You created todo {todo} in list {calendar}" : "Þú bjóst til verkefnið {todo} á listanum {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} eyddi verkefninu {todo} af listanum {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Þú eyddir verkefninu {todo} af listanum {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} uppfærði verkefnið {todo} á listanum {calendar}",
- "You updated todo {todo} in list {calendar}" : "Þú uppfærðir verkefnið {todo} á listanum {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} leysti verkefnið {todo} á listanum {calendar}",
- "You solved todo {todo} in list {calendar}" : "Þú leystir verkefnið {todo} á listanum {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} opnaði aftur verkefnið {todo} á listanum {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Þú opnaðir aftur verkefnið {todo} á listanum {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} bjó til verkefnið {todo} á listanum {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Þú bjóst til verkefnið {todo} á listanum {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} eyddi verkefninu {todo} af listanum {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Þú eyddir verkefninu {todo} af listanum {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} uppfærði verkefnið {todo} á listanum {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Þú uppfærðir verkefnið {todo} á listanum {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} leysti verkefnið {todo} á listanum {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Þú leystirverkefnið {todo} á listanum {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} opnaði aftur verkefnið {todo} á listanum {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Þú opnaðir aftur verkefnið {todo} á listanum {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} færði verkefnið {todo} á listanum {sourceCalendar} yfir á listann {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Þú færðir verkefnið {todo} á listanum {sourceCalendar} yfir á listann {targetCalendar}",
+ "Calendar, contacts and tasks" : "Dagatal, tengiliðir og verkefni",
"A <strong>calendar</strong> was modified" : "<strong>Dagatali</strong> var breytt",
"A calendar <strong>event</strong> was modified" : "<strong>Atburði</strong> dagatals var breytt",
- "A calendar <strong>todo</strong> was modified" : "<strong>Verkefnalista</strong> dagatals var breytt",
+ "A calendar <strong>to-do</strong> was modified" : "<strong>Verkþætti</strong> dagatals var breytt",
"Contact birthdays" : "Afmælisdagar tengiliðar",
+ "Death of %s" : "Andlát %s",
+ "Untitled calendar" : "Ónefnt dagatal",
"Calendar:" : "Dagatal:",
"Date:" : "Dagsetning:",
"Where:" : "Hvar:",
"Description:" : "Lýsing:",
- "Untitled event" : "Ónefndur atburður",
"_%n year_::_%n years_" : ["%n ár","%n ár"],
"_%n month_::_%n months_" : ["%n mánuður","%n mánuðir"],
"_%n day_::_%n days_" : ["%n dagur","%n dagar"],
@@ -59,43 +70,245 @@
"Description: %s" : "Lýsing: %s",
"Where: %s" : "Hvar: %s",
"%1$s via %2$s" : "%1$s með %2$s",
- "Invitation canceled" : "Hætt við boð",
- "Invitation updated" : "Boð uppfært",
+ "In the past on %1$s for the entire day" : "Í fortíðinni þann %1$s allan daginn",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Eftir mínútu þann %1$s allan daginn","Eftir %n mínútur þann %1$s allan daginn"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Eftir klukkustund þann %1$s allan daginn","Eftir %n klukkustundir þann %1$s allan daginn"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Eftir dag þann %1$s allan daginn","Eftir %n daga þann %1$s allan daginn"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Eftir viku þann %1$s allan daginn","Eftir %n vikur þann %1$s allan daginn"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Eftir mánuð þann %1$s allan daginn","Eftir %n mánuði þann %1$s allan daginn"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Eftir ár þann %1$s allan daginn","Eftir %n ár þann %1$s allan daginn"],
+ "In the past on %1$s between %2$s - %3$s" : "Í fortíðinni þann %1$s milli %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Eftir mínútu þann %1$s milli %2$s - %3$s","Eftir %n mínútur þann %1$s milli %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Eftir klukkustund þann %1$s milli %2$s - %3$s","Eftir %n klukkustundir þann %1$s milli %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Eftir dag þann %1$s milli %2$s - %3$s","Eftir %n daga þann %1$s milli %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Eftir viku þann %1$s milli %2$s - %3$s","Eftir %n vikur þann %1$s milli %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Eftir mánuð þann %1$s milli %2$s - %3$s","Eftir %n mánuði þann %1$s milli %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Eftir ár þann %1$s milli %2$s - %3$s","Eftir %n ár þann %1$s milli %2$s - %3$s"],
+ "Could not generate when statement" : "Mistókst að útbúa yfirlýsingu fyrir hvenær",
+ "Every Day for the entire day" : "Á hverjum degi allan daginn",
+ "Every Day for the entire day until %1$s" : "Á hverjum degi allan daginn til %1$s",
+ "Every Day between %1$s - %2$s" : "Á hverjum degi milli %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Á hverjum degi milli %1$s - %2$s til %3$s",
+ "Every %1$d Days for the entire day" : "Á %1$d daga fresti allan daginn",
+ "Every %1$d Days for the entire day until %2$s" : "Á %1$d daga fresti allan daginn til %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Á %1$d daga fresti milli %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Á %1$d daga fresti milli %2$s - %3$s til %4$s",
+ "Could not generate event recurrence statement" : "Mistókst að útbúa yfirlýsingu fyrir næstu endurtekningu atburðar",
+ "Every Week on %1$s for the entire day" : "Í hverri viku á %1$s allan daginn",
+ "Every Week on %1$s for the entire day until %2$s" : "Í hverri viku á %1$s allan daginn til %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Í hverri viku á %1$s milli %3$s - %2$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Í hverri viku á %1$s milli %2$s - %3$s til %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Á %1$d vikna fresti á %2$s allan daginn",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Á %1$d vikna fresti á %2$s allan daginn til %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Á %1$d vikna fresti á %2$s milli %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Á %1$d vikna fresti á %2$s milli %3$s - %4$s til %5$s",
+ "Every Month on the %1$s for the entire day" : "Í hverjum mánuði á %1$s allan daginn",
+ "Every Month on the %1$s for the entire day until %2$s" : "Í hverjum mánuði á %1$s allan daginn til %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Í hverjum mánuði á %1$s milli %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Í hverjum mánuði á %1$s milli %2$s - %3$s til %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Á %1$d mánaða fresti á %2$s allan daginn",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Á %1$d mánaða fresti á %2$s allan daginn til %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Á %1$d mánaða fresti á %2$s milli %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Á %1$d mánaða fresti á %2$s milli %3$s - %4$s til %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Á hverju ári í %1$s þann %2$s allan daginn",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Á hverju ári í %1$s þann %2$s allan daginn til %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Á hverju ári í %1$s þann %2$s milli %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Á hverju ári í %1$s þann %2$s milli %3$s - %4$s til %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Á %1$d ára fresti í %2$s þann %3$s allan daginn",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Á %1$d ára fresti í %2$s þann %3$s allan daginn til %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Á %1$d ára fresti í %2$s þann %3$s milli %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Á %1$d ára fresti í %2$s þann %3$s milli %4$s - %5$s til %6$s",
+ "On specific dates for the entire day until %1$s" : "Á tilteknu dögum allan daginn til %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "Á tilteknum dögum frá %1$s - %2$s til %3$s",
+ "In the past on %1$s" : "Í fortíðinni þann %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Eftir mínútu þann %1$s","Eftir %n mínútur þann %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Eftir klukkustund þann %1$s","Eftir %n klukkustundir þann %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Eftir dag þann %1$s","Eftir %n daga þann %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Eftir viku þann %1$s","Eftir %n vikur þann %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Eftir mánuð þann %1$s","Eftir %n mánuði þann %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Eftir ár þann %1$s","Eftir %n ár þann %1$s"],
+ "In the past on %1$s then on %2$s" : "Í fortíðinni þann %1$s og síðan %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Eftir mínútu þann %1$s og síðan þann %2$s","Eftir %n mínútur þann %1$s og síðan þann %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Eftir klukkustund þann %1$s og síðan þann %2$s","Eftir %n klukkustundir þann %1$s og síðan þann %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Eftir dag þann %1$s og síðan þann %2$s","Eftir %n daga þann %1$s og síðan þann %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Eftir viku þann %1$s og síðan þann %2$s","Eftir %n vikur þann %1$s og síðan þann %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Eftir mánuð þann %1$s og síðan þann %2$s","Eftir %n mánuði þann %1$s og síðan þann %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Eftir ár þann %1$s og síðan þann %2$s","Eftir %n ár þann %1$s og síðan þann %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "Í fortíðinni þann %1$s og síðan %2$s og %3$s",
+ "_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_" : ["Eftir mínútu þann %1$s og síðan þann %2$s og %3$s","Eftir %n mínútur þann %1$s og síðan þann %2$s og %3$s"],
+ "_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_" : ["Eftir klukkustund þann %1$s og síðan þann %2$s og %3$s","Eftir %n klukkustundir þann %1$s og síðan þann %2$s og %3$s"],
+ "_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_" : ["Eftir dag þann %1$s og síðan þann %2$s og %3$s","Eftir %n daga þann %1$s og síðan þann %2$s og %3$s"],
+ "_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_" : ["Eftir viku þann %1$s og síðan þann %2$s og %3$s","Eftir %n vikur þann %1$s og síðan þann %2$s og %3$s"],
+ "_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_" : ["Eftir mánuð þann %1$s og síðan þann %2$s og %3$s","Eftir %n mánuði þann %1$s og síðan þann %2$s og %3$s"],
+ "_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_" : ["Eftir ár þann %1$s og síðan þann %2$s og %3$s","Eftir %n ár þann %1$s og síðan þann %2$s og %3$s"],
+ "Could not generate next recurrence statement" : "Mistókst að útbúa yfirlýsingu fyrir næstu endurtekningu",
+ "Cancelled: %1$s" : "Hætt við: %1$s",
+ "\"%1$s\" has been canceled" : "Hætt hefur verið við \"%1$s\"",
+ "Re: %1$s" : "Svar: %1s",
+ "%1$s has accepted your invitation" : "%1$s hefur samþykkt boð þitt",
+ "%1$s has tentatively accepted your invitation" : "%1$s hefur samþykkt boð þitt með fyrirvara",
+ "%1$s has declined your invitation" : "%1$s hafnaði boði þínu",
+ "%1$s has responded to your invitation" : "%1$s hefur svarað boði þínu",
+ "Invitation updated: %1$s" : "Boð uppfært: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s uppfærði atburðinn \"%2$s\"",
+ "Invitation: %1$s" : "Boð: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s vill fá að bjóða þér í \"%2$s\"",
+ "Organizer:" : "Skipuleggjandi:",
+ "Attendees:" : "Þáttakendur:",
+ "Title:" : "Titill:",
+ "When:" : "Hvenær:",
"Location:" : "Staðsetning:",
"Link:" : "Tengill:",
+ "Occurring:" : "Gerist:",
"Accept" : "Samþykkja",
"Decline" : "Hafna",
"More options …" : "Fleiri valkostir ...",
"More options at %s" : "Fleiri valkostir á %s",
+ "Monday" : "mánudag",
+ "Tuesday" : "þriðjudag",
+ "Wednesday" : "miðvikudag",
+ "Thursday" : "fimmtudag",
+ "Friday" : "föstudag",
+ "Saturday" : "laugardag",
+ "Sunday" : "sunnudag",
+ "January" : "Janúar",
+ "February" : "Febrúar",
+ "March" : "Mars",
+ "April" : "Apríl",
+ "May" : "Maí",
+ "June" : "Júní",
+ "July" : "Júlí",
+ "August" : "Ágúst",
+ "September" : "September",
+ "October" : "Október",
+ "November" : "Nóvember",
+ "December" : "Desember",
+ "First" : "fyrsta",
+ "Second" : "annan",
+ "Third" : "þriðja",
+ "Fourth" : "fjórða",
+ "Fifth" : "fimmta",
+ "Last" : "síðasta",
+ "Second Last" : "næstsíðasta",
+ "Third Last" : "þriðja síðasta",
+ "Fourth Last" : "fjórða síðasta",
+ "Fifth Last" : "fimmta síðasta",
"Contacts" : "Tengiliðir",
+ "{actor} created address book {addressbook}" : "{actor} útbjó nafnaskrána {addressbook}",
+ "You created address book {addressbook}" : "Þú bjóst til nafnaskrána {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} eyddi nafnaskránni {addressbook}",
+ "You deleted address book {addressbook}" : "Þú eyddir nafnaskránni {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} uppfærði nafnaskrána {addressbook}",
+ "You updated address book {addressbook}" : "Þú uppfærðir nafnaskrána {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} deildi nafnaskránni {addressbook} með þér",
+ "You shared address book {addressbook} with {user}" : "Þú deildir nafnaskránni {addressbook} með {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} deildi nafnaskránni {addressbook} með {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} hætti að deila nafnaskránni {addressbook} með þér",
+ "You unshared address book {addressbook} from {user}" : "Þú hættir að deila nafnaskránni {addressbook} með {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} hætti að deila nafnaskránni {addressbook} með {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} hætti að deila nafnaskránni {addressbook} með sjálfum sér",
+ "You shared address book {addressbook} with group {group}" : "Þú deildir nafnaskránni {addressbook} með hópnum {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} deildi nafnaskránni {addressbook} með hópnum {group}",
+ "You unshared address book {addressbook} from group {group}" : "Þú hættir að deila nafnaskránni {addressbook} með hópnum {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} hætti að deila nafnaskránni {addressbook} með hópnum {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} útbjó tengiliðinn {card} í nafnaskránni {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Þú útbjóst tengiliðinn {card} í nafnaskránni {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} eyddi tengiliðnum {card} úr nafnaskránni {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Þú eyddir tengiliðnum {card} úr nafnaskránni {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} uppfærði tengiliðinn {card} í nafnaskránni {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Þú uppfærðir tengiliðinn {card} í nafnaskránni {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>Tengilið</strong> eða <strong>nafnaskrá</strong> var breytt",
+ "Accounts" : "Notandaaðgangar",
+ "System address book which holds all accounts" : "Kerfisnafnaskrá sem inniheldur alla notendaaðganga",
+ "File is not updatable: %1$s" : "Skráin er ekki uppfæranleg: %1$s",
+ "Failed to get storage for file" : "Mistókst að fá geymslu fyrir skrá",
+ "Could not write to final file, canceled by hook" : "Gat ekki skrifað í endanlega skrá, hætt við af tengikrækju",
+ "Could not write file contents" : "Mistókst að skrifa innihald skrár",
+ "_%n byte_::_%n bytes_" : ["%n bæti","%n bæti"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Villa kom upp við að afrita skrá á áfangastað (afritað: %1$s, væntanleg skráarstærð: %2$s)",
+ "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." : "Átti von á skráarstærðinni %1$s en las (frá Nextcloud-biðlara) og skrifaði (í Nextcloud-geymslurými) %2$s. Gæti annað hvort verið vandamál í netkerfi varðandi sendingu eða vandamál við skrifun á netþjóni.",
+ "Could not rename part file to final file, canceled by hook" : "Gat ekki endurnefnt hlutaskrá sem endanlega skrá, hætt við af tengikrækju",
+ "Could not rename part file to final file" : "Gat ekki endurnefnt hlutaskrá sem endanlega skrá",
+ "Failed to check file size: %1$s" : "Mistókst að athuga skráarstærð: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "Gat ekki opnað skrá: %1$s, skráin virðist vera til.",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Gat ekki opnað skrá: %1$s, skráin virðist ekki vera til.",
+ "Encryption not ready: %1$s" : "Dulritun ekki tilbúin: %1$s",
+ "Failed to open file: %1$s" : "Mistókst að opna skrá: %1$s",
+ "Failed to unlink: %1$s" : "Mistókst að aftengja: %1$s",
+ "Failed to write file contents: %1$s" : "Mistókst að skrifa innihald skrár: %1$s",
+ "File not found: %1$s" : "Skrá fannst ekki: %1$s",
+ "Invalid target path" : "Ógild markslóð",
+ "System is in maintenance mode." : "Kerfið er í viðhaldsham.",
+ "Upgrade needed" : "Uppfærsla nauðsynleg",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "%s þarf að vera sett upp til að nota HTTPS til að CalDAV og CardDAV virki með iOS/macOS.",
"Configures a CalDAV account" : "Setur upp CalDAV aðgang",
"Configures a CardDAV account" : "Setur upp CardDAV aðgang",
- "Tasks" : "Verkefni",
+ "Events" : "Atburðir",
"Untitled task" : "Ónefnt verkefni",
- "WebDAV" : "WebDAV",
+ "Completed on %s" : "Lokið þann %s",
+ "Due on %s by %s" : "Á að ljúka %s af %s",
+ "Due on %s" : "Á að ljúka þann %s",
+ "DAV system address book" : "DAV-kerfisnafnaskrá",
+ "No outstanding DAV system address book sync." : "Engar eftirstandandi samstillingar við DAV-kerfisnafnaskrá.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Samstilling við DAV-nafnaskrá kerfisins hefur ekki enn verið keyrð, þar sem tilvikið þitt er með fleiri en 1000 notendur eða að villa hefur komið upp. Keyrðu samstillinguna handvirkt með því að kalla á occ dav:sync-system-addressbook\".",
"WebDAV endpoint" : "WebDAV-endapunktur",
- "to" : "til",
- "Monday" : "Mánudagur",
- "Tuesday" : "Þriðjudagur",
- "Wednesday" : "Miðvikudagur",
- "Thursday" : "Fimmtudagur",
- "Friday" : "Föstudagur",
- "Saturday" : "Laugardagur",
- "Sunday" : "Sunnudagur",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Tókst ekki að athuga hvort vefþjónninn sé sett upp á réttan hátt til að leyfa samstillingu skráa í gegnum WebDAV. Athugaðu hvort svo sé.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Vefþjónninn er ekki enn sett upp á réttan hátt til að leyfa skráasamstillingu því WebDAV viðmótið virðist vera skemmt.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Vefþjónninn þinn er settur upp á réttan hátt til að leyfa samstillingu skráa í gegnum WebDAV.",
+ "Migrated calendar (%1$s)" : "Yfirfært dagatal (%1$s)",
+ "Calendars including events, details and attendees" : "Dagatöl sem innihalda atburði, nánari upplýsingar og þátttakendur",
+ "Contacts and groups" : "Tengiliðir og hópar",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "Fjarvera vistuð",
+ "Failed to save your absence settings" : "Mistókst að vista fjarvistastillingar þínar",
+ "Absence cleared" : "Fjarvera hreinsuð",
+ "Failed to clear your absence settings" : "Mistókst að hreinsa fjarvistastillingar þínar",
+ "First day" : "Fyrsta dag",
+ "Last day (inclusive)" : "Síðasta dag (meðtalinn)",
+ "Out of office replacement (optional)" : "Afleysing við fjarveru utan fyrirtækis (valfrjálst)",
+ "Name of the replacement" : "Nafn þess sem tekur við í afleysingu",
+ "No results." : "Engar niðurstöður.",
+ "Start typing." : "Byrjaðu að skrifa.",
+ "Short absence status" : "Staða stuttrar fjarveru",
+ "Long absence Message" : "Skilaboð vegna langrar fjarveru",
"Save" : "Vista",
+ "Disable absence" : "Gera fjarveru óvirka",
+ "Failed to load availability" : "Mistókst að hlaða inn lausum tímum",
+ "Saved availability" : "Vistaði lausa tíma",
+ "Failed to save availability" : "Mistókst að hlaða vista lausa tíma",
+ "Time zone:" : "Tímabelti:",
+ "to" : "til",
+ "Delete slot" : "Eyða tímahólfi",
+ "No working hours set" : "Enginn vinnutími stilltur",
+ "Add slot" : "Bæta við tímahólfi",
+ "Weekdays" : "Virkir dagar",
+ "Pick a start time for {dayName}" : "Veldu upphafstíma fyrir {dayName}",
+ "Pick a end time for {dayName}" : "Veldu lokatíma fyrir {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Setja stöðu notenda sjálfkrafa á \"Ekki ónáða\" utan þess lausa tíma sem viðkomandi er tiltækur, til að þagga niður í öllum tilkynningum.",
+ "Cancel" : "Hætta við",
+ "Import" : "Flytja inn",
+ "Error while saving settings" : "Villa við að vista stillingar",
+ "Reset to default" : "Endurstilla á sjálfgefið",
+ "Availability" : "Aðgengileiki",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Ef þú stillir vinnutímana þína, geta aðrir séð hvenær þú sért fjarverandi þegar þeir bóka fundi með þér.",
+ "Absence" : "Fjarverandi",
+ "Configure your next absence period." : "Stilltu næsta fjarvistatímabilið þitt",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Settu líka upp {calendarappstoreopen}Calendar-forritið{linkclose}, eða {calendardocopen}-tengdu tölvuna þína & farsíma fyrir samstillingu ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Gakktu úr skugga um að {emailopen}póstþjóninn{linkclose} sé rétt uppsettur.",
"Calendar server" : "Dagatalaþjónn",
"Send invitations to attendees" : "Senda boð til þátttakenda",
"Automatically generate a birthday calendar" : "Útbúa fæðingardagatal sjálfvirkt",
"Birthday calendars will be generated by a background job." : "Fæðingardagatöl verða útbúin í bakvinnsluferli.",
"Hence they will not be available immediately after enabling but will show up after some time." : "Þar með verða þau ekki tilbúin strax eftir að þetta er virkjað, heldur birtast þau eftir nokkurn tíma.",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Settu líka upp {calendarappstoreopen}Calendar-forritið{linkclose}, eða {calendardocopen}-tengdu tölvuna þína & farsíma fyrir samstillingu ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Gakktu úr skugga um að {emailopen}póstþjóninn{linkclose} sé rétt uppsettur.",
+ "Send notifications for events" : "Senda tilkynningar fyrir atburði",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Tilkynningar eru sendar í gegnum bakgrunnsverk, þannig að þær verða að gerast nógu oft.",
+ "Send reminder notifications to calendar sharees as well" : "Senda einnig tilkynningar til þeirra sem deila dagatalinu",
+ "Reminders are always sent to organizers and attendees." : "Áminningar eru alltaf sendar til skipuleggjenda og þátttakenda.",
+ "Enable notifications for events via push" : "Senda tilkynningar fyrir atburði sem ýtitilkynningar",
"There was an error updating your attendance status." : "Það kom upp villa við að uppfæra mætingarstöðu þína.",
"Please contact the organizer directly." : "Hafðu samband beint við skipuleggjendurna.",
"Are you accepting the invitation?" : "Ætlar þú að samþykkja boðið?",
"Tentative" : "Bráðabirgða",
- "Comment" : "Athugasemd",
"Your attendance was updated successfully." : "Mætingarstaða þín var uppfærð."
},"pluralForm" :"nplurals=2; plural=(n % 10 != 1 || n % 100 == 11);"
} \ No newline at end of file
diff --git a/apps/dav/l10n/it.js b/apps/dav/l10n/it.js
index 21e8ce68a31..f441b42c3d8 100644
--- a/apps/dav/l10n/it.js
+++ b/apps/dav/l10n/it.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Calendario",
- "Todos" : "Cose da fare",
+ "Tasks" : "Attività",
"Personal" : "Personale",
"{actor} created calendar {calendar}" : "{actor} ha creato il calendario {calendar}",
"You created calendar {calendar}" : "Hai creato il calendario {calendar}",
@@ -25,41 +25,46 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} ha condiviso il calendario {calendar} con il gruppo {group}",
"You unshared calendar {calendar} from group {group}" : "Hai rimosso la condivisione del calendario {calendar} con il gruppo {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} ha rimosso la condivisione del calendario {calendar} con il gruppo {group}",
+ "Untitled event" : "Evento senza titolo",
"{actor} created event {event} in calendar {calendar}" : "{actor} ha creato l'evento {event} nel calendario {calendar}",
"You created event {event} in calendar {calendar}" : "Hai creato l'evento {event} nel calendario {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} ha eliminato l'evento {event} dal calendario {calendar}",
"You deleted event {event} from calendar {calendar}" : "Hai ha eliminato l'evento {event} dal calendario {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} ha aggiornato l'evento {event} nel calendario {calendar}",
"You updated event {event} in calendar {calendar}" : "Hai aggiornato l'evento {event} nel calendario {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} ha spostato l'evento {event} dal calendario {sourceCalendar} al calendario {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Hai spostato l'evento {event} dal calendario {sourceCalendar} al calendario {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} ha ripristinato l'evento {event} del calendario {calendar}",
"You restored event {event} of calendar {calendar}" : "Hai ripristinato l'evento {event} del calendario {calendar}",
"Busy" : "Occupato",
- "{actor} created todo {todo} in list {calendar}" : "{actor} ha creato la cosa da fare {todo} nell'elenco {calendar}",
- "You created todo {todo} in list {calendar}" : "Hai creato la cosa da fare {todo} nell'elenco {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} ha eliminato la cosa da fare {todo} dall'elenco {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Hai eliminato la cosa da fare {todo} dall'elenco {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} ha aggiornato la cosa da fare {todo} nell'elenco {calendar}",
- "You updated todo {todo} in list {calendar}" : "Hai aggiornato la cosa da fare {todo} nell'elenco {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} ha risolto la cosa da fare {todo} nell'elenco {calendar}",
- "You solved todo {todo} in list {calendar}" : "Hai risolto la cosa da fare {todo} nell'elenco {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} ha riaperto la cosa da fare {todo} nell'elenco {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Hai riaperto la cosa da fare {todo} nell'elenco {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} ha aggiunto la cosa da fare {todo} nella lista {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Hai aggiunto la cosa da fare {todo} nella lista {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} ha eliminato la cosa da fare {todo} dalla lista {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Hai eliminato la cosa da fare {todo} dalla lista {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} ha aggiornato la cosa da fare {todo} nella lista {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Hai aggiornato la cosa da fare {todo} nella lista {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} ha risolto la cosa da fare {todo} nella lista {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Hai risolto la cosa da fare {todo} nella lista {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} ha riaperto la cosa da fare {todo} nella lista {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Hai riaperto la cosa da fare {todo} nella lista {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} ha spostato la cosa da fare {todo} dalla lista {sourceCalendar} alla lista {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Hai spostato la cosa da fare {todo} dalla lista {sourceCalendar} alla lista {targetCalendar}",
"Calendar, contacts and tasks" : "Calendario, contatti e attività",
"A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> è stato modificato",
"A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> del calendario è stato modificato",
- "A calendar <strong>todo</strong> was modified" : "Una <strong>cosa da fare</strong> del calendario è stata modificata",
+ "A calendar <strong>to-do</strong> was modified" : "Una <strong>cosa da fare</strong> del calendario è stata modificata",
"Contact birthdays" : "Date di nascita dei contatti",
"Death of %s" : "Morte di %s",
+ "Untitled calendar" : "Calendario senza titolo",
"Calendar:" : "Calendario:",
"Date:" : "Data:",
"Where:" : "Dove:",
"Description:" : "Descrizione:",
- "Untitled event" : "Evento senza titolo",
- "_%n year_::_%n years_" : ["%n anno","%n anni"],
- "_%n month_::_%n months_" : ["%n mese","%n mesi"],
- "_%n day_::_%n days_" : ["%n giorno","%n giorni"],
- "_%n hour_::_%n hours_" : ["%n ora","%n ore"],
- "_%n minute_::_%n minutes_" : ["%n minuto","%n minuti"],
+ "_%n year_::_%n years_" : ["%n anno","%n anni","%n anni"],
+ "_%n month_::_%n months_" : ["%n mese","%n mesi","%n mesi"],
+ "_%n day_::_%n days_" : ["%n giorno","%n giorni","%n giorni"],
+ "_%n hour_::_%n hours_" : ["%n ora","%n ore","%n ore"],
+ "_%n minute_::_%n minutes_" : ["%n minuto","%n minuti","%n minuti"],
"%s (in %s)" : "%s (in %s)",
"%s (%s ago)" : "%s (%s fa)",
"Calendar: %s" : "Calendario: %s",
@@ -67,22 +72,57 @@ OC.L10N.register(
"Description: %s" : "Descrizione: %s",
"Where: %s" : "Dove: %s",
"%1$s via %2$s" : "%1$s tramite %2$s",
+ "Every Day for the entire day" : "Ogni giorno per l'intero giorno",
+ "Every Day for the entire day until %1$s" : "Ogni Giorno per l'intero giorno fino a %1$s",
+ "Every Day between %1$s - %2$s" : "Ogni giorno tra %1$s-%2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Ogni giorno tra %1$s -%2$s fino al %3$s",
+ "Every %1$d Days for the entire day" : "Ogni %1$d Giorni per tutto il giorno",
+ "Every %1$d Days for the entire day until %2$s" : "Ogni %1$d Giorni per l'intero giorno fino al %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Ogni %1$d Giorni tra %2$s-%3$s",
+ "Every Week on %1$s for the entire day" : "Ogni settimana di %1$s per l'intero giorno",
+ "Every Week on %1$s for the entire day until %2$s" : "Ogni settimana di %1$s per l'intero giorno fino al %2$s",
"Cancelled: %1$s" : "Annullato: %1$s",
- "Invitation canceled" : "Invito annullato",
+ "\"%1$s\" has been canceled" : "\"%1$s\" è stato annullato",
"Re: %1$s" : "Re: %1$s",
- "Invitation updated" : "Invito aggiornato",
+ "%1$s has accepted your invitation" : "%1$s ha accettato il tuo invito",
+ "%1$s has tentatively accepted your invitation" : "%1$s ha provvisoriamente accettato il tuo invito",
+ "%1$s has declined your invitation" : "%1$s ha rifiutato il tuo invito",
+ "%1$s has responded to your invitation" : "%1$s ha risposto al tuo invito",
+ "Invitation updated: %1$s" : "Invito aggiornato: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s ha aggiornato l'evento \"%2$s\"",
"Invitation: %1$s" : "Invito: %1$s",
- "Invitation" : "Invito",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s vorrebbe invitarti in \"%2$s\"",
+ "Organizer:" : "Organizzatore:",
+ "Attendees:" : "Partecipanti:",
"Title:" : "Titolo:",
- "Time:" : "Ora:",
+ "When:" : "Quando:",
"Location:" : "Posizione:",
"Link:" : "Collegamento:",
- "Organizer:" : "Organizzatore:",
- "Attendees:" : "Partecipanti:",
"Accept" : "Accetta",
"Decline" : "Rifiuta",
"More options …" : "Altre opzioni...",
"More options at %s" : "Altre opzioni alle %s",
+ "Monday" : "Lunedì",
+ "Tuesday" : "Martedì",
+ "Wednesday" : "Mercoledì",
+ "Thursday" : "Giovedì",
+ "Friday" : "Venerdì",
+ "Saturday" : "Sabato",
+ "Sunday" : "Domenica",
+ "January" : "Gennaio",
+ "February" : "Febbraio",
+ "March" : "Marzo",
+ "April" : "Aprile",
+ "May" : "Maggio",
+ "June" : "Giugno",
+ "July" : "Luglio",
+ "August" : "Agosto",
+ "September" : "Settembre",
+ "October" : "Ottobre",
+ "November" : "Novembre",
+ "December" : "Dicembre",
+ "First" : "Primo",
+ "Last" : "Ultimo",
"Contacts" : "Contatti",
"{actor} created address book {addressbook}" : "{actor} ha creato la rubrica {addressbook}",
"You created address book {addressbook}" : "Hai creato la rubrica {addressbook}",
@@ -91,10 +131,10 @@ OC.L10N.register(
"{actor} updated address book {addressbook}" : "{actor} ha aggiornato la rubrica {addressbook}",
"You updated address book {addressbook}" : "Hai aggiornato la rubrica {addressbook}",
"{actor} shared address book {addressbook} with you" : "{actor} ha condiviso la rubrica {addressbook} con te",
- "You shared address book {addressbook} with {user}" : "Hai condiviso la rubrica {addressbook} con {actor}",
+ "You shared address book {addressbook} with {user}" : "Hai condiviso la rubrica {addressbook} con {user}",
"{actor} shared address book {addressbook} with {user}" : "{actor} ha condiviso la rubrica {addressbook} con {user}",
"{actor} unshared address book {addressbook} from you" : "{actor} ha eliminato la condivisione della rubrica {addressbook} con te",
- "You unshared address book {addressbook} from {user}" : "Hai eliminato la condivisione della rubrica {addressbook} con {actor}",
+ "You unshared address book {addressbook} from {user}" : "Hai eliminato la condivisione della rubrica {addressbook} con {user}",
"{actor} unshared address book {addressbook} from {user}" : "{actor} ha eliminato la condivisone della rubrica {addressbook} da {user}",
"{actor} unshared address book {addressbook} from themselves" : "{actor} ha eliminato la condivisone della rubrica {addressbook} da se stesso",
"You shared address book {addressbook} with group {group}" : "Hai condiviso la rubrica {addressbook} con il gruppo {group}",
@@ -103,26 +143,25 @@ OC.L10N.register(
"{actor} unshared address book {addressbook} from group {group}" : "{actor} ha eliminato la condivisone della rubrica {addressbook} con il gruppo {group}",
"{actor} created contact {card} in address book {addressbook}" : "{actor} ha creato il contatto {card} nella rubrica {addressbook}",
"You created contact {card} in address book {addressbook}" : "Hai creato il contatto {card} nella rubrica {addressbook}",
- "{actor} deleted contact {card} from address book {addressbook}" : "{actor} ha cancellato il contatto {card} nella rubrica {addressbook}",
- "You deleted contact {card} from address book {addressbook}" : "Hai cancellato il contatto {card} nella rubrica {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} ha eliminato il contatto {card} nella rubrica {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Hai eliminato il contatto {card} nella rubrica {addressbook}",
"{actor} updated contact {card} in address book {addressbook}" : "{actor} ha aggiornato il contatto {card} nella rubrica {addressbook}",
"You updated contact {card} in address book {addressbook}" : "Hai aggiornato il contatto {card} nella rubrica {addressbook}",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "Un <strong>contatto</strong> o <strong>rubrica</strong> sono stati modificati ",
+ "Accounts" : "Account",
+ "System address book which holds all accounts" : "Rubrica di sistema che contiene tutti gli account",
"File is not updatable: %1$s" : "Il file non è aggiornabile: %1$s",
"Could not write to final file, canceled by hook" : "Impossibile scrivere nel file finale, annullato da hook",
"Could not write file contents" : "Impossibile scrivere il contenuto del file",
- "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "_%n byte_::_%n bytes_" : ["%n byte","%n byte","%n byte"],
"Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Errore durante la copia del file nella destinazione (copiato: %1$s, dimensione prevista del file: %2$s)",
"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." : "Dimensione prevista del file %1$s, letto (dal client Nextcloud) e scritto (nell'archivio Nextcloud) %2$s. Potrebbe trattarsi di un problema di rete sul lato d'invio o di un problema di scrittura nell'archivio sul lato server.",
"Could not rename part file to final file, canceled by hook" : "Impossibile rinominare il file di parte in file finale, annullato da hook",
"Could not rename part file to final file" : "Impossibile rinominare il file di parte in file finale",
"Failed to check file size: %1$s" : "Verifica della dimensione del file non riuscito: %1$s",
- "Could not open file" : "Impossibile aprire il file",
- "Encryption not ready: %1$s" : "Crittografia non pronta: %1$s",
+ "Encryption not ready: %1$s" : "Cifratura non pronta: %1$s",
"Failed to open file: %1$s" : "Apertura del file non riuscito: %1$s",
- "Failed to unlink: %1$s" : "Scollegamento fallito: %1$s",
- "Invalid chunk name" : "Nome non valido per lo spezzone",
- "Could not rename part file assembled from chunks" : "Non è possibile rinominare il file assemblato da più spezzoni",
+ "Failed to unlink: %1$s" : "Scollegamento non riuscito: %1$s",
"Failed to write file contents: %1$s" : "Scrittura del contenuto del file non riuscita: %1$s",
"File not found: %1$s" : "File non trovato: %1$s",
"System is in maintenance mode." : "Il sistema è in modalità di manutenzione.",
@@ -131,30 +170,53 @@ OC.L10N.register(
"Configures a CalDAV account" : "Configura un account CalDAV",
"Configures a CardDAV account" : "Configura un account CardDAV",
"Events" : "Eventi",
- "Tasks" : "Attività",
"Untitled task" : "Attività senza titolo",
"Completed on %s" : "Completata il %s",
"Due on %s by %s" : "Scade il %s per %s",
"Due on %s" : "Scade il %s",
+ "DAV system address book" : "Rubrica di sistema DAV",
+ "No outstanding DAV system address book sync." : "Nessuna sincronizzazione della rubrica del sistema DAV in sospeso.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "La sincronizzazione della rubrica del sistema DAV non è ancora stata eseguita poiché la tua istanza ha più di 1000 utenti o perché si è verificato un errore. Eseguila a mano chiamando \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "Terminatore WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Impossibile verificare se il server è configurato correttamente per consentire la sincronizzazione di file via WebDAV. Controllalo a mano.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Il tuo server web non è configurato correttamente per consentire la sincronizzazione dei file, poiché l'interfaccia WebDAV sembra essere danneggiata.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Il tuo server è configurato correttamente per consentire la sincronizzazione di file via WebDAV.",
"Migrated calendar (%1$s)" : "Calendario migrato (%1$s)",
+ "Calendars including events, details and attendees" : "Calendari inclusi eventi, dettagli e partecipanti",
"Contacts and groups" : "Contatti e gruppi",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "Terminatore WebDAV",
- "Availability" : "Disponibilità",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Se imposti il tuo orario di lavoro, gli altri utenti potranno vedere quando non sei in ufficio per organizzare una riunione.",
+ "Absence saved" : "Assenza salvata",
+ "Failed to save your absence settings" : "Impossibile salvare le impostazioni di assenza",
+ "Absence cleared" : "Assenza cancellata",
+ "Failed to clear your absence settings" : "Impossibile cancellare le impostazioni di assenza",
+ "First day" : "Primo giorno",
+ "Last day (inclusive)" : "Ultimo giorno (inclusivo)",
+ "Short absence status" : "Stato di assenza breve",
+ "Long absence Message" : "Messaggio di assenza lunga",
+ "Save" : "Salva",
+ "Disable absence" : "Disattiva assenza",
+ "Failed to load availability" : "Caricamento disponibilità non riuscito",
+ "Saved availability" : "Disponibilità salvata",
+ "Failed to save availability" : "Salvataggio disponibilità non riuscito",
"Time zone:" : "Fuso orario:",
"to" : "a",
"Delete slot" : "Elimina slot",
"No working hours set" : "Orari lavorativi non impostati",
"Add slot" : "Aggiungi slot",
- "Monday" : "Lunedì",
- "Tuesday" : "Martedì",
- "Wednesday" : "Mercoledì",
- "Thursday" : "Giovedì",
- "Friday" : "Venerdì",
- "Saturday" : "Sabato",
- "Sunday" : "Domenica",
- "Save" : "Salva",
+ "Weekdays" : "Giorni feriali",
+ "Pick a start time for {dayName}" : "Scegli un orario di inizio per {dayName}",
+ "Pick a end time for {dayName}" : "Scegli un orario di fine per {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Imposta automaticamente lo stato dell'utente su \"Non disturbare\" al di fuori della disponibilità per disattivare tutte le notifiche.",
+ "Cancel" : "Annulla",
+ "Import" : "Importa",
+ "Error while saving settings" : "Errore durante il salvataggio delle impostazioni",
+ "Reset to default" : "Ripristina valori predefiniti",
+ "Availability" : "Disponibilità",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Se imposti il tuo orario di lavoro, le altre persone potranno vedere quando non sei in ufficio per organizzare una riunione.",
+ "Absence" : "Assenza",
+ "Configure your next absence period." : "Configura il tuo prossimo periodo di assenza.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installa anche {calendarappstoreopen}l'applicazione Calendario{linkclose}, o {calendardocopen}connetti il tuo desktop e mobile per la sincronizzazione ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Assicurati di configurare correttamente {emailopen}il server di posta{linkclose}.",
"Calendar server" : "Server di calendari",
"Send invitations to attendees" : "Invia gli inviti ai partecipanti",
"Automatically generate a birthday calendar" : "Genera automaticamente un calendario dei compleanni",
@@ -162,16 +224,13 @@ OC.L10N.register(
"Hence they will not be available immediately after enabling but will show up after some time." : "Per cui non saranno disponibili immediatamente dopo l'abilitazione, ma saranno mostrati dopo qualche istante.",
"Send notifications for events" : "Invia notifiche per eventi",
"Notifications are sent via background jobs, so these must occur often enough." : "Le notifiche saranno inviate tramite operazioni in background, per cui tali operazioni devono essere eseguite abbastanza spesso.",
+ "Send reminder notifications to calendar sharees as well" : "Invia notifiche di promemoria anche ai partecipanti al calendario",
+ "Reminders are always sent to organizers and attendees." : "I promemoria vengono sempre inviati agli organizzatori e ai partecipanti.",
"Enable notifications for events via push" : "Abilita notifiche per eventi tramite push",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installa anche {calendarappstoreopen}l'applicazione Calendario{linkclose}, o {calendardocopen}connetti il tuo desktop e mobile per la sincronizzazione ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Assicurati di configurare correttamente {emailopen}il server di posta{linkclose}.",
"There was an error updating your attendance status." : "Si è verificato un errore durante l'aggiornamento dello stato della tua partecipazione.",
"Please contact the organizer directly." : "Contatta direttamente l'amministratore.",
"Are you accepting the invitation?" : "Accetti l'invito?",
"Tentative" : "Provvisorio",
- "Number of guests" : "Numero di ospiti",
- "Comment" : "Commento",
- "Your attendance was updated successfully." : "La tua partecipazione è stata aggiornata correttamente.",
- "Calendar and tasks" : "Calendario e attività"
+ "Your attendance was updated successfully." : "La tua partecipazione è stata aggiornata correttamente."
},
-"nplurals=2; plural=(n != 1);");
+"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;");
diff --git a/apps/dav/l10n/it.json b/apps/dav/l10n/it.json
index 591a087ef0c..035866d3115 100644
--- a/apps/dav/l10n/it.json
+++ b/apps/dav/l10n/it.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Calendario",
- "Todos" : "Cose da fare",
+ "Tasks" : "Attività",
"Personal" : "Personale",
"{actor} created calendar {calendar}" : "{actor} ha creato il calendario {calendar}",
"You created calendar {calendar}" : "Hai creato il calendario {calendar}",
@@ -23,41 +23,46 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} ha condiviso il calendario {calendar} con il gruppo {group}",
"You unshared calendar {calendar} from group {group}" : "Hai rimosso la condivisione del calendario {calendar} con il gruppo {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} ha rimosso la condivisione del calendario {calendar} con il gruppo {group}",
+ "Untitled event" : "Evento senza titolo",
"{actor} created event {event} in calendar {calendar}" : "{actor} ha creato l'evento {event} nel calendario {calendar}",
"You created event {event} in calendar {calendar}" : "Hai creato l'evento {event} nel calendario {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} ha eliminato l'evento {event} dal calendario {calendar}",
"You deleted event {event} from calendar {calendar}" : "Hai ha eliminato l'evento {event} dal calendario {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} ha aggiornato l'evento {event} nel calendario {calendar}",
"You updated event {event} in calendar {calendar}" : "Hai aggiornato l'evento {event} nel calendario {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} ha spostato l'evento {event} dal calendario {sourceCalendar} al calendario {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Hai spostato l'evento {event} dal calendario {sourceCalendar} al calendario {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} ha ripristinato l'evento {event} del calendario {calendar}",
"You restored event {event} of calendar {calendar}" : "Hai ripristinato l'evento {event} del calendario {calendar}",
"Busy" : "Occupato",
- "{actor} created todo {todo} in list {calendar}" : "{actor} ha creato la cosa da fare {todo} nell'elenco {calendar}",
- "You created todo {todo} in list {calendar}" : "Hai creato la cosa da fare {todo} nell'elenco {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} ha eliminato la cosa da fare {todo} dall'elenco {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Hai eliminato la cosa da fare {todo} dall'elenco {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} ha aggiornato la cosa da fare {todo} nell'elenco {calendar}",
- "You updated todo {todo} in list {calendar}" : "Hai aggiornato la cosa da fare {todo} nell'elenco {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} ha risolto la cosa da fare {todo} nell'elenco {calendar}",
- "You solved todo {todo} in list {calendar}" : "Hai risolto la cosa da fare {todo} nell'elenco {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} ha riaperto la cosa da fare {todo} nell'elenco {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Hai riaperto la cosa da fare {todo} nell'elenco {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} ha aggiunto la cosa da fare {todo} nella lista {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Hai aggiunto la cosa da fare {todo} nella lista {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} ha eliminato la cosa da fare {todo} dalla lista {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Hai eliminato la cosa da fare {todo} dalla lista {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} ha aggiornato la cosa da fare {todo} nella lista {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Hai aggiornato la cosa da fare {todo} nella lista {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} ha risolto la cosa da fare {todo} nella lista {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Hai risolto la cosa da fare {todo} nella lista {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} ha riaperto la cosa da fare {todo} nella lista {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Hai riaperto la cosa da fare {todo} nella lista {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} ha spostato la cosa da fare {todo} dalla lista {sourceCalendar} alla lista {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Hai spostato la cosa da fare {todo} dalla lista {sourceCalendar} alla lista {targetCalendar}",
"Calendar, contacts and tasks" : "Calendario, contatti e attività",
"A <strong>calendar</strong> was modified" : "Un <strong>calendario</strong> è stato modificato",
"A calendar <strong>event</strong> was modified" : "Un <strong>evento</strong> del calendario è stato modificato",
- "A calendar <strong>todo</strong> was modified" : "Una <strong>cosa da fare</strong> del calendario è stata modificata",
+ "A calendar <strong>to-do</strong> was modified" : "Una <strong>cosa da fare</strong> del calendario è stata modificata",
"Contact birthdays" : "Date di nascita dei contatti",
"Death of %s" : "Morte di %s",
+ "Untitled calendar" : "Calendario senza titolo",
"Calendar:" : "Calendario:",
"Date:" : "Data:",
"Where:" : "Dove:",
"Description:" : "Descrizione:",
- "Untitled event" : "Evento senza titolo",
- "_%n year_::_%n years_" : ["%n anno","%n anni"],
- "_%n month_::_%n months_" : ["%n mese","%n mesi"],
- "_%n day_::_%n days_" : ["%n giorno","%n giorni"],
- "_%n hour_::_%n hours_" : ["%n ora","%n ore"],
- "_%n minute_::_%n minutes_" : ["%n minuto","%n minuti"],
+ "_%n year_::_%n years_" : ["%n anno","%n anni","%n anni"],
+ "_%n month_::_%n months_" : ["%n mese","%n mesi","%n mesi"],
+ "_%n day_::_%n days_" : ["%n giorno","%n giorni","%n giorni"],
+ "_%n hour_::_%n hours_" : ["%n ora","%n ore","%n ore"],
+ "_%n minute_::_%n minutes_" : ["%n minuto","%n minuti","%n minuti"],
"%s (in %s)" : "%s (in %s)",
"%s (%s ago)" : "%s (%s fa)",
"Calendar: %s" : "Calendario: %s",
@@ -65,22 +70,57 @@
"Description: %s" : "Descrizione: %s",
"Where: %s" : "Dove: %s",
"%1$s via %2$s" : "%1$s tramite %2$s",
+ "Every Day for the entire day" : "Ogni giorno per l'intero giorno",
+ "Every Day for the entire day until %1$s" : "Ogni Giorno per l'intero giorno fino a %1$s",
+ "Every Day between %1$s - %2$s" : "Ogni giorno tra %1$s-%2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Ogni giorno tra %1$s -%2$s fino al %3$s",
+ "Every %1$d Days for the entire day" : "Ogni %1$d Giorni per tutto il giorno",
+ "Every %1$d Days for the entire day until %2$s" : "Ogni %1$d Giorni per l'intero giorno fino al %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Ogni %1$d Giorni tra %2$s-%3$s",
+ "Every Week on %1$s for the entire day" : "Ogni settimana di %1$s per l'intero giorno",
+ "Every Week on %1$s for the entire day until %2$s" : "Ogni settimana di %1$s per l'intero giorno fino al %2$s",
"Cancelled: %1$s" : "Annullato: %1$s",
- "Invitation canceled" : "Invito annullato",
+ "\"%1$s\" has been canceled" : "\"%1$s\" è stato annullato",
"Re: %1$s" : "Re: %1$s",
- "Invitation updated" : "Invito aggiornato",
+ "%1$s has accepted your invitation" : "%1$s ha accettato il tuo invito",
+ "%1$s has tentatively accepted your invitation" : "%1$s ha provvisoriamente accettato il tuo invito",
+ "%1$s has declined your invitation" : "%1$s ha rifiutato il tuo invito",
+ "%1$s has responded to your invitation" : "%1$s ha risposto al tuo invito",
+ "Invitation updated: %1$s" : "Invito aggiornato: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s ha aggiornato l'evento \"%2$s\"",
"Invitation: %1$s" : "Invito: %1$s",
- "Invitation" : "Invito",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s vorrebbe invitarti in \"%2$s\"",
+ "Organizer:" : "Organizzatore:",
+ "Attendees:" : "Partecipanti:",
"Title:" : "Titolo:",
- "Time:" : "Ora:",
+ "When:" : "Quando:",
"Location:" : "Posizione:",
"Link:" : "Collegamento:",
- "Organizer:" : "Organizzatore:",
- "Attendees:" : "Partecipanti:",
"Accept" : "Accetta",
"Decline" : "Rifiuta",
"More options …" : "Altre opzioni...",
"More options at %s" : "Altre opzioni alle %s",
+ "Monday" : "Lunedì",
+ "Tuesday" : "Martedì",
+ "Wednesday" : "Mercoledì",
+ "Thursday" : "Giovedì",
+ "Friday" : "Venerdì",
+ "Saturday" : "Sabato",
+ "Sunday" : "Domenica",
+ "January" : "Gennaio",
+ "February" : "Febbraio",
+ "March" : "Marzo",
+ "April" : "Aprile",
+ "May" : "Maggio",
+ "June" : "Giugno",
+ "July" : "Luglio",
+ "August" : "Agosto",
+ "September" : "Settembre",
+ "October" : "Ottobre",
+ "November" : "Novembre",
+ "December" : "Dicembre",
+ "First" : "Primo",
+ "Last" : "Ultimo",
"Contacts" : "Contatti",
"{actor} created address book {addressbook}" : "{actor} ha creato la rubrica {addressbook}",
"You created address book {addressbook}" : "Hai creato la rubrica {addressbook}",
@@ -89,10 +129,10 @@
"{actor} updated address book {addressbook}" : "{actor} ha aggiornato la rubrica {addressbook}",
"You updated address book {addressbook}" : "Hai aggiornato la rubrica {addressbook}",
"{actor} shared address book {addressbook} with you" : "{actor} ha condiviso la rubrica {addressbook} con te",
- "You shared address book {addressbook} with {user}" : "Hai condiviso la rubrica {addressbook} con {actor}",
+ "You shared address book {addressbook} with {user}" : "Hai condiviso la rubrica {addressbook} con {user}",
"{actor} shared address book {addressbook} with {user}" : "{actor} ha condiviso la rubrica {addressbook} con {user}",
"{actor} unshared address book {addressbook} from you" : "{actor} ha eliminato la condivisione della rubrica {addressbook} con te",
- "You unshared address book {addressbook} from {user}" : "Hai eliminato la condivisione della rubrica {addressbook} con {actor}",
+ "You unshared address book {addressbook} from {user}" : "Hai eliminato la condivisione della rubrica {addressbook} con {user}",
"{actor} unshared address book {addressbook} from {user}" : "{actor} ha eliminato la condivisone della rubrica {addressbook} da {user}",
"{actor} unshared address book {addressbook} from themselves" : "{actor} ha eliminato la condivisone della rubrica {addressbook} da se stesso",
"You shared address book {addressbook} with group {group}" : "Hai condiviso la rubrica {addressbook} con il gruppo {group}",
@@ -101,26 +141,25 @@
"{actor} unshared address book {addressbook} from group {group}" : "{actor} ha eliminato la condivisone della rubrica {addressbook} con il gruppo {group}",
"{actor} created contact {card} in address book {addressbook}" : "{actor} ha creato il contatto {card} nella rubrica {addressbook}",
"You created contact {card} in address book {addressbook}" : "Hai creato il contatto {card} nella rubrica {addressbook}",
- "{actor} deleted contact {card} from address book {addressbook}" : "{actor} ha cancellato il contatto {card} nella rubrica {addressbook}",
- "You deleted contact {card} from address book {addressbook}" : "Hai cancellato il contatto {card} nella rubrica {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} ha eliminato il contatto {card} nella rubrica {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Hai eliminato il contatto {card} nella rubrica {addressbook}",
"{actor} updated contact {card} in address book {addressbook}" : "{actor} ha aggiornato il contatto {card} nella rubrica {addressbook}",
"You updated contact {card} in address book {addressbook}" : "Hai aggiornato il contatto {card} nella rubrica {addressbook}",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "Un <strong>contatto</strong> o <strong>rubrica</strong> sono stati modificati ",
+ "Accounts" : "Account",
+ "System address book which holds all accounts" : "Rubrica di sistema che contiene tutti gli account",
"File is not updatable: %1$s" : "Il file non è aggiornabile: %1$s",
"Could not write to final file, canceled by hook" : "Impossibile scrivere nel file finale, annullato da hook",
"Could not write file contents" : "Impossibile scrivere il contenuto del file",
- "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "_%n byte_::_%n bytes_" : ["%n byte","%n byte","%n byte"],
"Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Errore durante la copia del file nella destinazione (copiato: %1$s, dimensione prevista del file: %2$s)",
"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." : "Dimensione prevista del file %1$s, letto (dal client Nextcloud) e scritto (nell'archivio Nextcloud) %2$s. Potrebbe trattarsi di un problema di rete sul lato d'invio o di un problema di scrittura nell'archivio sul lato server.",
"Could not rename part file to final file, canceled by hook" : "Impossibile rinominare il file di parte in file finale, annullato da hook",
"Could not rename part file to final file" : "Impossibile rinominare il file di parte in file finale",
"Failed to check file size: %1$s" : "Verifica della dimensione del file non riuscito: %1$s",
- "Could not open file" : "Impossibile aprire il file",
- "Encryption not ready: %1$s" : "Crittografia non pronta: %1$s",
+ "Encryption not ready: %1$s" : "Cifratura non pronta: %1$s",
"Failed to open file: %1$s" : "Apertura del file non riuscito: %1$s",
- "Failed to unlink: %1$s" : "Scollegamento fallito: %1$s",
- "Invalid chunk name" : "Nome non valido per lo spezzone",
- "Could not rename part file assembled from chunks" : "Non è possibile rinominare il file assemblato da più spezzoni",
+ "Failed to unlink: %1$s" : "Scollegamento non riuscito: %1$s",
"Failed to write file contents: %1$s" : "Scrittura del contenuto del file non riuscita: %1$s",
"File not found: %1$s" : "File non trovato: %1$s",
"System is in maintenance mode." : "Il sistema è in modalità di manutenzione.",
@@ -129,30 +168,53 @@
"Configures a CalDAV account" : "Configura un account CalDAV",
"Configures a CardDAV account" : "Configura un account CardDAV",
"Events" : "Eventi",
- "Tasks" : "Attività",
"Untitled task" : "Attività senza titolo",
"Completed on %s" : "Completata il %s",
"Due on %s by %s" : "Scade il %s per %s",
"Due on %s" : "Scade il %s",
+ "DAV system address book" : "Rubrica di sistema DAV",
+ "No outstanding DAV system address book sync." : "Nessuna sincronizzazione della rubrica del sistema DAV in sospeso.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "La sincronizzazione della rubrica del sistema DAV non è ancora stata eseguita poiché la tua istanza ha più di 1000 utenti o perché si è verificato un errore. Eseguila a mano chiamando \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "Terminatore WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Impossibile verificare se il server è configurato correttamente per consentire la sincronizzazione di file via WebDAV. Controllalo a mano.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Il tuo server web non è configurato correttamente per consentire la sincronizzazione dei file, poiché l'interfaccia WebDAV sembra essere danneggiata.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Il tuo server è configurato correttamente per consentire la sincronizzazione di file via WebDAV.",
"Migrated calendar (%1$s)" : "Calendario migrato (%1$s)",
+ "Calendars including events, details and attendees" : "Calendari inclusi eventi, dettagli e partecipanti",
"Contacts and groups" : "Contatti e gruppi",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "Terminatore WebDAV",
- "Availability" : "Disponibilità",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Se imposti il tuo orario di lavoro, gli altri utenti potranno vedere quando non sei in ufficio per organizzare una riunione.",
+ "Absence saved" : "Assenza salvata",
+ "Failed to save your absence settings" : "Impossibile salvare le impostazioni di assenza",
+ "Absence cleared" : "Assenza cancellata",
+ "Failed to clear your absence settings" : "Impossibile cancellare le impostazioni di assenza",
+ "First day" : "Primo giorno",
+ "Last day (inclusive)" : "Ultimo giorno (inclusivo)",
+ "Short absence status" : "Stato di assenza breve",
+ "Long absence Message" : "Messaggio di assenza lunga",
+ "Save" : "Salva",
+ "Disable absence" : "Disattiva assenza",
+ "Failed to load availability" : "Caricamento disponibilità non riuscito",
+ "Saved availability" : "Disponibilità salvata",
+ "Failed to save availability" : "Salvataggio disponibilità non riuscito",
"Time zone:" : "Fuso orario:",
"to" : "a",
"Delete slot" : "Elimina slot",
"No working hours set" : "Orari lavorativi non impostati",
"Add slot" : "Aggiungi slot",
- "Monday" : "Lunedì",
- "Tuesday" : "Martedì",
- "Wednesday" : "Mercoledì",
- "Thursday" : "Giovedì",
- "Friday" : "Venerdì",
- "Saturday" : "Sabato",
- "Sunday" : "Domenica",
- "Save" : "Salva",
+ "Weekdays" : "Giorni feriali",
+ "Pick a start time for {dayName}" : "Scegli un orario di inizio per {dayName}",
+ "Pick a end time for {dayName}" : "Scegli un orario di fine per {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Imposta automaticamente lo stato dell'utente su \"Non disturbare\" al di fuori della disponibilità per disattivare tutte le notifiche.",
+ "Cancel" : "Annulla",
+ "Import" : "Importa",
+ "Error while saving settings" : "Errore durante il salvataggio delle impostazioni",
+ "Reset to default" : "Ripristina valori predefiniti",
+ "Availability" : "Disponibilità",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Se imposti il tuo orario di lavoro, le altre persone potranno vedere quando non sei in ufficio per organizzare una riunione.",
+ "Absence" : "Assenza",
+ "Configure your next absence period." : "Configura il tuo prossimo periodo di assenza.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installa anche {calendarappstoreopen}l'applicazione Calendario{linkclose}, o {calendardocopen}connetti il tuo desktop e mobile per la sincronizzazione ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Assicurati di configurare correttamente {emailopen}il server di posta{linkclose}.",
"Calendar server" : "Server di calendari",
"Send invitations to attendees" : "Invia gli inviti ai partecipanti",
"Automatically generate a birthday calendar" : "Genera automaticamente un calendario dei compleanni",
@@ -160,16 +222,13 @@
"Hence they will not be available immediately after enabling but will show up after some time." : "Per cui non saranno disponibili immediatamente dopo l'abilitazione, ma saranno mostrati dopo qualche istante.",
"Send notifications for events" : "Invia notifiche per eventi",
"Notifications are sent via background jobs, so these must occur often enough." : "Le notifiche saranno inviate tramite operazioni in background, per cui tali operazioni devono essere eseguite abbastanza spesso.",
+ "Send reminder notifications to calendar sharees as well" : "Invia notifiche di promemoria anche ai partecipanti al calendario",
+ "Reminders are always sent to organizers and attendees." : "I promemoria vengono sempre inviati agli organizzatori e ai partecipanti.",
"Enable notifications for events via push" : "Abilita notifiche per eventi tramite push",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installa anche {calendarappstoreopen}l'applicazione Calendario{linkclose}, o {calendardocopen}connetti il tuo desktop e mobile per la sincronizzazione ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Assicurati di configurare correttamente {emailopen}il server di posta{linkclose}.",
"There was an error updating your attendance status." : "Si è verificato un errore durante l'aggiornamento dello stato della tua partecipazione.",
"Please contact the organizer directly." : "Contatta direttamente l'amministratore.",
"Are you accepting the invitation?" : "Accetti l'invito?",
"Tentative" : "Provvisorio",
- "Number of guests" : "Numero di ospiti",
- "Comment" : "Commento",
- "Your attendance was updated successfully." : "La tua partecipazione è stata aggiornata correttamente.",
- "Calendar and tasks" : "Calendario e attività"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
+ "Your attendance was updated successfully." : "La tua partecipazione è stata aggiornata correttamente."
+},"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"
} \ No newline at end of file
diff --git a/apps/dav/l10n/ja.js b/apps/dav/l10n/ja.js
index b44b42af4d4..2efce569bb8 100644
--- a/apps/dav/l10n/ja.js
+++ b/apps/dav/l10n/ja.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "カレンダー",
- "Todos" : "ToDo",
+ "Tasks" : "タスク",
"Personal" : "個人",
"{actor} created calendar {calendar}" : "{actor}はカレンダー {calendar} を作成しました",
"You created calendar {calendar}" : "カレンダー {calendar} を作成しました",
@@ -25,41 +25,46 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : " {actor}がカレンダー{calendar} を グループ{group} と共有しました",
"You unshared calendar {calendar} from group {group}" : "グループ{group}からカレンダー{calendar}の共有を解除しました",
"{actor} unshared calendar {calendar} from group {group}" : "{actor}がグループ{group}からカレンダー{calendar}の共有を解除しました",
+ "Untitled event" : "無題のイベント",
"{actor} created event {event} in calendar {calendar}" : "{actor}はカレンダー {calendar} のイベント{event}を作成しました",
"You created event {event} in calendar {calendar}" : "カレンダー {calendar} のイベント{event}を作成しました",
"{actor} deleted event {event} from calendar {calendar}" : "{actor}はカレンダー {calendar} のイベント{event}を削除しました",
"You deleted event {event} from calendar {calendar}" : "カレンダー {calendar} のイベント{event}を削除しました",
"{actor} updated event {event} in calendar {calendar}" : "{actor}はカレンダー {calendar} のイベント{event}を更新しました",
"You updated event {event} in calendar {calendar}" : "カレンダー {calendar} のイベント{event}を更新しました",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} はイベント {event} をカレンダー {sourceCalendar} からカレンダー {targetCalendar} に移動しました。",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "あなたはイベント {event} をカレンダー {sourceCalendar} からカレンダー {targetCalendar} に移動しました。",
"{actor} restored event {event} of calendar {calendar}" : "{actor}はカレンダー {calendar}のイベント {event}を復元しました",
"You restored event {event} of calendar {calendar}" : "カレンダー {calendar} のイベント {event}を復元しました",
"Busy" : "ビジー",
- "{actor} created todo {todo} in list {calendar}" : "{actor}はリスト{calendar}のtodo {todo}を作成しました",
- "You created todo {todo} in list {calendar}" : "リスト {calendar} にtodo {todo} を作成しました",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor}リスト{カレンダー}からtodo {todo}を削除しました",
- "You deleted todo {todo} from list {calendar}" : "あなたはtodo {todo}を{calendar}のリストから削除しました。",
- "{actor} updated todo {todo} in list {calendar}" : "{actor}リスト{calendar}のtodo {todo}を更新しました",
- "You updated todo {todo} in list {calendar}" : "リスト{calendar}のtodo {todo}を更新しました。",
- "{actor} solved todo {todo} in list {calendar}" : "{actor}リスト{calendar}のtodo {todo}を解決しました",
- "You solved todo {todo} in list {calendar}" : "リスト{calendar}のtodo {todo}を解決しました。",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor}リスト{calendar}のToDo {todo}を再開しました",
- "You reopened todo {todo} in list {calendar}" : "リスト{calendar}のtodo {todo}を再開しました",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} がリスト {calendar} に ToDo {todo} を作成しました",
+ "You created to-do {todo} in list {calendar}" : "あなたはリスト {calendar} に ToDo {todo} を作成しました",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} は {calendar} リストから {todo} のToDoを削除しました",
+ "You deleted to-do {todo} from list {calendar}" : "あなたはリスト {calendar} からToDo {todo} を削除しました",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} がリスト {calendar} のToDo {todo} を更新しました",
+ "You updated to-do {todo} in list {calendar}" : "あなたはリスト {calendar} の ToDo {todo} を更新しました",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} が {calendar} リストの {todo} を解決しました",
+ "You solved to-do {todo} in list {calendar}" : "あなたは {calendar} リストの {todo} を解決しました",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} がリスト {calendar} の ToDo {todo} を再開しました",
+ "You reopened to-do {todo} in list {calendar}" : "あなたはリスト {calendar} の ToDo {todo} を再開しました",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} は {sourceCalendar} リストから {targetCalendar} リストに ToDo {todo} を移動しました",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "あなたは {sourceCalendar} リストから {targetCalendar} リストに ToDo {todo} を移動しました",
"Calendar, contacts and tasks" : "カレンダー、連絡帳とタスク",
"A <strong>calendar</strong> was modified" : "<strong>カレンダー</strong>が変更されたとき",
"A calendar <strong>event</strong> was modified" : "カレンダーの<strong>イベント</strong>が変更されたとき",
- "A calendar <strong>todo</strong> was modified" : "カレンダーの<strong>ToDo</strong>が変更されたとき",
+ "A calendar <strong>to-do</strong> was modified" : "カレンダーの<strong>ToDo</strong>が変更されました",
"Contact birthdays" : "誕生日",
"Death of %s" : "%sの命日",
+ "Untitled calendar" : "無題のカレンダー",
"Calendar:" : "カレンダー:",
"Date:" : "日付:",
"Where:" : "場所:",
"Description:" : "説明:",
- "Untitled event" : "無題のイベント",
"_%n year_::_%n years_" : ["%n年"],
"_%n month_::_%n months_" : ["%nヶ月"],
"_%n day_::_%n days_" : ["%n日"],
"_%n hour_::_%n hours_" : ["%n時間"],
- "_%n minute_::_%n minutes_" : ["%n秒"],
+ "_%n minute_::_%n minutes_" : ["%n分"],
"%s (in %s)" : "%s(%s後)",
"%s (%s ago)" : "%s(%s前)",
"Calendar: %s" : "カレンダー:%s",
@@ -67,22 +72,129 @@ OC.L10N.register(
"Description: %s" : "説明:%s",
"Where: %s" : "場所:%s",
"%1$s via %2$s" : "%1$s に %2$s から",
+ "In the past on %1$s for the entire day" : "過去に%1$sで終日",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["終日%1$sの%n分"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["終日%1$sの%n時間"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["終日%1$sの%n日"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["終日%1$sの%n週間"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["終日%1$sの%nヶ月"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["終日%1$sの%n年間"],
+ "In the past on %1$s between %2$s - %3$s" : "過去%1$s、%2$s~%3$sの間",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["%2$s~%3$sの間の%1$sの%n分"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["%2$s~%3$sの間の%1$sの%n時間"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["%2$s~%3$sの間の%1$sの%n日間"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["%2$s~%3$sの間の%1$sの%n週間"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["%2$s~%3$sの間の%1$sの%nヶ月"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["%2$s~%3$sの間の%1$sの%n年間"],
+ "Could not generate when statement" : "when文を生成できませんでした",
+ "Every Day for the entire day" : "毎日、終日",
+ "Every Day for the entire day until %1$s" : "%1$sまでの終日、毎日",
+ "Every Day between %1$s - %2$s" : "%1$sから%2$sの間の毎日",
+ "Every Day between %1$s - %2$s until %3$s" : "%1$sから%2$sの間で%3$sまで毎日",
+ "Every %1$d Days for the entire day" : "%1$d日ごとに終日",
+ "Every %1$d Days for the entire day until %2$s" : "%1$d日ごと、%2$sまで終日",
+ "Every %1$d Days between %2$s - %3$s" : "%2$s~%3$sの間で%1$d日ごと",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "%1$d日ごと %2$s~%3$sの間、%4$sまで",
+ "Could not generate event recurrence statement" : "イベントの再帰ステートメントを生成できませんでした",
+ "Every Week on %1$s for the entire day" : "毎週%1$sで終日",
+ "Every Week on %1$s for the entire day until %2$s" : "毎週%1$sに%2$sまで終日",
+ "Every Week on %1$s between %2$s - %3$s" : "毎週%1$s、%2$s~%3$sの間",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "毎週%1$s、%2$s~%3$sの間に%4$sまで",
+ "Every %1$d Weeks on %2$s for the entire day" : "%1$d週ごと%2$sで終日",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "%1$d週ごと%2$sに%3$sまで終日",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "%1$d週ごとの%2$s %3$s~%4$sの間",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "%1$d週ごと%2$s %5$sまで%3$s~%4$sの間",
+ "Every Month on the %1$s for the entire day" : "毎月%1$sに終日",
+ "Every Month on the %1$s for the entire day until %2$s" : "毎月%1$s、%2$sまでの終日",
+ "Every Month on the %1$s between %2$s - %3$s" : "毎月%2$s~%3$sの間の%1$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "毎月、%2$s~%3$sの間の%1$sに、%4$sまで",
+ "Every %1$d Months on the %2$s for the entire day" : "%1$dカ月ごとの%2$sに終日",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "%1$dカ月ごとの%2$s、%3$sまで終日",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "%1$dカ月ごとの%2$s、%3$s~%4$sの間",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "%1$dカ月ごとの%2$s、%3$s~%4$sの間、%5$sまで",
+ "Every Year in %1$s on the %2$s for the entire day" : "毎年%1$s %2$sに終日",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "毎年%1$s %2$s %3$sまで終日",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "毎年%1$s %2$s %3$s~%4$sの間の終日",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "毎年%1$s %2$s %3$s~ %4$s %5$sまで ",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "%1$d年ごとの%2$s %3$sの終日",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "%1$d年ごとの%2$s %3$s %4$sまでの終日",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "%1$d年ごとの%2$s %3$s %4$s~%5$sの間",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "%1$d年ごとの%2$s %3$s %4$s~%5$sの間、%6$sまで",
+ "On specific dates for the entire day until %1$s" : "%1$sまでの特定の日付の終日",
+ "On specific dates between %1$s - %2$s until %3$s" : "%1$s~%2$sの間、%3$sまでの特定の日付",
+ "In the past on %1$s" : "過去%1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["%1$sの%n分間"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["%1$sの%n時間"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["%1$sの%n日間"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["%1$sの%n週間"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["%1$sの%nヶ月"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["%1$sの%n年間"],
+ "In the past on %1$s then on %2$s" : "過去%1$s、その後%2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["過去%1$sの%n分間、その後%2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["過去%1$sの%n時間、その後%2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["過去%1$sの%n日間、その後%2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["過去%1$sの%n週間、その後%2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["過去%1$sの%nヶ月、その後%2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["過去%1$sの%n年間、その後%2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "過去%1$s、その後%2$sと%3$s",
+ "_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_" : ["%1$sの%n分間、その後%2$sと%3$s"],
+ "_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_" : ["%1$sの%n時間、その後%2$sと%3$s"],
+ "_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_" : ["%1$sの%n日間、その後%2$sと%3$s"],
+ "_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_" : ["%1$sの%n週間、その後%2$sと%3$s"],
+ "_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_" : ["%1$sの%nヶ月、その後%2$sと%3$s"],
+ "_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_" : ["%1$sの%n年間、その後%2$sと%3$s"],
+ "Could not generate next recurrence statement" : "次の再帰ステートメントを生成できませんでした",
"Cancelled: %1$s" : "キャンセル: %1$s",
- "Invitation canceled" : "招待のキャンセル",
+ "\"%1$s\" has been canceled" : "%1$sはキャンセルされました",
"Re: %1$s" : "更新: %1$s",
- "Invitation updated" : "招待の更新",
+ "%1$s has accepted your invitation" : "%1$sが招待を受け付けました",
+ "%1$s has tentatively accepted your invitation" : "%1$sはあなたの招待を暫定承諾しました",
+ "%1$s has declined your invitation" : "%1$sはあなたの招待を受けつけませんでした",
+ "%1$s has responded to your invitation" : "%1$sはあなたの招待に応じました",
+ "Invitation updated: %1$s" : "招待状が更新されました: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$sによりイベント\"%2$s\"が更新されました",
"Invitation: %1$s" : "招待: %1$s",
- "Invitation" : "招待",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$sから \"%2$s\" に招待されました",
+ "Organizer:" : "主催者:",
+ "Attendees:" : "参加者:",
"Title:" : "タイトル:",
- "Time:" : "時刻:",
+ "When:" : "いつ:",
"Location:" : "場所:",
"Link:" : "リンク:",
- "Organizer:" : "主催者:",
- "Attendees:" : "参加者:",
+ "Occurring:" : "発生:",
"Accept" : "承諾",
"Decline" : "拒否",
"More options …" : "他のオプション …",
"More options at %s" : "%s のその他のオプション",
+ "Monday" : "月曜日",
+ "Tuesday" : "火曜日",
+ "Wednesday" : "水曜日",
+ "Thursday" : "木曜日",
+ "Friday" : "金曜日",
+ "Saturday" : "土曜日",
+ "Sunday" : "日曜日",
+ "January" : "1月",
+ "February" : "2月",
+ "March" : "3月",
+ "April" : "4月",
+ "May" : "5月",
+ "June" : "6月",
+ "July" : "7月",
+ "August" : "8月",
+ "September" : "9月",
+ "October" : "10月",
+ "November" : "11月",
+ "December" : "12月",
+ "First" : "第1",
+ "Second" : "第2",
+ "Third" : "第3",
+ "Fourth" : "第4",
+ "Fifth" : "5日目",
+ "Last" : "最後",
+ "Second Last" : "最後から2番目",
+ "Third Last" : "最後から3番目",
+ "Fourth Last" : "最後から4番目",
+ "Fifth Last" : "最後から5日目",
"Contacts" : "連絡先",
"{actor} created address book {addressbook}" : "{actor}がアドレス帳 {addressbook}を作成しました",
"You created address book {addressbook}" : "アドレス帳 {addressbook}を作成しました",
@@ -108,31 +220,103 @@ OC.L10N.register(
"{actor} updated contact {card} in address book {addressbook}" : "{actor}がアドレス帳 {addressbook}の連絡先 {card}を更新しました",
"You updated contact {card} in address book {addressbook}" : "アドレス帳 {addressbook}の連絡先 {card}を更新しました",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "連絡先やアドレス帳が変更されたとき",
+ "Accounts" : "アカウント",
+ "System address book which holds all accounts" : "全アカウントが記録されているシステムアドレス帳",
+ "File is not updatable: %1$s" : "ファイルが更新できません:%1$s",
+ "Failed to get storage for file" : "ファイルのストレージを取得できませんでした",
+ "Could not write to final file, canceled by hook" : "最終ファイルへの書き込みができなかったため、フックによりキャンセルされた",
+ "Could not write file contents" : "ファイルの内容を書き込むことができませんでした",
+ "_%n byte_::_%n bytes_" : ["%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "ファイルをコピー先へコピー中にエラーが発生しました (コピー済: %1$s, 想定ファイルサイズ: %2$s)",
+ "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." : "Nextcloudクライアントからの想定ファイルサイズは、%1$s ですが、Nextcloudストレージへの書き込みファイルサイズは %2$s でした。送信側のネットワークの問題またはサーバー側のストレージへの書き込みに問題がある可能性があります。",
+ "Could not rename part file to final file, canceled by hook" : "最終ファイルの名前の変更が出来なかったため、フックによりキャンセルされました",
+ "Could not rename part file to final file" : "最終ファイルの名前の変更が出来ませんでした",
+ "Failed to check file size: %1$s" : "ファイルサイズの確認に失敗: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "ファイルを開けませんでした: %1$s、ファイルは存在するようです",
+ "Could not open file: %1$s, file doesn't seem to exist" : "ファイルを開けませんでした: %1$s ファイルが存在しないようです。",
+ "Encryption not ready: %1$s" : "暗号化の準備が出来ていません: %1$s",
+ "Failed to open file: %1$s" : "ファイルを開くのに失敗: %1$s",
+ "Failed to unlink: %1$s" : "リンクの解除に失敗: %1$s",
+ "Failed to write file contents: %1$s" : "ファイルの内容の書き込みに失敗: %1$s",
+ "File not found: %1$s" : "ファイルが見つかりません: %1$s",
+ "Invalid target path" : "無効なターゲットパス",
"System is in maintenance mode." : "システムはメンテナンスモードです。",
"Upgrade needed" : "アップグレードが必要です",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "iOS / macOSでCalDAVおよびCardDAVを使用するには、%sにHTTPSを設定する必要があります。",
"Configures a CalDAV account" : "CalDAVアカウントを設定します",
"Configures a CardDAV account" : "CardDAVアカウントを設定します",
"Events" : "イベント",
- "Tasks" : "タスク",
"Untitled task" : "タイトルなしタスク",
"Completed on %s" : "%sに完了",
"Due on %s by %s" : "期限日%s が%sにより設定",
"Due on %s" : "期限日:%s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Nextcloudカレンダーへようこそ!\n\nNextcloudカレンダーの柔軟なプランニングをお試しください!\n\nNextcloudカレンダーなら、こんなことができます:\n- イベントを簡単に作成、編集、管理できます。\n- 複数のカレンダーを作成し、チームメイトや友人、家族と共有できます。\n- 空き時間を確認し、忙しい時間を他の人に表示する。\n- CalDAV経由でアプリやデバイスとシームレスに統合。\n- 定期的なイベントのスケジュール、通知やその他の設定など、エクスペリエンスをカスタマイズできます。",
+ "Example event - open me!" : "イベント例 - 開いてください!",
+ "System Address Book" : "システム連絡先リスト",
+ "The system address book contains contact information for all users in your instance." : "システムのアドレス帳には、インスタンスのすべてのユーザーの連絡先情報が含まれています。",
+ "Enable System Address Book" : "システム連絡先リストを有効にする",
+ "DAV system address book" : "DAVシステムアドレス帳",
+ "No outstanding DAV system address book sync." : "DAVシステムアドレス帳の同期が完了していません。",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAVシステムのアドレス帳同期は、インスタンスに1000人以上のユーザーがいるか、エラーが発生したためまだ実行されていません。手動で実行するには、\"occ dav:sync-system-addressbook\"を呼び出してください。",
+ "WebDAV endpoint" : "WebDAVエンドポイント",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "WebDAVでのファイル同期を許可するようにWebサーバーが正しく設定されているか確認できませんでした。手動で確認してください。",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "WebDAVインターフェースが動作していないようです。Webサーバーは、ファイルの同期を許可するよう適切に設定されていません。",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "WebDAVによるファイル同期を許可するように、Webサーバーが適切に設定されています。",
+ "Migrated calendar (%1$s)" : "カレンダーを移行しました (%1$s)",
+ "Calendars including events, details and attendees" : "カレンダーには、イベント、イベントの詳細及び出席者が含まれます",
"Contacts and groups" : "連絡先とグループ",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAVエンドポイント",
- "Time zone:" : "時間帯:",
+ "Absence saved" : "不在を保存しました",
+ "Failed to save your absence settings" : "不在設定の保存に失敗しました",
+ "Absence cleared" : "不在を削除しました",
+ "Failed to clear your absence settings" : "不在設定の解除に失敗しました",
+ "First day" : "初日",
+ "Last day (inclusive)" : "最終日(含む)",
+ "Out of office replacement (optional)" : "不在時の代理者(オプション)",
+ "Name of the replacement" : "交代要員の氏名",
+ "No results." : "結果はありません。",
+ "Start typing." : "入力を開始する。",
+ "Short absence status" : "短い不在のステータス",
+ "Long absence Message" : "長期不在のメッセージ",
+ "Save" : "保存",
+ "Disable absence" : "不在を無効にする",
+ "Failed to load availability" : "可用性の読み込みに失敗",
+ "Saved availability" : "可用性を保存しました",
+ "Failed to save availability" : "可用性を保存しました",
+ "Time zone:" : "タイムゾーン:",
"to" : "宛先",
"Delete slot" : "スロットを削除",
- "Monday" : "月曜日",
- "Tuesday" : "火曜日",
- "Wednesday" : "水曜日",
- "Thursday" : "木曜日",
- "Friday" : "金曜日",
- "Saturday" : "土曜日",
- "Sunday" : "日曜日",
- "Save" : "保存",
+ "No working hours set" : "勤務時間未設定",
+ "Add slot" : "スロットを追加",
+ "Weekdays" : "平日",
+ "Pick a start time for {dayName}" : "{dayName} 開始時間を指定してください",
+ "Pick a end time for {dayName}" : "{dayName} の終了時間を指定してください",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "利用時間外は自動的にユーザーステータスを\"非通知\" に設定し、すべての通知をミュートします。",
+ "Cancel" : "キャンセル",
+ "Import" : "インポート",
+ "Error while saving settings" : "設定の保存中にエラーが発生",
+ "Contact reset successfully" : "連絡先のリセットが正常に完了しました",
+ "Error while resetting contact" : "連絡先のリセット中にエラーが発生しました",
+ "Contact imported successfully" : "連絡先のインポートが正常に完了しました",
+ "Error while importing contact" : "連絡先のインポート中にエラーが発生しました",
+ "Import contact" : "連絡先のインポート",
+ "Reset to default" : "デフォルトに戻す",
+ "Import contacts" : "連絡先のインポート",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "新しい .vcf ファイルをインポートすると、既存のデフォルト連絡先が削除され、新しいものに置き換えられます。続行しますか?",
+ "Failed to save example event creation setting" : "イベント作成例設定の保存に失敗しました",
+ "Failed to upload the example event" : "イベント例のアップロードに失敗しました",
+ "Custom example event was saved successfully" : "カスタムイベント例は正常に保存されました",
+ "Failed to delete the custom example event" : "カスタムイベント例の削除に失敗しました",
+ "Custom example event was deleted successfully" : "カスタムイベント例は正常に削除されました",
+ "Import calendar event" : "カレンダーイベントのインポート",
+ "Uploading a new event will overwrite the existing one." : "新しいイベントをアップロードすると既存のイベントは上書きされます。",
+ "Upload event" : "イベントをアップロード",
+ "Availability" : "応対可能日時",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "勤務時間を設定すると、他のユーザが会議を予約する際に、あなたがいつ不在であるかがわかります。",
+ "Absence" : "不在",
+ "Configure your next absence period." : "次の不在期間を設定します。",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "{calendarappstoreopen}カレンダーアプリ{linkclose}、または{calendardocopen}を同期させるためにデスクトップとモバイルを接続する ↗{linkclose}もインストールしてください。",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "{emailopen}メールサーバー{linkclose}を正しく設定してください。",
"Calendar server" : "カレンダーサーバー",
"Send invitations to attendees" : "参加者に招待状を送信する",
"Automatically generate a birthday calendar" : "自動的に誕生日カレンダーを生成する",
@@ -140,15 +324,15 @@ OC.L10N.register(
"Hence they will not be available immediately after enabling but will show up after some time." : "したがって、有効にした直後は利用できませんが、しばらくしてから表示されます。",
"Send notifications for events" : "イベントの通知を送信",
"Notifications are sent via background jobs, so these must occur often enough." : "通知はバックグラウンドジョブを介して送信されるため、十分な頻度で発生します。",
+ "Send reminder notifications to calendar sharees as well" : "カレンダー共有にもリマインダー通知を送信する",
+ "Reminders are always sent to organizers and attendees." : "リマインダーを常に作成者と出席者に送信します。",
"Enable notifications for events via push" : "イベントのプッシュ通知を有効にする",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "{calendarappstoreopen}カレンダーアプリ{linkclose}、または{calendardocopen}を同期させるためにデスクトップとモバイルを接続する ↗{linkclose}もインストールしてください。",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "{emailopen}メールサーバー{linkclose}を正しく設定してください。",
+ "Example content" : "コンテンツ例",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "コンテンツ例はNextcloudの機能を紹介するものです。デフォルトのコンテンツはNextcloudに同梱されており、カスタムコンテンツで置き換えることができます。",
"There was an error updating your attendance status." : "出席状況の更新中にエラーが発生しました。",
"Please contact the organizer directly." : "主催者に直接お問い合わせください。",
"Are you accepting the invitation?" : "招待を受け入れていますか?",
"Tentative" : "暫定的",
- "Comment" : "コメント",
- "Your attendance was updated successfully." : "出席は正常に更新されました。",
- "Calendar and tasks" : "カレンダーとタスク"
+ "Your attendance was updated successfully." : "出席は正常に更新されました。"
},
"nplurals=1; plural=0;");
diff --git a/apps/dav/l10n/ja.json b/apps/dav/l10n/ja.json
index 336df7f9d23..737b71f4180 100644
--- a/apps/dav/l10n/ja.json
+++ b/apps/dav/l10n/ja.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "カレンダー",
- "Todos" : "ToDo",
+ "Tasks" : "タスク",
"Personal" : "個人",
"{actor} created calendar {calendar}" : "{actor}はカレンダー {calendar} を作成しました",
"You created calendar {calendar}" : "カレンダー {calendar} を作成しました",
@@ -23,41 +23,46 @@
"{actor} shared calendar {calendar} with group {group}" : " {actor}がカレンダー{calendar} を グループ{group} と共有しました",
"You unshared calendar {calendar} from group {group}" : "グループ{group}からカレンダー{calendar}の共有を解除しました",
"{actor} unshared calendar {calendar} from group {group}" : "{actor}がグループ{group}からカレンダー{calendar}の共有を解除しました",
+ "Untitled event" : "無題のイベント",
"{actor} created event {event} in calendar {calendar}" : "{actor}はカレンダー {calendar} のイベント{event}を作成しました",
"You created event {event} in calendar {calendar}" : "カレンダー {calendar} のイベント{event}を作成しました",
"{actor} deleted event {event} from calendar {calendar}" : "{actor}はカレンダー {calendar} のイベント{event}を削除しました",
"You deleted event {event} from calendar {calendar}" : "カレンダー {calendar} のイベント{event}を削除しました",
"{actor} updated event {event} in calendar {calendar}" : "{actor}はカレンダー {calendar} のイベント{event}を更新しました",
"You updated event {event} in calendar {calendar}" : "カレンダー {calendar} のイベント{event}を更新しました",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} はイベント {event} をカレンダー {sourceCalendar} からカレンダー {targetCalendar} に移動しました。",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "あなたはイベント {event} をカレンダー {sourceCalendar} からカレンダー {targetCalendar} に移動しました。",
"{actor} restored event {event} of calendar {calendar}" : "{actor}はカレンダー {calendar}のイベント {event}を復元しました",
"You restored event {event} of calendar {calendar}" : "カレンダー {calendar} のイベント {event}を復元しました",
"Busy" : "ビジー",
- "{actor} created todo {todo} in list {calendar}" : "{actor}はリスト{calendar}のtodo {todo}を作成しました",
- "You created todo {todo} in list {calendar}" : "リスト {calendar} にtodo {todo} を作成しました",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor}リスト{カレンダー}からtodo {todo}を削除しました",
- "You deleted todo {todo} from list {calendar}" : "あなたはtodo {todo}を{calendar}のリストから削除しました。",
- "{actor} updated todo {todo} in list {calendar}" : "{actor}リスト{calendar}のtodo {todo}を更新しました",
- "You updated todo {todo} in list {calendar}" : "リスト{calendar}のtodo {todo}を更新しました。",
- "{actor} solved todo {todo} in list {calendar}" : "{actor}リスト{calendar}のtodo {todo}を解決しました",
- "You solved todo {todo} in list {calendar}" : "リスト{calendar}のtodo {todo}を解決しました。",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor}リスト{calendar}のToDo {todo}を再開しました",
- "You reopened todo {todo} in list {calendar}" : "リスト{calendar}のtodo {todo}を再開しました",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} がリスト {calendar} に ToDo {todo} を作成しました",
+ "You created to-do {todo} in list {calendar}" : "あなたはリスト {calendar} に ToDo {todo} を作成しました",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} は {calendar} リストから {todo} のToDoを削除しました",
+ "You deleted to-do {todo} from list {calendar}" : "あなたはリスト {calendar} からToDo {todo} を削除しました",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} がリスト {calendar} のToDo {todo} を更新しました",
+ "You updated to-do {todo} in list {calendar}" : "あなたはリスト {calendar} の ToDo {todo} を更新しました",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} が {calendar} リストの {todo} を解決しました",
+ "You solved to-do {todo} in list {calendar}" : "あなたは {calendar} リストの {todo} を解決しました",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} がリスト {calendar} の ToDo {todo} を再開しました",
+ "You reopened to-do {todo} in list {calendar}" : "あなたはリスト {calendar} の ToDo {todo} を再開しました",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} は {sourceCalendar} リストから {targetCalendar} リストに ToDo {todo} を移動しました",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "あなたは {sourceCalendar} リストから {targetCalendar} リストに ToDo {todo} を移動しました",
"Calendar, contacts and tasks" : "カレンダー、連絡帳とタスク",
"A <strong>calendar</strong> was modified" : "<strong>カレンダー</strong>が変更されたとき",
"A calendar <strong>event</strong> was modified" : "カレンダーの<strong>イベント</strong>が変更されたとき",
- "A calendar <strong>todo</strong> was modified" : "カレンダーの<strong>ToDo</strong>が変更されたとき",
+ "A calendar <strong>to-do</strong> was modified" : "カレンダーの<strong>ToDo</strong>が変更されました",
"Contact birthdays" : "誕生日",
"Death of %s" : "%sの命日",
+ "Untitled calendar" : "無題のカレンダー",
"Calendar:" : "カレンダー:",
"Date:" : "日付:",
"Where:" : "場所:",
"Description:" : "説明:",
- "Untitled event" : "無題のイベント",
"_%n year_::_%n years_" : ["%n年"],
"_%n month_::_%n months_" : ["%nヶ月"],
"_%n day_::_%n days_" : ["%n日"],
"_%n hour_::_%n hours_" : ["%n時間"],
- "_%n minute_::_%n minutes_" : ["%n秒"],
+ "_%n minute_::_%n minutes_" : ["%n分"],
"%s (in %s)" : "%s(%s後)",
"%s (%s ago)" : "%s(%s前)",
"Calendar: %s" : "カレンダー:%s",
@@ -65,22 +70,129 @@
"Description: %s" : "説明:%s",
"Where: %s" : "場所:%s",
"%1$s via %2$s" : "%1$s に %2$s から",
+ "In the past on %1$s for the entire day" : "過去に%1$sで終日",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["終日%1$sの%n分"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["終日%1$sの%n時間"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["終日%1$sの%n日"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["終日%1$sの%n週間"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["終日%1$sの%nヶ月"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["終日%1$sの%n年間"],
+ "In the past on %1$s between %2$s - %3$s" : "過去%1$s、%2$s~%3$sの間",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["%2$s~%3$sの間の%1$sの%n分"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["%2$s~%3$sの間の%1$sの%n時間"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["%2$s~%3$sの間の%1$sの%n日間"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["%2$s~%3$sの間の%1$sの%n週間"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["%2$s~%3$sの間の%1$sの%nヶ月"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["%2$s~%3$sの間の%1$sの%n年間"],
+ "Could not generate when statement" : "when文を生成できませんでした",
+ "Every Day for the entire day" : "毎日、終日",
+ "Every Day for the entire day until %1$s" : "%1$sまでの終日、毎日",
+ "Every Day between %1$s - %2$s" : "%1$sから%2$sの間の毎日",
+ "Every Day between %1$s - %2$s until %3$s" : "%1$sから%2$sの間で%3$sまで毎日",
+ "Every %1$d Days for the entire day" : "%1$d日ごとに終日",
+ "Every %1$d Days for the entire day until %2$s" : "%1$d日ごと、%2$sまで終日",
+ "Every %1$d Days between %2$s - %3$s" : "%2$s~%3$sの間で%1$d日ごと",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "%1$d日ごと %2$s~%3$sの間、%4$sまで",
+ "Could not generate event recurrence statement" : "イベントの再帰ステートメントを生成できませんでした",
+ "Every Week on %1$s for the entire day" : "毎週%1$sで終日",
+ "Every Week on %1$s for the entire day until %2$s" : "毎週%1$sに%2$sまで終日",
+ "Every Week on %1$s between %2$s - %3$s" : "毎週%1$s、%2$s~%3$sの間",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "毎週%1$s、%2$s~%3$sの間に%4$sまで",
+ "Every %1$d Weeks on %2$s for the entire day" : "%1$d週ごと%2$sで終日",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "%1$d週ごと%2$sに%3$sまで終日",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "%1$d週ごとの%2$s %3$s~%4$sの間",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "%1$d週ごと%2$s %5$sまで%3$s~%4$sの間",
+ "Every Month on the %1$s for the entire day" : "毎月%1$sに終日",
+ "Every Month on the %1$s for the entire day until %2$s" : "毎月%1$s、%2$sまでの終日",
+ "Every Month on the %1$s between %2$s - %3$s" : "毎月%2$s~%3$sの間の%1$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "毎月、%2$s~%3$sの間の%1$sに、%4$sまで",
+ "Every %1$d Months on the %2$s for the entire day" : "%1$dカ月ごとの%2$sに終日",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "%1$dカ月ごとの%2$s、%3$sまで終日",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "%1$dカ月ごとの%2$s、%3$s~%4$sの間",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "%1$dカ月ごとの%2$s、%3$s~%4$sの間、%5$sまで",
+ "Every Year in %1$s on the %2$s for the entire day" : "毎年%1$s %2$sに終日",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "毎年%1$s %2$s %3$sまで終日",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "毎年%1$s %2$s %3$s~%4$sの間の終日",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "毎年%1$s %2$s %3$s~ %4$s %5$sまで ",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "%1$d年ごとの%2$s %3$sの終日",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "%1$d年ごとの%2$s %3$s %4$sまでの終日",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "%1$d年ごとの%2$s %3$s %4$s~%5$sの間",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "%1$d年ごとの%2$s %3$s %4$s~%5$sの間、%6$sまで",
+ "On specific dates for the entire day until %1$s" : "%1$sまでの特定の日付の終日",
+ "On specific dates between %1$s - %2$s until %3$s" : "%1$s~%2$sの間、%3$sまでの特定の日付",
+ "In the past on %1$s" : "過去%1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["%1$sの%n分間"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["%1$sの%n時間"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["%1$sの%n日間"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["%1$sの%n週間"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["%1$sの%nヶ月"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["%1$sの%n年間"],
+ "In the past on %1$s then on %2$s" : "過去%1$s、その後%2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["過去%1$sの%n分間、その後%2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["過去%1$sの%n時間、その後%2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["過去%1$sの%n日間、その後%2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["過去%1$sの%n週間、その後%2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["過去%1$sの%nヶ月、その後%2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["過去%1$sの%n年間、その後%2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "過去%1$s、その後%2$sと%3$s",
+ "_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_" : ["%1$sの%n分間、その後%2$sと%3$s"],
+ "_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_" : ["%1$sの%n時間、その後%2$sと%3$s"],
+ "_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_" : ["%1$sの%n日間、その後%2$sと%3$s"],
+ "_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_" : ["%1$sの%n週間、その後%2$sと%3$s"],
+ "_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_" : ["%1$sの%nヶ月、その後%2$sと%3$s"],
+ "_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_" : ["%1$sの%n年間、その後%2$sと%3$s"],
+ "Could not generate next recurrence statement" : "次の再帰ステートメントを生成できませんでした",
"Cancelled: %1$s" : "キャンセル: %1$s",
- "Invitation canceled" : "招待のキャンセル",
+ "\"%1$s\" has been canceled" : "%1$sはキャンセルされました",
"Re: %1$s" : "更新: %1$s",
- "Invitation updated" : "招待の更新",
+ "%1$s has accepted your invitation" : "%1$sが招待を受け付けました",
+ "%1$s has tentatively accepted your invitation" : "%1$sはあなたの招待を暫定承諾しました",
+ "%1$s has declined your invitation" : "%1$sはあなたの招待を受けつけませんでした",
+ "%1$s has responded to your invitation" : "%1$sはあなたの招待に応じました",
+ "Invitation updated: %1$s" : "招待状が更新されました: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$sによりイベント\"%2$s\"が更新されました",
"Invitation: %1$s" : "招待: %1$s",
- "Invitation" : "招待",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$sから \"%2$s\" に招待されました",
+ "Organizer:" : "主催者:",
+ "Attendees:" : "参加者:",
"Title:" : "タイトル:",
- "Time:" : "時刻:",
+ "When:" : "いつ:",
"Location:" : "場所:",
"Link:" : "リンク:",
- "Organizer:" : "主催者:",
- "Attendees:" : "参加者:",
+ "Occurring:" : "発生:",
"Accept" : "承諾",
"Decline" : "拒否",
"More options …" : "他のオプション …",
"More options at %s" : "%s のその他のオプション",
+ "Monday" : "月曜日",
+ "Tuesday" : "火曜日",
+ "Wednesday" : "水曜日",
+ "Thursday" : "木曜日",
+ "Friday" : "金曜日",
+ "Saturday" : "土曜日",
+ "Sunday" : "日曜日",
+ "January" : "1月",
+ "February" : "2月",
+ "March" : "3月",
+ "April" : "4月",
+ "May" : "5月",
+ "June" : "6月",
+ "July" : "7月",
+ "August" : "8月",
+ "September" : "9月",
+ "October" : "10月",
+ "November" : "11月",
+ "December" : "12月",
+ "First" : "第1",
+ "Second" : "第2",
+ "Third" : "第3",
+ "Fourth" : "第4",
+ "Fifth" : "5日目",
+ "Last" : "最後",
+ "Second Last" : "最後から2番目",
+ "Third Last" : "最後から3番目",
+ "Fourth Last" : "最後から4番目",
+ "Fifth Last" : "最後から5日目",
"Contacts" : "連絡先",
"{actor} created address book {addressbook}" : "{actor}がアドレス帳 {addressbook}を作成しました",
"You created address book {addressbook}" : "アドレス帳 {addressbook}を作成しました",
@@ -106,31 +218,103 @@
"{actor} updated contact {card} in address book {addressbook}" : "{actor}がアドレス帳 {addressbook}の連絡先 {card}を更新しました",
"You updated contact {card} in address book {addressbook}" : "アドレス帳 {addressbook}の連絡先 {card}を更新しました",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "連絡先やアドレス帳が変更されたとき",
+ "Accounts" : "アカウント",
+ "System address book which holds all accounts" : "全アカウントが記録されているシステムアドレス帳",
+ "File is not updatable: %1$s" : "ファイルが更新できません:%1$s",
+ "Failed to get storage for file" : "ファイルのストレージを取得できませんでした",
+ "Could not write to final file, canceled by hook" : "最終ファイルへの書き込みができなかったため、フックによりキャンセルされた",
+ "Could not write file contents" : "ファイルの内容を書き込むことができませんでした",
+ "_%n byte_::_%n bytes_" : ["%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "ファイルをコピー先へコピー中にエラーが発生しました (コピー済: %1$s, 想定ファイルサイズ: %2$s)",
+ "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." : "Nextcloudクライアントからの想定ファイルサイズは、%1$s ですが、Nextcloudストレージへの書き込みファイルサイズは %2$s でした。送信側のネットワークの問題またはサーバー側のストレージへの書き込みに問題がある可能性があります。",
+ "Could not rename part file to final file, canceled by hook" : "最終ファイルの名前の変更が出来なかったため、フックによりキャンセルされました",
+ "Could not rename part file to final file" : "最終ファイルの名前の変更が出来ませんでした",
+ "Failed to check file size: %1$s" : "ファイルサイズの確認に失敗: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "ファイルを開けませんでした: %1$s、ファイルは存在するようです",
+ "Could not open file: %1$s, file doesn't seem to exist" : "ファイルを開けませんでした: %1$s ファイルが存在しないようです。",
+ "Encryption not ready: %1$s" : "暗号化の準備が出来ていません: %1$s",
+ "Failed to open file: %1$s" : "ファイルを開くのに失敗: %1$s",
+ "Failed to unlink: %1$s" : "リンクの解除に失敗: %1$s",
+ "Failed to write file contents: %1$s" : "ファイルの内容の書き込みに失敗: %1$s",
+ "File not found: %1$s" : "ファイルが見つかりません: %1$s",
+ "Invalid target path" : "無効なターゲットパス",
"System is in maintenance mode." : "システムはメンテナンスモードです。",
"Upgrade needed" : "アップグレードが必要です",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "iOS / macOSでCalDAVおよびCardDAVを使用するには、%sにHTTPSを設定する必要があります。",
"Configures a CalDAV account" : "CalDAVアカウントを設定します",
"Configures a CardDAV account" : "CardDAVアカウントを設定します",
"Events" : "イベント",
- "Tasks" : "タスク",
"Untitled task" : "タイトルなしタスク",
"Completed on %s" : "%sに完了",
"Due on %s by %s" : "期限日%s が%sにより設定",
"Due on %s" : "期限日:%s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Nextcloudカレンダーへようこそ!\n\nNextcloudカレンダーの柔軟なプランニングをお試しください!\n\nNextcloudカレンダーなら、こんなことができます:\n- イベントを簡単に作成、編集、管理できます。\n- 複数のカレンダーを作成し、チームメイトや友人、家族と共有できます。\n- 空き時間を確認し、忙しい時間を他の人に表示する。\n- CalDAV経由でアプリやデバイスとシームレスに統合。\n- 定期的なイベントのスケジュール、通知やその他の設定など、エクスペリエンスをカスタマイズできます。",
+ "Example event - open me!" : "イベント例 - 開いてください!",
+ "System Address Book" : "システム連絡先リスト",
+ "The system address book contains contact information for all users in your instance." : "システムのアドレス帳には、インスタンスのすべてのユーザーの連絡先情報が含まれています。",
+ "Enable System Address Book" : "システム連絡先リストを有効にする",
+ "DAV system address book" : "DAVシステムアドレス帳",
+ "No outstanding DAV system address book sync." : "DAVシステムアドレス帳の同期が完了していません。",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAVシステムのアドレス帳同期は、インスタンスに1000人以上のユーザーがいるか、エラーが発生したためまだ実行されていません。手動で実行するには、\"occ dav:sync-system-addressbook\"を呼び出してください。",
+ "WebDAV endpoint" : "WebDAVエンドポイント",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "WebDAVでのファイル同期を許可するようにWebサーバーが正しく設定されているか確認できませんでした。手動で確認してください。",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "WebDAVインターフェースが動作していないようです。Webサーバーは、ファイルの同期を許可するよう適切に設定されていません。",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "WebDAVによるファイル同期を許可するように、Webサーバーが適切に設定されています。",
+ "Migrated calendar (%1$s)" : "カレンダーを移行しました (%1$s)",
+ "Calendars including events, details and attendees" : "カレンダーには、イベント、イベントの詳細及び出席者が含まれます",
"Contacts and groups" : "連絡先とグループ",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAVエンドポイント",
- "Time zone:" : "時間帯:",
+ "Absence saved" : "不在を保存しました",
+ "Failed to save your absence settings" : "不在設定の保存に失敗しました",
+ "Absence cleared" : "不在を削除しました",
+ "Failed to clear your absence settings" : "不在設定の解除に失敗しました",
+ "First day" : "初日",
+ "Last day (inclusive)" : "最終日(含む)",
+ "Out of office replacement (optional)" : "不在時の代理者(オプション)",
+ "Name of the replacement" : "交代要員の氏名",
+ "No results." : "結果はありません。",
+ "Start typing." : "入力を開始する。",
+ "Short absence status" : "短い不在のステータス",
+ "Long absence Message" : "長期不在のメッセージ",
+ "Save" : "保存",
+ "Disable absence" : "不在を無効にする",
+ "Failed to load availability" : "可用性の読み込みに失敗",
+ "Saved availability" : "可用性を保存しました",
+ "Failed to save availability" : "可用性を保存しました",
+ "Time zone:" : "タイムゾーン:",
"to" : "宛先",
"Delete slot" : "スロットを削除",
- "Monday" : "月曜日",
- "Tuesday" : "火曜日",
- "Wednesday" : "水曜日",
- "Thursday" : "木曜日",
- "Friday" : "金曜日",
- "Saturday" : "土曜日",
- "Sunday" : "日曜日",
- "Save" : "保存",
+ "No working hours set" : "勤務時間未設定",
+ "Add slot" : "スロットを追加",
+ "Weekdays" : "平日",
+ "Pick a start time for {dayName}" : "{dayName} 開始時間を指定してください",
+ "Pick a end time for {dayName}" : "{dayName} の終了時間を指定してください",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "利用時間外は自動的にユーザーステータスを\"非通知\" に設定し、すべての通知をミュートします。",
+ "Cancel" : "キャンセル",
+ "Import" : "インポート",
+ "Error while saving settings" : "設定の保存中にエラーが発生",
+ "Contact reset successfully" : "連絡先のリセットが正常に完了しました",
+ "Error while resetting contact" : "連絡先のリセット中にエラーが発生しました",
+ "Contact imported successfully" : "連絡先のインポートが正常に完了しました",
+ "Error while importing contact" : "連絡先のインポート中にエラーが発生しました",
+ "Import contact" : "連絡先のインポート",
+ "Reset to default" : "デフォルトに戻す",
+ "Import contacts" : "連絡先のインポート",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "新しい .vcf ファイルをインポートすると、既存のデフォルト連絡先が削除され、新しいものに置き換えられます。続行しますか?",
+ "Failed to save example event creation setting" : "イベント作成例設定の保存に失敗しました",
+ "Failed to upload the example event" : "イベント例のアップロードに失敗しました",
+ "Custom example event was saved successfully" : "カスタムイベント例は正常に保存されました",
+ "Failed to delete the custom example event" : "カスタムイベント例の削除に失敗しました",
+ "Custom example event was deleted successfully" : "カスタムイベント例は正常に削除されました",
+ "Import calendar event" : "カレンダーイベントのインポート",
+ "Uploading a new event will overwrite the existing one." : "新しいイベントをアップロードすると既存のイベントは上書きされます。",
+ "Upload event" : "イベントをアップロード",
+ "Availability" : "応対可能日時",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "勤務時間を設定すると、他のユーザが会議を予約する際に、あなたがいつ不在であるかがわかります。",
+ "Absence" : "不在",
+ "Configure your next absence period." : "次の不在期間を設定します。",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "{calendarappstoreopen}カレンダーアプリ{linkclose}、または{calendardocopen}を同期させるためにデスクトップとモバイルを接続する ↗{linkclose}もインストールしてください。",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "{emailopen}メールサーバー{linkclose}を正しく設定してください。",
"Calendar server" : "カレンダーサーバー",
"Send invitations to attendees" : "参加者に招待状を送信する",
"Automatically generate a birthday calendar" : "自動的に誕生日カレンダーを生成する",
@@ -138,15 +322,15 @@
"Hence they will not be available immediately after enabling but will show up after some time." : "したがって、有効にした直後は利用できませんが、しばらくしてから表示されます。",
"Send notifications for events" : "イベントの通知を送信",
"Notifications are sent via background jobs, so these must occur often enough." : "通知はバックグラウンドジョブを介して送信されるため、十分な頻度で発生します。",
+ "Send reminder notifications to calendar sharees as well" : "カレンダー共有にもリマインダー通知を送信する",
+ "Reminders are always sent to organizers and attendees." : "リマインダーを常に作成者と出席者に送信します。",
"Enable notifications for events via push" : "イベントのプッシュ通知を有効にする",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "{calendarappstoreopen}カレンダーアプリ{linkclose}、または{calendardocopen}を同期させるためにデスクトップとモバイルを接続する ↗{linkclose}もインストールしてください。",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "{emailopen}メールサーバー{linkclose}を正しく設定してください。",
+ "Example content" : "コンテンツ例",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "コンテンツ例はNextcloudの機能を紹介するものです。デフォルトのコンテンツはNextcloudに同梱されており、カスタムコンテンツで置き換えることができます。",
"There was an error updating your attendance status." : "出席状況の更新中にエラーが発生しました。",
"Please contact the organizer directly." : "主催者に直接お問い合わせください。",
"Are you accepting the invitation?" : "招待を受け入れていますか?",
"Tentative" : "暫定的",
- "Comment" : "コメント",
- "Your attendance was updated successfully." : "出席は正常に更新されました。",
- "Calendar and tasks" : "カレンダーとタスク"
+ "Your attendance was updated successfully." : "出席は正常に更新されました。"
},"pluralForm" :"nplurals=1; plural=0;"
} \ No newline at end of file
diff --git a/apps/dav/l10n/ka.js b/apps/dav/l10n/ka.js
new file mode 100644
index 00000000000..682a8e1d254
--- /dev/null
+++ b/apps/dav/l10n/ka.js
@@ -0,0 +1,220 @@
+OC.L10N.register(
+ "dav",
+ {
+ "Calendar" : "Calendar",
+ "Tasks" : "Tasks",
+ "Personal" : "Personal",
+ "{actor} created calendar {calendar}" : "{actor} created calendar {calendar}",
+ "You created calendar {calendar}" : "You created calendar {calendar}",
+ "{actor} deleted calendar {calendar}" : "{actor} deleted calendar {calendar}",
+ "You deleted calendar {calendar}" : "You deleted calendar {calendar}",
+ "{actor} updated calendar {calendar}" : "{actor} updated calendar {calendar}",
+ "You updated calendar {calendar}" : "You updated calendar {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} restored calendar {calendar}",
+ "You restored calendar {calendar}" : "You restored calendar {calendar}",
+ "You shared calendar {calendar} as public link" : "You shared calendar {calendar} as public link",
+ "You removed public link for calendar {calendar}" : "You removed public link for calendar {calendar}",
+ "{actor} shared calendar {calendar} with you" : "{actor} shared calendar {calendar} with you",
+ "You shared calendar {calendar} with {user}" : "You shared calendar {calendar} with {user}",
+ "{actor} shared calendar {calendar} with {user}" : "{actor} shared calendar {calendar} with {user}",
+ "{actor} unshared calendar {calendar} from you" : "{actor} unshared calendar {calendar} from you",
+ "You unshared calendar {calendar} from {user}" : "You unshared calendar {calendar} from {user}",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor} unshared calendar {calendar} from {user}",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} unshared calendar {calendar} from themselves",
+ "You shared calendar {calendar} with group {group}" : "You shared calendar {calendar} with group {group}",
+ "{actor} shared calendar {calendar} with group {group}" : "{actor} shared calendar {calendar} with group {group}",
+ "You unshared calendar {calendar} from group {group}" : "You unshared calendar {calendar} from group {group}",
+ "{actor} unshared calendar {calendar} from group {group}" : "{actor} unshared calendar {calendar} from group {group}",
+ "Untitled event" : "Untitled event",
+ "{actor} created event {event} in calendar {calendar}" : "{actor} created event {event} in calendar {calendar}",
+ "You created event {event} in calendar {calendar}" : "You created event {event} in calendar {calendar}",
+ "{actor} deleted event {event} from calendar {calendar}" : "{actor} deleted event {event} from calendar {calendar}",
+ "You deleted event {event} from calendar {calendar}" : "You deleted event {event} from calendar {calendar}",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} updated event {event} in calendar {calendar}",
+ "You updated event {event} in calendar {calendar}" : "You updated event {event} in calendar {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} restored event {event} of calendar {calendar}",
+ "You restored event {event} of calendar {calendar}" : "You restored event {event} of calendar {calendar}",
+ "Busy" : "Busy",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} created to-do {todo} in list {calendar}",
+ "You created to-do {todo} in list {calendar}" : "You created to-do {todo} in list {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} deleted to-do {todo} from list {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "You deleted to-do {todo} from list {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} updated to-do {todo} in list {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "You updated to-do {todo} in list {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} solved to-do {todo} in list {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "You solved to-do {todo} in list {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} reopened to-do {todo} in list {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "You reopened to-do {todo} in list {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}",
+ "Calendar, contacts and tasks" : "Calendar, contacts and tasks",
+ "A <strong>calendar</strong> was modified" : "A <strong>calendar</strong> was modified",
+ "A calendar <strong>event</strong> was modified" : "A calendar <strong>event</strong> was modified",
+ "A calendar <strong>to-do</strong> was modified" : "A calendar <strong>to-do</strong> was modified",
+ "Contact birthdays" : "Contact birthdays",
+ "Death of %s" : "Death of %s",
+ "Untitled calendar" : "Untitled calendar",
+ "Calendar:" : "Calendar:",
+ "Date:" : "Date:",
+ "Where:" : "Where:",
+ "Description:" : "Description:",
+ "_%n year_::_%n years_" : ["%n year","%n years"],
+ "_%n month_::_%n months_" : ["%n month","%n months"],
+ "_%n day_::_%n days_" : ["%n day","%n days"],
+ "_%n hour_::_%n hours_" : ["%n hour","%n hours"],
+ "_%n minute_::_%n minutes_" : ["%n minute","%n minutes"],
+ "%s (in %s)" : "%s (in %s)",
+ "%s (%s ago)" : "%s (%s ago)",
+ "Calendar: %s" : "Calendar: %s",
+ "Date: %s" : "Date: %s",
+ "Description: %s" : "Description: %s",
+ "Where: %s" : "Where: %s",
+ "%1$s via %2$s" : "%1$s via %2$s",
+ "Cancelled: %1$s" : "Cancelled: %1$s",
+ "\"%1$s\" has been canceled" : "\"%1$s\" has been canceled",
+ "Re: %1$s" : "Re: %1$s",
+ "%1$s has accepted your invitation" : "%1$s has accepted your invitation",
+ "%1$s has tentatively accepted your invitation" : "%1$s has tentatively accepted your invitation",
+ "%1$s has declined your invitation" : "%1$s has declined your invitation",
+ "%1$s has responded to your invitation" : "%1$s has responded to your invitation",
+ "Invitation updated: %1$s" : "Invitation updated: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s updated the event \"%2$s\"",
+ "Invitation: %1$s" : "Invitation: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s would like to invite you to \"%2$s\"",
+ "Organizer:" : "Organizer:",
+ "Attendees:" : "Attendees:",
+ "Title:" : "Title:",
+ "Location:" : "Location:",
+ "Link:" : "Link:",
+ "Accept" : "Accept",
+ "Decline" : "Decline",
+ "More options …" : "More options …",
+ "More options at %s" : "More options at %s",
+ "Monday" : "Monday",
+ "Tuesday" : "Tuesday",
+ "Wednesday" : "Wednesday",
+ "Thursday" : "Thursday",
+ "Friday" : "Friday",
+ "Saturday" : "Saturday",
+ "Sunday" : "Sunday",
+ "January" : "January",
+ "February" : "February",
+ "March" : "March",
+ "April" : "April",
+ "May" : "May",
+ "June" : "June",
+ "July" : "July",
+ "August" : "August",
+ "September" : "September",
+ "October" : "October",
+ "November" : "November",
+ "December" : "December",
+ "Contacts" : "Contacts",
+ "{actor} created address book {addressbook}" : "{actor} created address book {addressbook}",
+ "You created address book {addressbook}" : "You created address book {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} deleted address book {addressbook}",
+ "You deleted address book {addressbook}" : "You deleted address book {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} updated address book {addressbook}",
+ "You updated address book {addressbook}" : "You updated address book {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} shared address book {addressbook} with you",
+ "You shared address book {addressbook} with {user}" : "You shared address book {addressbook} with {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} shared address book {addressbook} with {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} unshared address book {addressbook} from you",
+ "You unshared address book {addressbook} from {user}" : "You unshared address book {addressbook} from {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} unshared address book {addressbook} from {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} unshared address book {addressbook} from themselves",
+ "You shared address book {addressbook} with group {group}" : "You shared address book {addressbook} with group {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} shared address book {addressbook} with group {group}",
+ "You unshared address book {addressbook} from group {group}" : "You unshared address book {addressbook} from group {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} unshared address book {addressbook} from group {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} created contact {card} in address book {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "You created contact {card} in address book {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} deleted contact {card} from address book {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "You deleted contact {card} from address book {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} updated contact {card} in address book {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "You updated contact {card} in address book {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "A <strong>contact</strong> or <strong>address book</strong> was modified",
+ "Accounts" : "Accounts",
+ "System address book which holds all accounts" : "System address book which holds all accounts",
+ "File is not updatable: %1$s" : "File is not updatable: %1$s",
+ "Could not write to final file, canceled by hook" : "Could not write to final file, canceled by hook",
+ "Could not write file contents" : "Could not write file contents",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)",
+ "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." : "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.",
+ "Could not rename part file to final file, canceled by hook" : "Could not rename part file to final file, canceled by hook",
+ "Could not rename part file to final file" : "Could not rename part file to final file",
+ "Failed to check file size: %1$s" : "Failed to check file size: %1$s",
+ "Encryption not ready: %1$s" : "Encryption not ready: %1$s",
+ "Failed to open file: %1$s" : "Failed to open file: %1$s",
+ "Failed to unlink: %1$s" : "Failed to unlink: %1$s",
+ "Failed to write file contents: %1$s" : "Failed to write file contents: %1$s",
+ "File not found: %1$s" : "File not found: %1$s",
+ "System is in maintenance mode." : "System is in maintenance mode.",
+ "Upgrade needed" : "Upgrade needed",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS.",
+ "Configures a CalDAV account" : "Configures a CalDAV account",
+ "Configures a CardDAV account" : "Configures a CardDAV account",
+ "Events" : "Events",
+ "Untitled task" : "Untitled task",
+ "Completed on %s" : "Completed on %s",
+ "Due on %s by %s" : "Due on %s by %s",
+ "Due on %s" : "Due on %s",
+ "DAV system address book" : "DAV system address book",
+ "No outstanding DAV system address book sync." : "No outstanding DAV system address book sync.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "WebDAV endpoint",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken.",
+ "Migrated calendar (%1$s)" : "Migrated calendar (%1$s)",
+ "Calendars including events, details and attendees" : "Calendars including events, details and attendees",
+ "Contacts and groups" : "Contacts and groups",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "Absence saved",
+ "Failed to save your absence settings" : "Failed to save your absence settings",
+ "Absence cleared" : "Absence cleared",
+ "Failed to clear your absence settings" : "Failed to clear your absence settings",
+ "First day" : "First day",
+ "Last day (inclusive)" : "Last day (inclusive)",
+ "Short absence status" : "Short absence status",
+ "Long absence Message" : "Long absence Message",
+ "Save" : "Save",
+ "Disable absence" : "Disable absence",
+ "Failed to load availability" : "Failed to load availability",
+ "Saved availability" : "Saved availability",
+ "Failed to save availability" : "Failed to save availability",
+ "Time zone:" : "Time zone:",
+ "to" : "to",
+ "Delete slot" : "Delete slot",
+ "No working hours set" : "No working hours set",
+ "Add slot" : "Add slot",
+ "Pick a start time for {dayName}" : "Pick a start time for {dayName}",
+ "Pick a end time for {dayName}" : "Pick a end time for {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications.",
+ "Cancel" : "Cancel",
+ "Import" : "Import",
+ "Error while saving settings" : "Error while saving settings",
+ "Reset to default" : "Reset to default",
+ "Availability" : "Availability",
+ "Absence" : "Absence",
+ "Configure your next absence period." : "Configure your next absence period.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Please make sure to properly set up {emailopen}the email server{linkclose}.",
+ "Calendar server" : "Calendar server",
+ "Send invitations to attendees" : "Send invitations to attendees",
+ "Automatically generate a birthday calendar" : "Automatically generate a birthday calendar",
+ "Birthday calendars will be generated by a background job." : "Birthday calendars will be generated by a background job.",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "Hence they will not be available immediately after enabling but will show up after some time.",
+ "Send notifications for events" : "Send notifications for events",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Notifications are sent via background jobs, so these must occur often enough.",
+ "Send reminder notifications to calendar sharees as well" : "Send reminder notifications to calendar sharees as well",
+ "Reminders are always sent to organizers and attendees." : "Reminders are always sent to organizers and attendees.",
+ "Enable notifications for events via push" : "Enable notifications for events via push",
+ "There was an error updating your attendance status." : "There was an error updating your attendance status.",
+ "Please contact the organizer directly." : "Please contact the organizer directly.",
+ "Are you accepting the invitation?" : "Are you accepting the invitation?",
+ "Tentative" : "Tentative",
+ "Your attendance was updated successfully." : "Your attendance was updated successfully."
+},
+"nplurals=2; plural=(n!=1);");
diff --git a/apps/dav/l10n/ka.json b/apps/dav/l10n/ka.json
new file mode 100644
index 00000000000..3685b1a2173
--- /dev/null
+++ b/apps/dav/l10n/ka.json
@@ -0,0 +1,218 @@
+{ "translations": {
+ "Calendar" : "Calendar",
+ "Tasks" : "Tasks",
+ "Personal" : "Personal",
+ "{actor} created calendar {calendar}" : "{actor} created calendar {calendar}",
+ "You created calendar {calendar}" : "You created calendar {calendar}",
+ "{actor} deleted calendar {calendar}" : "{actor} deleted calendar {calendar}",
+ "You deleted calendar {calendar}" : "You deleted calendar {calendar}",
+ "{actor} updated calendar {calendar}" : "{actor} updated calendar {calendar}",
+ "You updated calendar {calendar}" : "You updated calendar {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} restored calendar {calendar}",
+ "You restored calendar {calendar}" : "You restored calendar {calendar}",
+ "You shared calendar {calendar} as public link" : "You shared calendar {calendar} as public link",
+ "You removed public link for calendar {calendar}" : "You removed public link for calendar {calendar}",
+ "{actor} shared calendar {calendar} with you" : "{actor} shared calendar {calendar} with you",
+ "You shared calendar {calendar} with {user}" : "You shared calendar {calendar} with {user}",
+ "{actor} shared calendar {calendar} with {user}" : "{actor} shared calendar {calendar} with {user}",
+ "{actor} unshared calendar {calendar} from you" : "{actor} unshared calendar {calendar} from you",
+ "You unshared calendar {calendar} from {user}" : "You unshared calendar {calendar} from {user}",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor} unshared calendar {calendar} from {user}",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} unshared calendar {calendar} from themselves",
+ "You shared calendar {calendar} with group {group}" : "You shared calendar {calendar} with group {group}",
+ "{actor} shared calendar {calendar} with group {group}" : "{actor} shared calendar {calendar} with group {group}",
+ "You unshared calendar {calendar} from group {group}" : "You unshared calendar {calendar} from group {group}",
+ "{actor} unshared calendar {calendar} from group {group}" : "{actor} unshared calendar {calendar} from group {group}",
+ "Untitled event" : "Untitled event",
+ "{actor} created event {event} in calendar {calendar}" : "{actor} created event {event} in calendar {calendar}",
+ "You created event {event} in calendar {calendar}" : "You created event {event} in calendar {calendar}",
+ "{actor} deleted event {event} from calendar {calendar}" : "{actor} deleted event {event} from calendar {calendar}",
+ "You deleted event {event} from calendar {calendar}" : "You deleted event {event} from calendar {calendar}",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} updated event {event} in calendar {calendar}",
+ "You updated event {event} in calendar {calendar}" : "You updated event {event} in calendar {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} restored event {event} of calendar {calendar}",
+ "You restored event {event} of calendar {calendar}" : "You restored event {event} of calendar {calendar}",
+ "Busy" : "Busy",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} created to-do {todo} in list {calendar}",
+ "You created to-do {todo} in list {calendar}" : "You created to-do {todo} in list {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} deleted to-do {todo} from list {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "You deleted to-do {todo} from list {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} updated to-do {todo} in list {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "You updated to-do {todo} in list {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} solved to-do {todo} in list {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "You solved to-do {todo} in list {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} reopened to-do {todo} in list {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "You reopened to-do {todo} in list {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}",
+ "Calendar, contacts and tasks" : "Calendar, contacts and tasks",
+ "A <strong>calendar</strong> was modified" : "A <strong>calendar</strong> was modified",
+ "A calendar <strong>event</strong> was modified" : "A calendar <strong>event</strong> was modified",
+ "A calendar <strong>to-do</strong> was modified" : "A calendar <strong>to-do</strong> was modified",
+ "Contact birthdays" : "Contact birthdays",
+ "Death of %s" : "Death of %s",
+ "Untitled calendar" : "Untitled calendar",
+ "Calendar:" : "Calendar:",
+ "Date:" : "Date:",
+ "Where:" : "Where:",
+ "Description:" : "Description:",
+ "_%n year_::_%n years_" : ["%n year","%n years"],
+ "_%n month_::_%n months_" : ["%n month","%n months"],
+ "_%n day_::_%n days_" : ["%n day","%n days"],
+ "_%n hour_::_%n hours_" : ["%n hour","%n hours"],
+ "_%n minute_::_%n minutes_" : ["%n minute","%n minutes"],
+ "%s (in %s)" : "%s (in %s)",
+ "%s (%s ago)" : "%s (%s ago)",
+ "Calendar: %s" : "Calendar: %s",
+ "Date: %s" : "Date: %s",
+ "Description: %s" : "Description: %s",
+ "Where: %s" : "Where: %s",
+ "%1$s via %2$s" : "%1$s via %2$s",
+ "Cancelled: %1$s" : "Cancelled: %1$s",
+ "\"%1$s\" has been canceled" : "\"%1$s\" has been canceled",
+ "Re: %1$s" : "Re: %1$s",
+ "%1$s has accepted your invitation" : "%1$s has accepted your invitation",
+ "%1$s has tentatively accepted your invitation" : "%1$s has tentatively accepted your invitation",
+ "%1$s has declined your invitation" : "%1$s has declined your invitation",
+ "%1$s has responded to your invitation" : "%1$s has responded to your invitation",
+ "Invitation updated: %1$s" : "Invitation updated: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s updated the event \"%2$s\"",
+ "Invitation: %1$s" : "Invitation: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s would like to invite you to \"%2$s\"",
+ "Organizer:" : "Organizer:",
+ "Attendees:" : "Attendees:",
+ "Title:" : "Title:",
+ "Location:" : "Location:",
+ "Link:" : "Link:",
+ "Accept" : "Accept",
+ "Decline" : "Decline",
+ "More options …" : "More options …",
+ "More options at %s" : "More options at %s",
+ "Monday" : "Monday",
+ "Tuesday" : "Tuesday",
+ "Wednesday" : "Wednesday",
+ "Thursday" : "Thursday",
+ "Friday" : "Friday",
+ "Saturday" : "Saturday",
+ "Sunday" : "Sunday",
+ "January" : "January",
+ "February" : "February",
+ "March" : "March",
+ "April" : "April",
+ "May" : "May",
+ "June" : "June",
+ "July" : "July",
+ "August" : "August",
+ "September" : "September",
+ "October" : "October",
+ "November" : "November",
+ "December" : "December",
+ "Contacts" : "Contacts",
+ "{actor} created address book {addressbook}" : "{actor} created address book {addressbook}",
+ "You created address book {addressbook}" : "You created address book {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} deleted address book {addressbook}",
+ "You deleted address book {addressbook}" : "You deleted address book {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} updated address book {addressbook}",
+ "You updated address book {addressbook}" : "You updated address book {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} shared address book {addressbook} with you",
+ "You shared address book {addressbook} with {user}" : "You shared address book {addressbook} with {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} shared address book {addressbook} with {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} unshared address book {addressbook} from you",
+ "You unshared address book {addressbook} from {user}" : "You unshared address book {addressbook} from {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} unshared address book {addressbook} from {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} unshared address book {addressbook} from themselves",
+ "You shared address book {addressbook} with group {group}" : "You shared address book {addressbook} with group {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} shared address book {addressbook} with group {group}",
+ "You unshared address book {addressbook} from group {group}" : "You unshared address book {addressbook} from group {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} unshared address book {addressbook} from group {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} created contact {card} in address book {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "You created contact {card} in address book {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} deleted contact {card} from address book {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "You deleted contact {card} from address book {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} updated contact {card} in address book {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "You updated contact {card} in address book {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "A <strong>contact</strong> or <strong>address book</strong> was modified",
+ "Accounts" : "Accounts",
+ "System address book which holds all accounts" : "System address book which holds all accounts",
+ "File is not updatable: %1$s" : "File is not updatable: %1$s",
+ "Could not write to final file, canceled by hook" : "Could not write to final file, canceled by hook",
+ "Could not write file contents" : "Could not write file contents",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)",
+ "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." : "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.",
+ "Could not rename part file to final file, canceled by hook" : "Could not rename part file to final file, canceled by hook",
+ "Could not rename part file to final file" : "Could not rename part file to final file",
+ "Failed to check file size: %1$s" : "Failed to check file size: %1$s",
+ "Encryption not ready: %1$s" : "Encryption not ready: %1$s",
+ "Failed to open file: %1$s" : "Failed to open file: %1$s",
+ "Failed to unlink: %1$s" : "Failed to unlink: %1$s",
+ "Failed to write file contents: %1$s" : "Failed to write file contents: %1$s",
+ "File not found: %1$s" : "File not found: %1$s",
+ "System is in maintenance mode." : "System is in maintenance mode.",
+ "Upgrade needed" : "Upgrade needed",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS.",
+ "Configures a CalDAV account" : "Configures a CalDAV account",
+ "Configures a CardDAV account" : "Configures a CardDAV account",
+ "Events" : "Events",
+ "Untitled task" : "Untitled task",
+ "Completed on %s" : "Completed on %s",
+ "Due on %s by %s" : "Due on %s by %s",
+ "Due on %s" : "Due on %s",
+ "DAV system address book" : "DAV system address book",
+ "No outstanding DAV system address book sync." : "No outstanding DAV system address book sync.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "WebDAV endpoint",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken.",
+ "Migrated calendar (%1$s)" : "Migrated calendar (%1$s)",
+ "Calendars including events, details and attendees" : "Calendars including events, details and attendees",
+ "Contacts and groups" : "Contacts and groups",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "Absence saved",
+ "Failed to save your absence settings" : "Failed to save your absence settings",
+ "Absence cleared" : "Absence cleared",
+ "Failed to clear your absence settings" : "Failed to clear your absence settings",
+ "First day" : "First day",
+ "Last day (inclusive)" : "Last day (inclusive)",
+ "Short absence status" : "Short absence status",
+ "Long absence Message" : "Long absence Message",
+ "Save" : "Save",
+ "Disable absence" : "Disable absence",
+ "Failed to load availability" : "Failed to load availability",
+ "Saved availability" : "Saved availability",
+ "Failed to save availability" : "Failed to save availability",
+ "Time zone:" : "Time zone:",
+ "to" : "to",
+ "Delete slot" : "Delete slot",
+ "No working hours set" : "No working hours set",
+ "Add slot" : "Add slot",
+ "Pick a start time for {dayName}" : "Pick a start time for {dayName}",
+ "Pick a end time for {dayName}" : "Pick a end time for {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications.",
+ "Cancel" : "Cancel",
+ "Import" : "Import",
+ "Error while saving settings" : "Error while saving settings",
+ "Reset to default" : "Reset to default",
+ "Availability" : "Availability",
+ "Absence" : "Absence",
+ "Configure your next absence period." : "Configure your next absence period.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Please make sure to properly set up {emailopen}the email server{linkclose}.",
+ "Calendar server" : "Calendar server",
+ "Send invitations to attendees" : "Send invitations to attendees",
+ "Automatically generate a birthday calendar" : "Automatically generate a birthday calendar",
+ "Birthday calendars will be generated by a background job." : "Birthday calendars will be generated by a background job.",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "Hence they will not be available immediately after enabling but will show up after some time.",
+ "Send notifications for events" : "Send notifications for events",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Notifications are sent via background jobs, so these must occur often enough.",
+ "Send reminder notifications to calendar sharees as well" : "Send reminder notifications to calendar sharees as well",
+ "Reminders are always sent to organizers and attendees." : "Reminders are always sent to organizers and attendees.",
+ "Enable notifications for events via push" : "Enable notifications for events via push",
+ "There was an error updating your attendance status." : "There was an error updating your attendance status.",
+ "Please contact the organizer directly." : "Please contact the organizer directly.",
+ "Are you accepting the invitation?" : "Are you accepting the invitation?",
+ "Tentative" : "Tentative",
+ "Your attendance was updated successfully." : "Your attendance was updated successfully."
+},"pluralForm" :"nplurals=2; plural=(n!=1);"
+} \ No newline at end of file
diff --git a/apps/dav/l10n/ka_GE.js b/apps/dav/l10n/ka_GE.js
deleted file mode 100644
index 05fa6351239..00000000000
--- a/apps/dav/l10n/ka_GE.js
+++ /dev/null
@@ -1,66 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "კალენდარი",
- "Todos" : "შესასრულებელი დავალებები",
- "Personal" : "პირადი",
- "{actor} created calendar {calendar}" : "{actor} მომხმარებელმა შექმნა კალენდარი {calendar}",
- "You created calendar {calendar}" : "თქვენ შექმენით {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} მომხმარებელმამა გააუქმა კალენდარი {calendar}",
- "You deleted calendar {calendar}" : "თქვენ გააუქმეთ კალენდარი {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} მომხმარებელმა განაახლა კალენდარი {calendar}",
- "You updated calendar {calendar}" : "თქვენ განაახლეთ კალენდარი {calendar}",
- "You shared calendar {calendar} as public link" : "თქვენ გააზიარეთ კალენდარი {calendar} საზოგადო ბმულის სახით",
- "You removed public link for calendar {calendar}" : "თქვენ გააუქმეთ საზოგადო ბმული კალენდარზე {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} მომხმარებელმა თქვენთან გააზიარა კალენდარი {calendar}",
- "You shared calendar {calendar} with {user}" : "თქვენ გააზიარეთ კალენდარი {calendar} მომხმარებელთან {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} მომხმარებელმა გააზიარა {calendar} მომხმარებელთან {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} მომხმარებელმა თქვენთან შეწყვიტა {calendar} კალენდრის გაზიარება",
- "You unshared calendar {calendar} from {user}" : "თქვენ შეწყვიტეთ {calendar} კალენდრის გაზიარება მომხმარებელთან {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} მომხმარებელმა შეწყვიტა {calendar} კალენდრის გაზიარება მომხმარებელთან {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} მომხმარებელმა შეწყვიტა {calendar} კალენდრის გაზიარება",
- "You shared calendar {calendar} with group {group}" : "თქვენ გააზიარეთ კალენდარი {calendar} ჯგუფთან {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} მომხმარებელმა გააზიარა {calendar} კალენდარი ჯგუფთან {group}",
- "You unshared calendar {calendar} from group {group}" : "თქვენ შეწყვიტეთ კალენდრის {calendar} გაზიარება ჯგუფთან {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} მომხმარებელმა შეწყვიტა {calendar} კალენდრის გაზიარება ჯგუფთან {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} მომხმარებელმა შექმნა მოვლენა {event} კალენდარში {calendar}",
- "You created event {event} in calendar {calendar}" : "თქვენ შექმენით მოვლენა {event} კალენდარში {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} მომხმარებელმა გააუქმა მოვლენა {event} კალენდარში {calendar}",
- "You deleted event {event} from calendar {calendar}" : "თქვენ გააუქმეთ მოვლენა {event} კალენდარში {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} მომხმარებელმა განაახლა მოვლენა {event} კალენდარში {calendar}",
- "You updated event {event} in calendar {calendar}" : "თქვენ განაახლეთ მოვლენა {event} კალენდარში {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} მომხმარებელმა შექმნა შესასრულებელი დავალება {todo} სიაში {calendar}",
- "You created todo {todo} in list {calendar}" : "თქვენ შექმენით შესასრულებელი დავალება {todo} სიაში {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} მომხმარებელმა გააუქმა შესასრულებელი დავალება {todo} სიიდან {calendar}",
- "You deleted todo {todo} from list {calendar}" : "თქვენ გააუქმეთ შესასრულებელი დავალება {todo} სიიდან {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} მომხმარებელმა განაახლა შესასრულებელი დავალება {todo} სიაში {calendar}",
- "You updated todo {todo} in list {calendar}" : "თქვენ განაახლეთ შესასრულებელი დავალება {todo} in list {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} მომხმარებელმა დაასრულა შესასრულებელი დავალება {todo} სიაში {calendar}",
- "You solved todo {todo} in list {calendar}" : "თქვენ დაასრულეთ შესასრულებელი დავალება {todo} სიაში {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} მომხმარებელმა ხელახლა გახსნა შესასრულებელი დავალება {todo} სიაში {calendar}",
- "You reopened todo {todo} in list {calendar}" : "თქვენ ხელახლა გახსენით შესასრულებელი დავალება {todo} სიაში {calendar}",
- "A <strong>calendar</strong> was modified" : "<strong>კალენდარი</strong> შეიცვალა",
- "A calendar <strong>event</strong> was modified" : "კალენდრის <strong>მოვლენა</strong> შეიცვალა",
- "A calendar <strong>todo</strong> was modified" : "კალენდრის <strong>შესასრულებელი დავალება</strong> შეიცვალა",
- "Contact birthdays" : "კონტაქტების დაბადების დღეები",
- "Where:" : "სად:",
- "Description:" : "აღწერა:",
- "Invitation canceled" : "მოწვევა გაუქმდა",
- "Invitation updated" : "მოწვევა განახლდა",
- "Location:" : "ადგილმდებარეობა:",
- "Link:" : "ბმული:",
- "Accept" : "მიღება",
- "Decline" : "ურაყოფა",
- "Contacts" : "კონტაქტები",
- "Tasks" : "დავალებები",
- "WebDAV" : "WebDAV",
- "Tentative" : "საცდელი",
- "Save" : "შენახვა",
- "Send invitations to attendees" : "გაუგზავნეთ მოწვევა დამსწრეებს",
- "Automatically generate a birthday calendar" : "დაბადების დღეების კალენდრების ავტომატური გენერირება",
- "Birthday calendars will be generated by a background job." : "ბადადების დღეების კალენდრები გენერირებულ იქნება ფონურ რეჟიმში.",
- "Hence they will not be available immediately after enabling but will show up after some time." : "ისინი არ იქნებიან ხელმიწავდომნი უცბად, მაგრამ გამოჩნდებიან გარკვეული პერიოდის შემდეგ.",
- "Hello %s," : "გამარჯობა %s,",
- "When:" : "როდის:"
-},
-"nplurals=2; plural=(n!=1);");
diff --git a/apps/dav/l10n/ka_GE.json b/apps/dav/l10n/ka_GE.json
deleted file mode 100644
index 7597200d061..00000000000
--- a/apps/dav/l10n/ka_GE.json
+++ /dev/null
@@ -1,64 +0,0 @@
-{ "translations": {
- "Calendar" : "კალენდარი",
- "Todos" : "შესასრულებელი დავალებები",
- "Personal" : "პირადი",
- "{actor} created calendar {calendar}" : "{actor} მომხმარებელმა შექმნა კალენდარი {calendar}",
- "You created calendar {calendar}" : "თქვენ შექმენით {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} მომხმარებელმამა გააუქმა კალენდარი {calendar}",
- "You deleted calendar {calendar}" : "თქვენ გააუქმეთ კალენდარი {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} მომხმარებელმა განაახლა კალენდარი {calendar}",
- "You updated calendar {calendar}" : "თქვენ განაახლეთ კალენდარი {calendar}",
- "You shared calendar {calendar} as public link" : "თქვენ გააზიარეთ კალენდარი {calendar} საზოგადო ბმულის სახით",
- "You removed public link for calendar {calendar}" : "თქვენ გააუქმეთ საზოგადო ბმული კალენდარზე {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} მომხმარებელმა თქვენთან გააზიარა კალენდარი {calendar}",
- "You shared calendar {calendar} with {user}" : "თქვენ გააზიარეთ კალენდარი {calendar} მომხმარებელთან {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} მომხმარებელმა გააზიარა {calendar} მომხმარებელთან {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} მომხმარებელმა თქვენთან შეწყვიტა {calendar} კალენდრის გაზიარება",
- "You unshared calendar {calendar} from {user}" : "თქვენ შეწყვიტეთ {calendar} კალენდრის გაზიარება მომხმარებელთან {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} მომხმარებელმა შეწყვიტა {calendar} კალენდრის გაზიარება მომხმარებელთან {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} მომხმარებელმა შეწყვიტა {calendar} კალენდრის გაზიარება",
- "You shared calendar {calendar} with group {group}" : "თქვენ გააზიარეთ კალენდარი {calendar} ჯგუფთან {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} მომხმარებელმა გააზიარა {calendar} კალენდარი ჯგუფთან {group}",
- "You unshared calendar {calendar} from group {group}" : "თქვენ შეწყვიტეთ კალენდრის {calendar} გაზიარება ჯგუფთან {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} მომხმარებელმა შეწყვიტა {calendar} კალენდრის გაზიარება ჯგუფთან {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} მომხმარებელმა შექმნა მოვლენა {event} კალენდარში {calendar}",
- "You created event {event} in calendar {calendar}" : "თქვენ შექმენით მოვლენა {event} კალენდარში {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} მომხმარებელმა გააუქმა მოვლენა {event} კალენდარში {calendar}",
- "You deleted event {event} from calendar {calendar}" : "თქვენ გააუქმეთ მოვლენა {event} კალენდარში {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} მომხმარებელმა განაახლა მოვლენა {event} კალენდარში {calendar}",
- "You updated event {event} in calendar {calendar}" : "თქვენ განაახლეთ მოვლენა {event} კალენდარში {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} მომხმარებელმა შექმნა შესასრულებელი დავალება {todo} სიაში {calendar}",
- "You created todo {todo} in list {calendar}" : "თქვენ შექმენით შესასრულებელი დავალება {todo} სიაში {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} მომხმარებელმა გააუქმა შესასრულებელი დავალება {todo} სიიდან {calendar}",
- "You deleted todo {todo} from list {calendar}" : "თქვენ გააუქმეთ შესასრულებელი დავალება {todo} სიიდან {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} მომხმარებელმა განაახლა შესასრულებელი დავალება {todo} სიაში {calendar}",
- "You updated todo {todo} in list {calendar}" : "თქვენ განაახლეთ შესასრულებელი დავალება {todo} in list {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} მომხმარებელმა დაასრულა შესასრულებელი დავალება {todo} სიაში {calendar}",
- "You solved todo {todo} in list {calendar}" : "თქვენ დაასრულეთ შესასრულებელი დავალება {todo} სიაში {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} მომხმარებელმა ხელახლა გახსნა შესასრულებელი დავალება {todo} სიაში {calendar}",
- "You reopened todo {todo} in list {calendar}" : "თქვენ ხელახლა გახსენით შესასრულებელი დავალება {todo} სიაში {calendar}",
- "A <strong>calendar</strong> was modified" : "<strong>კალენდარი</strong> შეიცვალა",
- "A calendar <strong>event</strong> was modified" : "კალენდრის <strong>მოვლენა</strong> შეიცვალა",
- "A calendar <strong>todo</strong> was modified" : "კალენდრის <strong>შესასრულებელი დავალება</strong> შეიცვალა",
- "Contact birthdays" : "კონტაქტების დაბადების დღეები",
- "Where:" : "სად:",
- "Description:" : "აღწერა:",
- "Invitation canceled" : "მოწვევა გაუქმდა",
- "Invitation updated" : "მოწვევა განახლდა",
- "Location:" : "ადგილმდებარეობა:",
- "Link:" : "ბმული:",
- "Accept" : "მიღება",
- "Decline" : "ურაყოფა",
- "Contacts" : "კონტაქტები",
- "Tasks" : "დავალებები",
- "WebDAV" : "WebDAV",
- "Tentative" : "საცდელი",
- "Save" : "შენახვა",
- "Send invitations to attendees" : "გაუგზავნეთ მოწვევა დამსწრეებს",
- "Automatically generate a birthday calendar" : "დაბადების დღეების კალენდრების ავტომატური გენერირება",
- "Birthday calendars will be generated by a background job." : "ბადადების დღეების კალენდრები გენერირებულ იქნება ფონურ რეჟიმში.",
- "Hence they will not be available immediately after enabling but will show up after some time." : "ისინი არ იქნებიან ხელმიწავდომნი უცბად, მაგრამ გამოჩნდებიან გარკვეული პერიოდის შემდეგ.",
- "Hello %s," : "გამარჯობა %s,",
- "When:" : "როდის:"
-},"pluralForm" :"nplurals=2; plural=(n!=1);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/ko.js b/apps/dav/l10n/ko.js
index 75cec410fdf..ae3b8e5908c 100644
--- a/apps/dav/l10n/ko.js
+++ b/apps/dav/l10n/ko.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "달력",
- "Todos" : "할 일",
+ "Tasks" : "작업",
"Personal" : "개인",
"{actor} created calendar {calendar}" : "{actor} 님이 달력 {calendar}을(를) 생성함",
"You created calendar {calendar}" : "달력 {calendar}을(를) 생성함",
@@ -10,7 +10,7 @@ OC.L10N.register(
"You deleted calendar {calendar}" : "달력 {calendar}을(를) 삭제함",
"{actor} updated calendar {calendar}" : "{actor} 님이 달력 {calendar}을(를) 업데이트함",
"You updated calendar {calendar}" : "달력 {calendar}을(를) 업데이트함",
- "{actor} restored calendar {calendar}" : "{actor} 님이 달력 {calendar}을(를) 복구함",
+ "{actor} restored calendar {calendar}" : "{actor}님이 달력 {calendar}을(를) 복구함",
"You restored calendar {calendar}" : "달력 {calendar}을(를) 복구함",
"You shared calendar {calendar} as public link" : "달력 {calendar}을(를) 공개 링크로 공유함",
"You removed public link for calendar {calendar}" : "달력 {calendar}의 공개 링크를 삭제함",
@@ -25,126 +25,197 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} 님이 달력 {calendar}을(를) 그룹 {group}와(과) 공유함",
"You unshared calendar {calendar} from group {group}" : "달력 {calendar}을(를) 그룹 {group}와(과) 공유하지 않음",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} 님이 달력 {calendar}을(를) 그룹 {group}와(과) 공유하지 않음",
+ "Untitled event" : "제목 없는 일정",
"{actor} created event {event} in calendar {calendar}" : "{actor} 님이 행사 {event}을(를) 달력 {calendar}에 생성함",
"You created event {event} in calendar {calendar}" : "행사 {event}을(를) 달력 {calendar}에 생성함",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} 님이 행사 {event}을(를) 달력 {calendar}에서 삭제함",
"You deleted event {event} from calendar {calendar}" : "행사 {event}을(를) 달력 {calendar}에서 삭제함",
"{actor} updated event {event} in calendar {calendar}" : "{actor} 님이 달력 {calendar}의 행사 {event}을(를) 업데이트함",
"You updated event {event} in calendar {calendar}" : "달력 {calendar}의 행사 {event}을(를) 업데이트함",
- "{actor} restored event {event} of calendar {calendar}" : "{actor} 님이 행사 {event}을(를) 달력 {calendar}에 복구함",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor}님이 행사 {event}을(를) 달력 {sourceCalendar}에서 달력 {targetCalendar}(으)로 옮김",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "행사 {event}을(를) 달력 {sourceCalendar}에서 달력 {targetCalendar}(으)로 옮김",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor}님이 행사 {event}을(를) 달력 {calendar}에 복구함",
"You restored event {event} of calendar {calendar}" : "행사 {event}을(를) 달력 {calendar}에 복구함",
"Busy" : "바쁨",
- "{actor} created todo {todo} in list {calendar}" : "{actor} 님이 목록 {calendar}에 할 일 {todo}을(를) 생성함",
- "You created todo {todo} in list {calendar}" : "목록 {calendar}에 할 일 {todo}을(를) 생성함",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} 님이 목록 {calendar}에서 할 일 {todo}을(를) 삭제함",
- "You deleted todo {todo} from list {calendar}" : "목록 {calendar}에서 할 일 {todo}을(를) 삭제함",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} 님이 목록 {calendar}의 할 일 {todo}을(를) 업데이트함",
- "You updated todo {todo} in list {calendar}" : "목록 {calendar}의 할 일 {todo}을(를) 업데이트함",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} 님이 목록 {calendar}의 할 일 {todo}을(를) 끝냄",
- "You solved todo {todo} in list {calendar}" : "목록 {calendar}의 할 일 {todo}을(를) 끝냄",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} 님이 목록 {calendar}의 할 일 {todo}을(를) 다시 염",
- "You reopened todo {todo} in list {calendar}" : "목록 {calendar}의 할 일 {todo}을(를) 다시 염",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor}님이 목록 {calendar}에 할 일 {todo}을(를) 생성함",
+ "You created to-do {todo} in list {calendar}" : "목록 {calendar}에 할 일 {todo}을(를) 생성함",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor}님이 목록 {calendar}에서 할 일 {todo}을(를) 삭제함",
+ "You deleted to-do {todo} from list {calendar}" : "목록 {calendar}에서 할 일 {todo}을(를) 삭제함",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor}님이 목록 {calendar}의 할 일 {todo}을(를) 업데이트함",
+ "You updated to-do {todo} in list {calendar}" : "목록 {calendar}의 할 일 {todo}을(를) 업데이트함",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor}님이 목록 {calendar}의 할 일 {todo}을(를) 끝냄",
+ "You solved to-do {todo} in list {calendar}" : "목록 {calendar}의 할 일 {todo}을(를) 끝냄",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor}님이 목록 {calendar}의 할 일 {todo}을(를) 다시 엶",
+ "You reopened to-do {todo} in list {calendar}" : "목록 {calendar}의 할 일 {todo}을(를) 다시 엶",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor}님이 할 일 {todo}을(를) 목록 {sourceCalendar}에서 목록 {targetCalendar}(으)로 옮김",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "할 일 {todo}을(를) 목록 {sourceCalendar}에서 목록 {targetCalendar}(으)로 옮김",
"Calendar, contacts and tasks" : "달력, 연락처 및 작업",
"A <strong>calendar</strong> was modified" : "<strong>달력</strong>이 수정됨",
"A calendar <strong>event</strong> was modified" : "달력 <strong>행사</strong>가 수정됨",
- "A calendar <strong>todo</strong> was modified" : "달력의 <strong>할 일</strong>이 수정됨",
+ "A calendar <strong>to-do</strong> was modified" : "달력 <strong>할 일</strong>이 수정됨",
"Contact birthdays" : "연락처에 등록된 생일",
- "Death of %s" : "%s의 사망",
+ "Death of %s" : "%s의 기일",
+ "Untitled calendar" : "제목없는 달력",
"Calendar:" : "달력:",
"Date:" : "날짜:",
"Where:" : "장소:",
"Description:" : "설명:",
- "Untitled event" : "제목없는 이벤트",
"_%n year_::_%n years_" : ["%n년"],
- "_%n month_::_%n months_" : ["%d개월"],
+ "_%n month_::_%n months_" : ["%n개월"],
"_%n day_::_%n days_" : ["%n일"],
- "_%n hour_::_%n hours_" : ["%d시간"],
- "_%n minute_::_%n minutes_" : ["%d분"],
- "%s (in %s)" : "%s(%s에)",
- "%s (%s ago)" : "%s(%s 전)",
+ "_%n hour_::_%n hours_" : ["%n시간"],
+ "_%n minute_::_%n minutes_" : ["%n분"],
+ "%s (in %s)" : "%s(%s 후)",
+ "%s (%s ago)" : "%s(%s 지남)",
"Calendar: %s" : "달력: %s",
"Date: %s" : "날짜: %s",
"Description: %s" : "설명: %s",
"Where: %s" : "장소: %s",
"%1$s via %2$s" : "%1$s(%2$s 경유)",
"Cancelled: %1$s" : "취소됨: %1$s",
- "Invitation canceled" : "초대장 취소됨",
+ "\"%1$s\" has been canceled" : "\"%1$s\"이(가) 취소되었습니다",
"Re: %1$s" : "Re: %1$s",
- "Invitation updated" : "초대장 업데이트됨",
+ "%1$s has accepted your invitation" : "%1$s이(가) 초대를 수락했습니다",
+ "%1$s has tentatively accepted your invitation" : "%1$s이(기) 초대를 잠정 수락했습니다",
+ "%1$s has declined your invitation" : "%1$s이(가) 초대를 거절했습니다",
+ "%1$s has responded to your invitation" : "%1$s이(가) 초대에 응답했습니다",
+ "Invitation updated: %1$s" : "초대 갱신됨: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s님이 일정 \"%2$s\"을(를) 갱신했습니다",
"Invitation: %1$s" : "초대: %1$s",
- "Invitation" : "초대",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s님이 나를 \"%2$s\"에 초대했습니다",
+ "Organizer:" : "주최자:",
+ "Attendees:" : "참석자:",
"Title:" : "제목:",
- "Time:" : "시간:",
+ "When:" : "일시:",
"Location:" : "위치:",
"Link:" : "링크:",
- "Organizer:" : "주최자:",
- "Attendees:" : "참석자:",
"Accept" : "수락",
"Decline" : "거절",
"More options …" : "더 많은 옵션 …",
"More options at %s" : "%s에 더 많은 옵션 있음",
+ "Monday" : "월요일",
+ "Tuesday" : "화요일",
+ "Wednesday" : "수요일",
+ "Thursday" : "목요일",
+ "Friday" : "금요일",
+ "Saturday" : "토요일",
+ "Sunday" : "일요일",
+ "January" : "1월",
+ "February" : "2월",
+ "March" : "3월",
+ "April" : "4월",
+ "May" : "5월",
+ "June" : "6월",
+ "July" : "7월",
+ "August" : "8월",
+ "September" : "9월",
+ "October" : "10월",
+ "November" : "11월",
+ "December" : "12월",
"Contacts" : "연락처",
- "{actor} created address book {addressbook}" : "{actor} 님이 주소록 {addressbook}을(를) 생성함",
+ "{actor} created address book {addressbook}" : "{actor}님이 주소록 {addressbook}을(를) 생성함",
"You created address book {addressbook}" : "주소록 {addressbook}을(를) 생성함",
- "{actor} deleted address book {addressbook}" : "{actor} 님이 주소록 {addressbook}을(를) 제거함",
+ "{actor} deleted address book {addressbook}" : "{actor}님이 주소록 {addressbook}을(를) 제거함",
"You deleted address book {addressbook}" : "주소록 {addressbook}을(를) 제거함",
- "{actor} updated address book {addressbook}" : "{actor} 님이 주소록 {addressbook}을(를) 갱신함",
+ "{actor} updated address book {addressbook}" : "{actor}님이 주소록 {addressbook}을(를) 갱신함",
"You updated address book {addressbook}" : "주소록 {addressbook}을(를) 갱신함",
- "{actor} shared address book {addressbook} with you" : "{actor} 님이 나와 주소록 {addressbook}을(를) 공유함",
- "You shared address book {addressbook} with {user}" : "{user} 님과 주소록 {addressbook}을(를) 공유함",
- "{actor} shared address book {addressbook} with {user}" : "{actor} 님이 {user} 님과 주소록 {addressbook}을(를) 공유함",
- "{actor} unshared address book {addressbook} from you" : "{actor} 님이 주소록 {addressbook}을(를) 더이상 공유하지 않음",
- "You unshared address book {addressbook} from {user}" : "{user} 님과 주소록 {addressbook}을(를) 더이상 공유하지 않음",
- "{actor} unshared address book {addressbook} from {user}" : "{actor} 님이 {user} 님과 주소록 {addressbook}을(를) 더이상 공유하지 않음",
+ "{actor} shared address book {addressbook} with you" : "{actor}님이 나와 주소록 {addressbook}을(를) 공유함",
+ "You shared address book {addressbook} with {user}" : "{user}님과 주소록 {addressbook}을(를) 공유함",
+ "{actor} shared address book {addressbook} with {user}" : "{actor}님이 {user}님과 주소록 {addressbook}을(를) 공유함",
+ "{actor} unshared address book {addressbook} from you" : "{actor}님이 주소록 {addressbook}의 공유를 해제함",
+ "You unshared address book {addressbook} from {user}" : "{user}님과 주소록 {addressbook}의 공유를 해제함",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor}님이 {user}님과 주소록 {addressbook}의 공유를 해제함",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor}님이 자신과 주소록 {addressbook}의 공유를 해제함",
"You shared address book {addressbook} with group {group}" : "그룹 {group}과(와) 주소록 {addressbook}을(를) 공유함",
- "{actor} shared address book {addressbook} with group {group}" : "{actor} 님이 그룹 {group}과(와) 주소록 {addressbook}을(를) 공유함",
- "You unshared address book {addressbook} from group {group}" : "그룹 {group}과(와) 주소록 {addressbook}을(를) 더이상 공유하지 않음",
- "{actor} unshared address book {addressbook} from group {group}" : "{actor} 님이 그룹 {group}과(와) 주소록 {addressbook}을(를) 더이상 공유하지 않음",
- "{actor} created contact {card} in address book {addressbook}" : "{actor} 님이 연락처 {card}을(를) 주소록 {addressbook}에 생성함",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor}님이 그룹 {group}과(와) 주소록 {addressbook}을(를) 공유함",
+ "You unshared address book {addressbook} from group {group}" : "그룹 {group}과(와) 주소록 {addressbook}의 공유를 해제함",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor}님이 그룹 {group}과(와) 주소록 {addressbook}의 공유를 해제함",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor}님이 연락처 {card}을(를) 주소록 {addressbook}에 생성함",
"You created contact {card} in address book {addressbook}" : "연락처 {card}을(를) 주소록 {addressbook}에 생성함",
- "{actor} deleted contact {card} from address book {addressbook}" : "{actor} 님이 연락처 {card}을(를) 주소록 {addressbook}에서 제거함",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor}님이 연락처 {card}을(를) 주소록 {addressbook}에서 제거함",
"You deleted contact {card} from address book {addressbook}" : "연락처 {card}을(를) 주소록 {addressbook}에서 제거함",
- "{actor} updated contact {card} in address book {addressbook}" : "{actor} 님이 주소록 {addressbook}의 연락처 {card}을(를) 갱신함",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor}님이 주소록 {addressbook}의 연락처 {card}을(를) 갱신함",
"You updated contact {card} in address book {addressbook}" : "주소록 {addressbook}의 연락처 {card}을(를) 갱신함",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>연락처</strong> 또는 <strong>주소록</strong>이 변경됨",
- "System is in maintenance mode." : "시스템이 유지 관리 모드입니다.",
+ "Accounts" : "계정",
+ "System address book which holds all accounts" : "시스템 주소록이 모든 계정 정보를 보유합니다",
+ "File is not updatable: %1$s" : "파일을 갱신할 수 없습니다: %1$s",
+ "Could not write to final file, canceled by hook" : "후크에 의해 취소되어 최종 파일에 쓸 수 없음",
+ "Could not write file contents" : "파일 내용을 쓸 수 없음",
+ "_%n byte_::_%n bytes_" : ["%n 바이트"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "파일을 대상 위치로 복사하는 동안 오류 발생 (복사됨: %1$s, 예상 파일 크기: %2$s)",
+ "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." : "예상 파일 크기는 %1$s이지만 읽고(Nextcloud 클라이언트에서) 및 쓴(Nextcloud 스토리지로) 크기는 %2$s입니다. 보내는 쪽의 네트워크 문제이거나 서버 쪽의 저장소에 쓰는 데 문제가 있을 수 있습니다.",
+ "Could not rename part file to final file, canceled by hook" : "후크에 의해 취소되어 부분 파일의 이름을 최종 파일로 바꿀 수 없음",
+ "Could not rename part file to final file" : "부분 파일의 이름을 최종 파일로 바꿀 수 없음",
+ "Failed to check file size: %1$s" : "파일 크기 확인 실패: %1$s",
+ "Encryption not ready: %1$s" : "암호화가 준비되지 않음: %1$s",
+ "Failed to open file: %1$s" : "파일을 열 수 없음: %1$s",
+ "Failed to unlink: %1$s" : "파일을 삭제할 수 없음: %1$s",
+ "Failed to write file contents: %1$s" : "파일 내용을 쓸 수 없음: %1$s",
+ "File not found: %1$s" : "파일을 찾을 수 없음: %1$s",
+ "System is in maintenance mode." : "시스템이 유지 보수 모드입니다.",
"Upgrade needed" : "업그레이드 필요",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "iOS/macOS에서 CalDAV 및 CardDAV를 사용하려면 %s에서 HTTPS를 사용하도록 설정해야 합니다.",
"Configures a CalDAV account" : "CalDAV 계정 설정",
"Configures a CardDAV account" : "CardDAV 계정 설정",
- "Events" : "이벤트",
- "Tasks" : "작업",
+ "Events" : "일정",
"Untitled task" : "제목없는 작업",
"Completed on %s" : "%s에 완료됨",
- "Due on %s" : "만료일: %s",
+ "Due on %s by %s" : "%s일 %s에 만료됨",
+ "Due on %s" : "%s에 만료됨",
+ "DAV system address book" : "DAV 시스템 주소록",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV 시스템 주소록 동기화가 아직 작동하지 않았습니다. 이는 인스턴스의 사용자가 1000명을 초과하거나 오류가 발생했기 때문입니다. occ dav:sync-system-addressbook 명령어를 통해 수동으로 이를 수행하십시오.",
+ "WebDAV endpoint" : "WebDAV 종단점",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "WebDAV 인터페이스를 사용할 수 없어서 웹 서버에서 파일 동기화를 사용할 수 있도록 설정할 수 없습니다.",
+ "Migrated calendar (%1$s)" : "가져온 달력 (%1$s)",
+ "Calendars including events, details and attendees" : "일정, 세부 정보 및 참석자를 포함한 캘린더",
"Contacts and groups" : "연락처 및 그룹",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV 종단점",
- "to" : "받는 사람",
- "Monday" : "월요일",
- "Tuesday" : "화요일",
- "Wednesday" : "수요일",
- "Thursday" : "목요일",
- "Friday" : "금요일",
- "Saturday" : "토요일",
- "Sunday" : "일요일",
+ "Absence saved" : "부재 상태 저장됨",
+ "Failed to save your absence settings" : "부재 상태 설정 저장 실패",
+ "Absence cleared" : "부재 상태 비워짐",
+ "Failed to clear your absence settings" : "부재 설정 비우기 실패",
+ "First day" : "시작일",
+ "Last day (inclusive)" : "종료일 (이 날짜까지 포함됨)",
+ "Short absence status" : "부재 상태 개요",
+ "Long absence Message" : "부재 상태 상세 설명",
"Save" : "저장",
+ "Disable absence" : "부재 상태 비활성화",
+ "Failed to load availability" : "시간 조율 설정 불러오기 실패",
+ "Saved availability" : "시간 조율 설정 저장함",
+ "Failed to save availability" : "시간 조율 설정 저장 실패",
+ "Time zone:" : "시간대:",
+ "to" : "에서",
+ "Delete slot" : "시간대 삭제",
+ "No working hours set" : "업무 시간이 설정되지 않음",
+ "Add slot" : "시간대 추가",
+ "Pick a start time for {dayName}" : "{dayName} 시작 시각을 지정하십시오",
+ "Pick a end time for {dayName}" : "{dayName} 종료 시각을 지정하십시오",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "다른 용무 중일 때 자동으로 사용자를 '방해 금지' 모드로 설정해 모든 알림을 음소거합니다.",
+ "Cancel" : "취소",
+ "Import" : "가져오기",
+ "Error while saving settings" : "설정 저장 중 오류 발생",
+ "Reset to default" : "기본값으로 초기화",
+ "Availability" : "시간 조율",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "업무 시간을 설정하면, 다른 사람이 회의를 예약할 때 내 부재 중 시간을 확인할 수 있습니다.",
+ "Absence" : "부재",
+ "Configure your next absence period." : "다음 부재 기간을 설정하십시오.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "{calendarappstoreopen}달력 앱{linkclose}을 설치하거나 {calendardocopen}동기화할 데스크톱과 모바일 장치를 연결 ↗{linkclose}하십시오.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "{emailopen}이메일 서버{linkclose}가 올바르게 설치되어 있는지 확인하십시오..",
"Calendar server" : "달력 서버",
"Send invitations to attendees" : "참석자에게 초대장 보내기",
"Automatically generate a birthday calendar" : "자동으로 생일 달력 생성",
"Birthday calendars will be generated by a background job." : "배경 작업으로 생일 달력을 생성합니다.",
"Hence they will not be available immediately after enabling but will show up after some time." : "생일 달력이 생성되는 데 시간이 걸릴 수도 있습니다.",
- "Send notifications for events" : "이벤트에 대한 알림을 전송",
+ "Send notifications for events" : "일정에 대한 알림을 전송",
"Notifications are sent via background jobs, so these must occur often enough." : "알림은 배경 작업을 통해 전송되므로, 충분히 자주 표시됩니다.",
- "Enable notifications for events via push" : "이벤트에 대한 푸시 알림 활성화",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "{calendarappstoreopen}달력 앱{linkclose}을 설치하거나 {calendardocopen}동기화할 데스크톱과 모바일 장치를 연결 ↗{linkclose}하십시오.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "{emailopen}이메일 서버{linkclose}가 올바르게 설치되어 있는지 확인하십시오..",
+ "Send reminder notifications to calendar sharees as well" : "캘린더 공유자에게도 미리 알림 보내기",
+ "Reminders are always sent to organizers and attendees." : "미리 알림은 주최자와 참석자에게 항상 전송됩니다.",
+ "Enable notifications for events via push" : "일정에 대한 푸시 알림 활성화",
"There was an error updating your attendance status." : "참석 상태를 업데이트하는 중 오류가 발생했습니다.",
"Please contact the organizer directly." : "주최자에게 직접 연락하십시오.",
"Are you accepting the invitation?" : "초대를 수락하시겠습니까?",
- "Tentative" : "예정됨",
- "Comment" : "설명",
- "Your attendance was updated successfully." : "참석 정보를 업데이트했습니다.",
- "Calendar and tasks" : "달력과 작업"
+ "Tentative" : "보류",
+ "Your attendance was updated successfully." : "참석 정보를 업데이트했습니다."
},
"nplurals=1; plural=0;");
diff --git a/apps/dav/l10n/ko.json b/apps/dav/l10n/ko.json
index 27e4b160720..50d8fee9c21 100644
--- a/apps/dav/l10n/ko.json
+++ b/apps/dav/l10n/ko.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "달력",
- "Todos" : "할 일",
+ "Tasks" : "작업",
"Personal" : "개인",
"{actor} created calendar {calendar}" : "{actor} 님이 달력 {calendar}을(를) 생성함",
"You created calendar {calendar}" : "달력 {calendar}을(를) 생성함",
@@ -8,7 +8,7 @@
"You deleted calendar {calendar}" : "달력 {calendar}을(를) 삭제함",
"{actor} updated calendar {calendar}" : "{actor} 님이 달력 {calendar}을(를) 업데이트함",
"You updated calendar {calendar}" : "달력 {calendar}을(를) 업데이트함",
- "{actor} restored calendar {calendar}" : "{actor} 님이 달력 {calendar}을(를) 복구함",
+ "{actor} restored calendar {calendar}" : "{actor}님이 달력 {calendar}을(를) 복구함",
"You restored calendar {calendar}" : "달력 {calendar}을(를) 복구함",
"You shared calendar {calendar} as public link" : "달력 {calendar}을(를) 공개 링크로 공유함",
"You removed public link for calendar {calendar}" : "달력 {calendar}의 공개 링크를 삭제함",
@@ -23,126 +23,197 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} 님이 달력 {calendar}을(를) 그룹 {group}와(과) 공유함",
"You unshared calendar {calendar} from group {group}" : "달력 {calendar}을(를) 그룹 {group}와(과) 공유하지 않음",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} 님이 달력 {calendar}을(를) 그룹 {group}와(과) 공유하지 않음",
+ "Untitled event" : "제목 없는 일정",
"{actor} created event {event} in calendar {calendar}" : "{actor} 님이 행사 {event}을(를) 달력 {calendar}에 생성함",
"You created event {event} in calendar {calendar}" : "행사 {event}을(를) 달력 {calendar}에 생성함",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} 님이 행사 {event}을(를) 달력 {calendar}에서 삭제함",
"You deleted event {event} from calendar {calendar}" : "행사 {event}을(를) 달력 {calendar}에서 삭제함",
"{actor} updated event {event} in calendar {calendar}" : "{actor} 님이 달력 {calendar}의 행사 {event}을(를) 업데이트함",
"You updated event {event} in calendar {calendar}" : "달력 {calendar}의 행사 {event}을(를) 업데이트함",
- "{actor} restored event {event} of calendar {calendar}" : "{actor} 님이 행사 {event}을(를) 달력 {calendar}에 복구함",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor}님이 행사 {event}을(를) 달력 {sourceCalendar}에서 달력 {targetCalendar}(으)로 옮김",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "행사 {event}을(를) 달력 {sourceCalendar}에서 달력 {targetCalendar}(으)로 옮김",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor}님이 행사 {event}을(를) 달력 {calendar}에 복구함",
"You restored event {event} of calendar {calendar}" : "행사 {event}을(를) 달력 {calendar}에 복구함",
"Busy" : "바쁨",
- "{actor} created todo {todo} in list {calendar}" : "{actor} 님이 목록 {calendar}에 할 일 {todo}을(를) 생성함",
- "You created todo {todo} in list {calendar}" : "목록 {calendar}에 할 일 {todo}을(를) 생성함",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} 님이 목록 {calendar}에서 할 일 {todo}을(를) 삭제함",
- "You deleted todo {todo} from list {calendar}" : "목록 {calendar}에서 할 일 {todo}을(를) 삭제함",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} 님이 목록 {calendar}의 할 일 {todo}을(를) 업데이트함",
- "You updated todo {todo} in list {calendar}" : "목록 {calendar}의 할 일 {todo}을(를) 업데이트함",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} 님이 목록 {calendar}의 할 일 {todo}을(를) 끝냄",
- "You solved todo {todo} in list {calendar}" : "목록 {calendar}의 할 일 {todo}을(를) 끝냄",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} 님이 목록 {calendar}의 할 일 {todo}을(를) 다시 염",
- "You reopened todo {todo} in list {calendar}" : "목록 {calendar}의 할 일 {todo}을(를) 다시 염",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor}님이 목록 {calendar}에 할 일 {todo}을(를) 생성함",
+ "You created to-do {todo} in list {calendar}" : "목록 {calendar}에 할 일 {todo}을(를) 생성함",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor}님이 목록 {calendar}에서 할 일 {todo}을(를) 삭제함",
+ "You deleted to-do {todo} from list {calendar}" : "목록 {calendar}에서 할 일 {todo}을(를) 삭제함",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor}님이 목록 {calendar}의 할 일 {todo}을(를) 업데이트함",
+ "You updated to-do {todo} in list {calendar}" : "목록 {calendar}의 할 일 {todo}을(를) 업데이트함",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor}님이 목록 {calendar}의 할 일 {todo}을(를) 끝냄",
+ "You solved to-do {todo} in list {calendar}" : "목록 {calendar}의 할 일 {todo}을(를) 끝냄",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor}님이 목록 {calendar}의 할 일 {todo}을(를) 다시 엶",
+ "You reopened to-do {todo} in list {calendar}" : "목록 {calendar}의 할 일 {todo}을(를) 다시 엶",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor}님이 할 일 {todo}을(를) 목록 {sourceCalendar}에서 목록 {targetCalendar}(으)로 옮김",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "할 일 {todo}을(를) 목록 {sourceCalendar}에서 목록 {targetCalendar}(으)로 옮김",
"Calendar, contacts and tasks" : "달력, 연락처 및 작업",
"A <strong>calendar</strong> was modified" : "<strong>달력</strong>이 수정됨",
"A calendar <strong>event</strong> was modified" : "달력 <strong>행사</strong>가 수정됨",
- "A calendar <strong>todo</strong> was modified" : "달력의 <strong>할 일</strong>이 수정됨",
+ "A calendar <strong>to-do</strong> was modified" : "달력 <strong>할 일</strong>이 수정됨",
"Contact birthdays" : "연락처에 등록된 생일",
- "Death of %s" : "%s의 사망",
+ "Death of %s" : "%s의 기일",
+ "Untitled calendar" : "제목없는 달력",
"Calendar:" : "달력:",
"Date:" : "날짜:",
"Where:" : "장소:",
"Description:" : "설명:",
- "Untitled event" : "제목없는 이벤트",
"_%n year_::_%n years_" : ["%n년"],
- "_%n month_::_%n months_" : ["%d개월"],
+ "_%n month_::_%n months_" : ["%n개월"],
"_%n day_::_%n days_" : ["%n일"],
- "_%n hour_::_%n hours_" : ["%d시간"],
- "_%n minute_::_%n minutes_" : ["%d분"],
- "%s (in %s)" : "%s(%s에)",
- "%s (%s ago)" : "%s(%s 전)",
+ "_%n hour_::_%n hours_" : ["%n시간"],
+ "_%n minute_::_%n minutes_" : ["%n분"],
+ "%s (in %s)" : "%s(%s 후)",
+ "%s (%s ago)" : "%s(%s 지남)",
"Calendar: %s" : "달력: %s",
"Date: %s" : "날짜: %s",
"Description: %s" : "설명: %s",
"Where: %s" : "장소: %s",
"%1$s via %2$s" : "%1$s(%2$s 경유)",
"Cancelled: %1$s" : "취소됨: %1$s",
- "Invitation canceled" : "초대장 취소됨",
+ "\"%1$s\" has been canceled" : "\"%1$s\"이(가) 취소되었습니다",
"Re: %1$s" : "Re: %1$s",
- "Invitation updated" : "초대장 업데이트됨",
+ "%1$s has accepted your invitation" : "%1$s이(가) 초대를 수락했습니다",
+ "%1$s has tentatively accepted your invitation" : "%1$s이(기) 초대를 잠정 수락했습니다",
+ "%1$s has declined your invitation" : "%1$s이(가) 초대를 거절했습니다",
+ "%1$s has responded to your invitation" : "%1$s이(가) 초대에 응답했습니다",
+ "Invitation updated: %1$s" : "초대 갱신됨: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s님이 일정 \"%2$s\"을(를) 갱신했습니다",
"Invitation: %1$s" : "초대: %1$s",
- "Invitation" : "초대",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s님이 나를 \"%2$s\"에 초대했습니다",
+ "Organizer:" : "주최자:",
+ "Attendees:" : "참석자:",
"Title:" : "제목:",
- "Time:" : "시간:",
+ "When:" : "일시:",
"Location:" : "위치:",
"Link:" : "링크:",
- "Organizer:" : "주최자:",
- "Attendees:" : "참석자:",
"Accept" : "수락",
"Decline" : "거절",
"More options …" : "더 많은 옵션 …",
"More options at %s" : "%s에 더 많은 옵션 있음",
+ "Monday" : "월요일",
+ "Tuesday" : "화요일",
+ "Wednesday" : "수요일",
+ "Thursday" : "목요일",
+ "Friday" : "금요일",
+ "Saturday" : "토요일",
+ "Sunday" : "일요일",
+ "January" : "1월",
+ "February" : "2월",
+ "March" : "3월",
+ "April" : "4월",
+ "May" : "5월",
+ "June" : "6월",
+ "July" : "7월",
+ "August" : "8월",
+ "September" : "9월",
+ "October" : "10월",
+ "November" : "11월",
+ "December" : "12월",
"Contacts" : "연락처",
- "{actor} created address book {addressbook}" : "{actor} 님이 주소록 {addressbook}을(를) 생성함",
+ "{actor} created address book {addressbook}" : "{actor}님이 주소록 {addressbook}을(를) 생성함",
"You created address book {addressbook}" : "주소록 {addressbook}을(를) 생성함",
- "{actor} deleted address book {addressbook}" : "{actor} 님이 주소록 {addressbook}을(를) 제거함",
+ "{actor} deleted address book {addressbook}" : "{actor}님이 주소록 {addressbook}을(를) 제거함",
"You deleted address book {addressbook}" : "주소록 {addressbook}을(를) 제거함",
- "{actor} updated address book {addressbook}" : "{actor} 님이 주소록 {addressbook}을(를) 갱신함",
+ "{actor} updated address book {addressbook}" : "{actor}님이 주소록 {addressbook}을(를) 갱신함",
"You updated address book {addressbook}" : "주소록 {addressbook}을(를) 갱신함",
- "{actor} shared address book {addressbook} with you" : "{actor} 님이 나와 주소록 {addressbook}을(를) 공유함",
- "You shared address book {addressbook} with {user}" : "{user} 님과 주소록 {addressbook}을(를) 공유함",
- "{actor} shared address book {addressbook} with {user}" : "{actor} 님이 {user} 님과 주소록 {addressbook}을(를) 공유함",
- "{actor} unshared address book {addressbook} from you" : "{actor} 님이 주소록 {addressbook}을(를) 더이상 공유하지 않음",
- "You unshared address book {addressbook} from {user}" : "{user} 님과 주소록 {addressbook}을(를) 더이상 공유하지 않음",
- "{actor} unshared address book {addressbook} from {user}" : "{actor} 님이 {user} 님과 주소록 {addressbook}을(를) 더이상 공유하지 않음",
+ "{actor} shared address book {addressbook} with you" : "{actor}님이 나와 주소록 {addressbook}을(를) 공유함",
+ "You shared address book {addressbook} with {user}" : "{user}님과 주소록 {addressbook}을(를) 공유함",
+ "{actor} shared address book {addressbook} with {user}" : "{actor}님이 {user}님과 주소록 {addressbook}을(를) 공유함",
+ "{actor} unshared address book {addressbook} from you" : "{actor}님이 주소록 {addressbook}의 공유를 해제함",
+ "You unshared address book {addressbook} from {user}" : "{user}님과 주소록 {addressbook}의 공유를 해제함",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor}님이 {user}님과 주소록 {addressbook}의 공유를 해제함",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor}님이 자신과 주소록 {addressbook}의 공유를 해제함",
"You shared address book {addressbook} with group {group}" : "그룹 {group}과(와) 주소록 {addressbook}을(를) 공유함",
- "{actor} shared address book {addressbook} with group {group}" : "{actor} 님이 그룹 {group}과(와) 주소록 {addressbook}을(를) 공유함",
- "You unshared address book {addressbook} from group {group}" : "그룹 {group}과(와) 주소록 {addressbook}을(를) 더이상 공유하지 않음",
- "{actor} unshared address book {addressbook} from group {group}" : "{actor} 님이 그룹 {group}과(와) 주소록 {addressbook}을(를) 더이상 공유하지 않음",
- "{actor} created contact {card} in address book {addressbook}" : "{actor} 님이 연락처 {card}을(를) 주소록 {addressbook}에 생성함",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor}님이 그룹 {group}과(와) 주소록 {addressbook}을(를) 공유함",
+ "You unshared address book {addressbook} from group {group}" : "그룹 {group}과(와) 주소록 {addressbook}의 공유를 해제함",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor}님이 그룹 {group}과(와) 주소록 {addressbook}의 공유를 해제함",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor}님이 연락처 {card}을(를) 주소록 {addressbook}에 생성함",
"You created contact {card} in address book {addressbook}" : "연락처 {card}을(를) 주소록 {addressbook}에 생성함",
- "{actor} deleted contact {card} from address book {addressbook}" : "{actor} 님이 연락처 {card}을(를) 주소록 {addressbook}에서 제거함",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor}님이 연락처 {card}을(를) 주소록 {addressbook}에서 제거함",
"You deleted contact {card} from address book {addressbook}" : "연락처 {card}을(를) 주소록 {addressbook}에서 제거함",
- "{actor} updated contact {card} in address book {addressbook}" : "{actor} 님이 주소록 {addressbook}의 연락처 {card}을(를) 갱신함",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor}님이 주소록 {addressbook}의 연락처 {card}을(를) 갱신함",
"You updated contact {card} in address book {addressbook}" : "주소록 {addressbook}의 연락처 {card}을(를) 갱신함",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>연락처</strong> 또는 <strong>주소록</strong>이 변경됨",
- "System is in maintenance mode." : "시스템이 유지 관리 모드입니다.",
+ "Accounts" : "계정",
+ "System address book which holds all accounts" : "시스템 주소록이 모든 계정 정보를 보유합니다",
+ "File is not updatable: %1$s" : "파일을 갱신할 수 없습니다: %1$s",
+ "Could not write to final file, canceled by hook" : "후크에 의해 취소되어 최종 파일에 쓸 수 없음",
+ "Could not write file contents" : "파일 내용을 쓸 수 없음",
+ "_%n byte_::_%n bytes_" : ["%n 바이트"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "파일을 대상 위치로 복사하는 동안 오류 발생 (복사됨: %1$s, 예상 파일 크기: %2$s)",
+ "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." : "예상 파일 크기는 %1$s이지만 읽고(Nextcloud 클라이언트에서) 및 쓴(Nextcloud 스토리지로) 크기는 %2$s입니다. 보내는 쪽의 네트워크 문제이거나 서버 쪽의 저장소에 쓰는 데 문제가 있을 수 있습니다.",
+ "Could not rename part file to final file, canceled by hook" : "후크에 의해 취소되어 부분 파일의 이름을 최종 파일로 바꿀 수 없음",
+ "Could not rename part file to final file" : "부분 파일의 이름을 최종 파일로 바꿀 수 없음",
+ "Failed to check file size: %1$s" : "파일 크기 확인 실패: %1$s",
+ "Encryption not ready: %1$s" : "암호화가 준비되지 않음: %1$s",
+ "Failed to open file: %1$s" : "파일을 열 수 없음: %1$s",
+ "Failed to unlink: %1$s" : "파일을 삭제할 수 없음: %1$s",
+ "Failed to write file contents: %1$s" : "파일 내용을 쓸 수 없음: %1$s",
+ "File not found: %1$s" : "파일을 찾을 수 없음: %1$s",
+ "System is in maintenance mode." : "시스템이 유지 보수 모드입니다.",
"Upgrade needed" : "업그레이드 필요",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "iOS/macOS에서 CalDAV 및 CardDAV를 사용하려면 %s에서 HTTPS를 사용하도록 설정해야 합니다.",
"Configures a CalDAV account" : "CalDAV 계정 설정",
"Configures a CardDAV account" : "CardDAV 계정 설정",
- "Events" : "이벤트",
- "Tasks" : "작업",
+ "Events" : "일정",
"Untitled task" : "제목없는 작업",
"Completed on %s" : "%s에 완료됨",
- "Due on %s" : "만료일: %s",
+ "Due on %s by %s" : "%s일 %s에 만료됨",
+ "Due on %s" : "%s에 만료됨",
+ "DAV system address book" : "DAV 시스템 주소록",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV 시스템 주소록 동기화가 아직 작동하지 않았습니다. 이는 인스턴스의 사용자가 1000명을 초과하거나 오류가 발생했기 때문입니다. occ dav:sync-system-addressbook 명령어를 통해 수동으로 이를 수행하십시오.",
+ "WebDAV endpoint" : "WebDAV 종단점",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "WebDAV 인터페이스를 사용할 수 없어서 웹 서버에서 파일 동기화를 사용할 수 있도록 설정할 수 없습니다.",
+ "Migrated calendar (%1$s)" : "가져온 달력 (%1$s)",
+ "Calendars including events, details and attendees" : "일정, 세부 정보 및 참석자를 포함한 캘린더",
"Contacts and groups" : "연락처 및 그룹",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV 종단점",
- "to" : "받는 사람",
- "Monday" : "월요일",
- "Tuesday" : "화요일",
- "Wednesday" : "수요일",
- "Thursday" : "목요일",
- "Friday" : "금요일",
- "Saturday" : "토요일",
- "Sunday" : "일요일",
+ "Absence saved" : "부재 상태 저장됨",
+ "Failed to save your absence settings" : "부재 상태 설정 저장 실패",
+ "Absence cleared" : "부재 상태 비워짐",
+ "Failed to clear your absence settings" : "부재 설정 비우기 실패",
+ "First day" : "시작일",
+ "Last day (inclusive)" : "종료일 (이 날짜까지 포함됨)",
+ "Short absence status" : "부재 상태 개요",
+ "Long absence Message" : "부재 상태 상세 설명",
"Save" : "저장",
+ "Disable absence" : "부재 상태 비활성화",
+ "Failed to load availability" : "시간 조율 설정 불러오기 실패",
+ "Saved availability" : "시간 조율 설정 저장함",
+ "Failed to save availability" : "시간 조율 설정 저장 실패",
+ "Time zone:" : "시간대:",
+ "to" : "에서",
+ "Delete slot" : "시간대 삭제",
+ "No working hours set" : "업무 시간이 설정되지 않음",
+ "Add slot" : "시간대 추가",
+ "Pick a start time for {dayName}" : "{dayName} 시작 시각을 지정하십시오",
+ "Pick a end time for {dayName}" : "{dayName} 종료 시각을 지정하십시오",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "다른 용무 중일 때 자동으로 사용자를 '방해 금지' 모드로 설정해 모든 알림을 음소거합니다.",
+ "Cancel" : "취소",
+ "Import" : "가져오기",
+ "Error while saving settings" : "설정 저장 중 오류 발생",
+ "Reset to default" : "기본값으로 초기화",
+ "Availability" : "시간 조율",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "업무 시간을 설정하면, 다른 사람이 회의를 예약할 때 내 부재 중 시간을 확인할 수 있습니다.",
+ "Absence" : "부재",
+ "Configure your next absence period." : "다음 부재 기간을 설정하십시오.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "{calendarappstoreopen}달력 앱{linkclose}을 설치하거나 {calendardocopen}동기화할 데스크톱과 모바일 장치를 연결 ↗{linkclose}하십시오.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "{emailopen}이메일 서버{linkclose}가 올바르게 설치되어 있는지 확인하십시오..",
"Calendar server" : "달력 서버",
"Send invitations to attendees" : "참석자에게 초대장 보내기",
"Automatically generate a birthday calendar" : "자동으로 생일 달력 생성",
"Birthday calendars will be generated by a background job." : "배경 작업으로 생일 달력을 생성합니다.",
"Hence they will not be available immediately after enabling but will show up after some time." : "생일 달력이 생성되는 데 시간이 걸릴 수도 있습니다.",
- "Send notifications for events" : "이벤트에 대한 알림을 전송",
+ "Send notifications for events" : "일정에 대한 알림을 전송",
"Notifications are sent via background jobs, so these must occur often enough." : "알림은 배경 작업을 통해 전송되므로, 충분히 자주 표시됩니다.",
- "Enable notifications for events via push" : "이벤트에 대한 푸시 알림 활성화",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "{calendarappstoreopen}달력 앱{linkclose}을 설치하거나 {calendardocopen}동기화할 데스크톱과 모바일 장치를 연결 ↗{linkclose}하십시오.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "{emailopen}이메일 서버{linkclose}가 올바르게 설치되어 있는지 확인하십시오..",
+ "Send reminder notifications to calendar sharees as well" : "캘린더 공유자에게도 미리 알림 보내기",
+ "Reminders are always sent to organizers and attendees." : "미리 알림은 주최자와 참석자에게 항상 전송됩니다.",
+ "Enable notifications for events via push" : "일정에 대한 푸시 알림 활성화",
"There was an error updating your attendance status." : "참석 상태를 업데이트하는 중 오류가 발생했습니다.",
"Please contact the organizer directly." : "주최자에게 직접 연락하십시오.",
"Are you accepting the invitation?" : "초대를 수락하시겠습니까?",
- "Tentative" : "예정됨",
- "Comment" : "설명",
- "Your attendance was updated successfully." : "참석 정보를 업데이트했습니다.",
- "Calendar and tasks" : "달력과 작업"
+ "Tentative" : "보류",
+ "Your attendance was updated successfully." : "참석 정보를 업데이트했습니다."
},"pluralForm" :"nplurals=1; plural=0;"
} \ No newline at end of file
diff --git a/apps/dav/l10n/lt_LT.js b/apps/dav/l10n/lt_LT.js
deleted file mode 100644
index 23bb074c164..00000000000
--- a/apps/dav/l10n/lt_LT.js
+++ /dev/null
@@ -1,127 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Kalendorius",
- "Todos" : "Užduotys",
- "Personal" : "Asmeniniai",
- "{actor} created calendar {calendar}" : "{actor} sukūrė kalendorių {calendar}",
- "You created calendar {calendar}" : "Jūs sukūrėte kalendorių {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} ištrynė kalendorių {calendar}",
- "You deleted calendar {calendar}" : "Jūs ištrynėte kalendorių {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} atnaujino kalendorių {calendar}",
- "You updated calendar {calendar}" : "Jūs atnaujinote kalendorių {calendar}",
- "{actor} restored calendar {calendar}" : "{actor} atkūrė kalendorių {calendar}",
- "You restored calendar {calendar}" : "Jūs atkūrėte kalendorių {calendar}",
- "You shared calendar {calendar} as public link" : "Jūs pradėjote bendrinti kalendorių {calendar} kaip viešąją nuorodą",
- "You removed public link for calendar {calendar}" : "Jūs pašalinote viešąją nuorodą kalendoriui {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} pradėjo bendrinti su jumis kalendorių {calendar}",
- "You shared calendar {calendar} with {user}" : "Jūs pradėjote bendrinti kalendorių {calendar} su {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} pradėjo bendrinti kalendorių {calendar} su {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} nustojo bendrinti su jumis kalendorių {calendar}",
- "You unshared calendar {calendar} from {user}" : "Jūs nustojote bendrinti kalendorių {calendar} su {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} nustojo bendrinti kalendorių {calendar} su {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} nustojo bendrinti su savimi kalendorių {calendar}",
- "You shared calendar {calendar} with group {group}" : "Jūs pradėjote bendrinti kalendorių {calendar} su grupe {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} pradėjo bendrinti kalendorių {calendar} su grupe {group}",
- "You unshared calendar {calendar} from group {group}" : "Jūs nustojote bendrinti kalendorių {calendar} su grupe {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} nustojo bendrinti kalendorių {calendar} su grupe {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} kalendoriuje {calendar} sukūrė įvykį {event}",
- "You created event {event} in calendar {calendar}" : "Jūs kalendoriuje {calendar} sukūrėte įvykį {event}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} ištrynė įvykį {event} iš kalendoriaus {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Jūs ištrynėte įvykį {event} iš kalendoriaus {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} kalendoriuje {calendar} atnaujino įvykį {event}",
- "You updated event {event} in calendar {calendar}" : "Jūs kalendoriuje {calendar} atnaujinote įvykį {event}",
- "{actor} restored event {event} of calendar {calendar}" : "{actor} kalendoriuje {calendar} atkūrė įvykį {event}",
- "You restored event {event} of calendar {calendar}" : "Jūs kalendoriuje {calendar} atkūrėte įvykį {event}",
- "Busy" : "Užimtas laikas",
- "{actor} created todo {todo} in list {calendar}" : "{actor} sąraše {calendar} sukūrė užduotį {todo}",
- "You created todo {todo} in list {calendar}" : "Jūs sąraše {calendar} sukūrėte užduotį {todo}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} iš sąrašo {calendar} ištrynė užduotį {todo}",
- "You deleted todo {todo} from list {calendar}" : "Jūs iš sąrašo {calendar} ištrynėte užduotį {todo}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} sąraše {calendar} atnaujino užduotį {todo}",
- "You updated todo {todo} in list {calendar}" : "Jūs sąraše {calendar} atnaujinote užduotį {todo}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} sąraše {calendar} išsprendė užduotį {todo}",
- "You solved todo {todo} in list {calendar}" : "Jūs sąraše {calendar} išsprendėte užduotį {todo}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} sąraše {calendar} vėl pradėjo užduotį {todo}",
- "You reopened todo {todo} in list {calendar}" : "Jūs sąraše {calendar} vėl pradėjote užduotį {todo}",
- "Calendar, contacts and tasks" : "Kalendorius, adresatai ir užduotys",
- "A <strong>calendar</strong> was modified" : "<strong>Kalendorius</strong> buvo modifikuotas",
- "A calendar <strong>event</strong> was modified" : "Kalendoriaus <strong>įvykis</strong> buvo modifikuotas",
- "A calendar <strong>todo</strong> was modified" : "Kalendoriaus <strong>užduotis</strong> buvo modifikuota",
- "Contact birthdays" : "Adresatų gimtadieniai",
- "Calendar:" : "Kalendorius:",
- "Date:" : "Data:",
- "Where:" : "Kur:",
- "Description:" : "Aprašas:",
- "Untitled event" : "Įvykis be pavadinimo",
- "_%n year_::_%n years_" : ["%n metai","%n metai","%n metų","%n metai"],
- "_%n month_::_%n months_" : ["%n mėnesis","%n mėnesiai","%n mėnesių","%n mėnesis"],
- "_%n day_::_%n days_" : ["%n diena","%n dienos","%n dienų","%n diena"],
- "_%n hour_::_%n hours_" : ["%n valanda","%n valandos","%n valandų","%n valanda"],
- "_%n minute_::_%n minutes_" : ["%n minutė","%n minutės","%n minučių","%n minutė"],
- "%s (in %s)" : "%s (po %s)",
- "%s (%s ago)" : "%s (prieš %s)",
- "Calendar: %s" : "Kalendorius: %s",
- "Date: %s" : "Data: %s",
- "Description: %s" : "Aprašas: %s",
- "Where: %s" : "Kur: %s",
- "%1$s via %2$s" : "%1$s per %2$s",
- "Invitation canceled" : "Pakvietimo atsisakyta",
- "Invitation updated" : "Pakvietimas atnaujintas",
- "Invitation: %1$s" : "Pakvietimas: %1$s",
- "Invitation" : "Pakvietimas",
- "Title:" : "Pavadinimas:",
- "Time:" : "Laikas:",
- "Location:" : "Vieta:",
- "Link:" : "Nuoroda:",
- "Organizer:" : "Organizatorius:",
- "Attendees:" : "Kviestiniai:",
- "Accept" : "Priimti",
- "Decline" : "Atmesti",
- "More options …" : "Daugiau parinkčių…",
- "Contacts" : "Adresatai",
- "{actor} created address book {addressbook}" : "{actor} sukūrė adresų knygą {addressbook}",
- "You created address book {addressbook}" : "Jūs sukūrėte adresų knygą {addressbook}",
- "{actor} deleted address book {addressbook}" : "{actor} ištrynė adresų knygą {addressbook}",
- "You deleted address book {addressbook}" : "Jūs ištrynėte adresų knygą {addressbook}",
- "{actor} updated address book {addressbook}" : "{actor} atnaujino adresų knygą {addressbook}",
- "You updated address book {addressbook}" : "Jūs atnaujinote adresų knygą {addressbook}",
- "System is in maintenance mode." : "Sistema yra techninės priežiūros veiksenoje.",
- "Upgrade needed" : "Reikalingas naujinimas",
- "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Norint naudoti CalDAV ir CardDAV su iOS/macOS, jūsų %s turi būti sukonfigūruota taip, kad naudotų HTTPS.",
- "Configures a CalDAV account" : "Konfigūruoja CalDAV paskyrą",
- "Configures a CardDAV account" : "Konfigūruoja CardDAV paskyrą",
- "Events" : "Įvykiai",
- "Tasks" : "Užduotys",
- "Untitled task" : "Užduotis be pavadinimo",
- "Contacts and groups" : "Adresatai ir grupės",
- "WebDAV" : "WebDAV",
- "Availability" : "Pasiekiamumas",
- "Time zone:" : "Laiko juosta:",
- "to" : "iki",
- "No working hours set" : "Nenustatyta jokių darbo valandų",
- "Monday" : "Pirmadienis",
- "Tuesday" : "Antradienis",
- "Wednesday" : "Trečiadienis",
- "Thursday" : "Ketvirtadienis",
- "Friday" : "Penktadienis",
- "Saturday" : "Šeštadienis",
- "Sunday" : "Sekmadienis",
- "Save" : "Įrašyti",
- "Calendar server" : "Kalendoriaus serveris",
- "Send invitations to attendees" : "Siųsti pakvietimus kviestiniams",
- "Automatically generate a birthday calendar" : "Automatiškai sukurti gimtadienių kalendorių",
- "Birthday calendars will be generated by a background job." : "Gimtadienių kalendoriai bus sukurti foninės užduoties.",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Tai reiškia, kad jie nebus matomi iš karto įjungus, o pasirodys tik po kurio laiko.",
- "Send notifications for events" : "Siųsti įvykių pranešimus",
- "Notifications are sent via background jobs, so these must occur often enough." : "Pranešimai yra siunčiami per fonines užduotis, todėl jos privalo būti vykdomos gana dažnai.",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Taip pat įsidiekite {calendarappstoreopen}Kalendoriaus programėlę{linkclose} arba sinchronizavimui {calendardocopen}prijunkite savo darbalaukį ir mobilųjį ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Įsitikinkite, kad tinkamai nusistatėte {emailopen}el. pašto serverį{linkclose}.",
- "Please contact the organizer directly." : "Prašome susisiekti su organizatoriumi tiesiogiai.",
- "Are you accepting the invitation?" : "Ar priimate pakvietimą?",
- "Tentative" : "Preliminarus",
- "Number of guests" : "Svečių skaičius",
- "Comment" : "Komentaras",
- "Calendar and tasks" : "Kalendorius ir užduotys"
-},
-"nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);");
diff --git a/apps/dav/l10n/lt_LT.json b/apps/dav/l10n/lt_LT.json
deleted file mode 100644
index 7fa6e34d138..00000000000
--- a/apps/dav/l10n/lt_LT.json
+++ /dev/null
@@ -1,125 +0,0 @@
-{ "translations": {
- "Calendar" : "Kalendorius",
- "Todos" : "Užduotys",
- "Personal" : "Asmeniniai",
- "{actor} created calendar {calendar}" : "{actor} sukūrė kalendorių {calendar}",
- "You created calendar {calendar}" : "Jūs sukūrėte kalendorių {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} ištrynė kalendorių {calendar}",
- "You deleted calendar {calendar}" : "Jūs ištrynėte kalendorių {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} atnaujino kalendorių {calendar}",
- "You updated calendar {calendar}" : "Jūs atnaujinote kalendorių {calendar}",
- "{actor} restored calendar {calendar}" : "{actor} atkūrė kalendorių {calendar}",
- "You restored calendar {calendar}" : "Jūs atkūrėte kalendorių {calendar}",
- "You shared calendar {calendar} as public link" : "Jūs pradėjote bendrinti kalendorių {calendar} kaip viešąją nuorodą",
- "You removed public link for calendar {calendar}" : "Jūs pašalinote viešąją nuorodą kalendoriui {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} pradėjo bendrinti su jumis kalendorių {calendar}",
- "You shared calendar {calendar} with {user}" : "Jūs pradėjote bendrinti kalendorių {calendar} su {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} pradėjo bendrinti kalendorių {calendar} su {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} nustojo bendrinti su jumis kalendorių {calendar}",
- "You unshared calendar {calendar} from {user}" : "Jūs nustojote bendrinti kalendorių {calendar} su {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} nustojo bendrinti kalendorių {calendar} su {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} nustojo bendrinti su savimi kalendorių {calendar}",
- "You shared calendar {calendar} with group {group}" : "Jūs pradėjote bendrinti kalendorių {calendar} su grupe {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} pradėjo bendrinti kalendorių {calendar} su grupe {group}",
- "You unshared calendar {calendar} from group {group}" : "Jūs nustojote bendrinti kalendorių {calendar} su grupe {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} nustojo bendrinti kalendorių {calendar} su grupe {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} kalendoriuje {calendar} sukūrė įvykį {event}",
- "You created event {event} in calendar {calendar}" : "Jūs kalendoriuje {calendar} sukūrėte įvykį {event}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} ištrynė įvykį {event} iš kalendoriaus {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Jūs ištrynėte įvykį {event} iš kalendoriaus {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} kalendoriuje {calendar} atnaujino įvykį {event}",
- "You updated event {event} in calendar {calendar}" : "Jūs kalendoriuje {calendar} atnaujinote įvykį {event}",
- "{actor} restored event {event} of calendar {calendar}" : "{actor} kalendoriuje {calendar} atkūrė įvykį {event}",
- "You restored event {event} of calendar {calendar}" : "Jūs kalendoriuje {calendar} atkūrėte įvykį {event}",
- "Busy" : "Užimtas laikas",
- "{actor} created todo {todo} in list {calendar}" : "{actor} sąraše {calendar} sukūrė užduotį {todo}",
- "You created todo {todo} in list {calendar}" : "Jūs sąraše {calendar} sukūrėte užduotį {todo}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} iš sąrašo {calendar} ištrynė užduotį {todo}",
- "You deleted todo {todo} from list {calendar}" : "Jūs iš sąrašo {calendar} ištrynėte užduotį {todo}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} sąraše {calendar} atnaujino užduotį {todo}",
- "You updated todo {todo} in list {calendar}" : "Jūs sąraše {calendar} atnaujinote užduotį {todo}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} sąraše {calendar} išsprendė užduotį {todo}",
- "You solved todo {todo} in list {calendar}" : "Jūs sąraše {calendar} išsprendėte užduotį {todo}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} sąraše {calendar} vėl pradėjo užduotį {todo}",
- "You reopened todo {todo} in list {calendar}" : "Jūs sąraše {calendar} vėl pradėjote užduotį {todo}",
- "Calendar, contacts and tasks" : "Kalendorius, adresatai ir užduotys",
- "A <strong>calendar</strong> was modified" : "<strong>Kalendorius</strong> buvo modifikuotas",
- "A calendar <strong>event</strong> was modified" : "Kalendoriaus <strong>įvykis</strong> buvo modifikuotas",
- "A calendar <strong>todo</strong> was modified" : "Kalendoriaus <strong>užduotis</strong> buvo modifikuota",
- "Contact birthdays" : "Adresatų gimtadieniai",
- "Calendar:" : "Kalendorius:",
- "Date:" : "Data:",
- "Where:" : "Kur:",
- "Description:" : "Aprašas:",
- "Untitled event" : "Įvykis be pavadinimo",
- "_%n year_::_%n years_" : ["%n metai","%n metai","%n metų","%n metai"],
- "_%n month_::_%n months_" : ["%n mėnesis","%n mėnesiai","%n mėnesių","%n mėnesis"],
- "_%n day_::_%n days_" : ["%n diena","%n dienos","%n dienų","%n diena"],
- "_%n hour_::_%n hours_" : ["%n valanda","%n valandos","%n valandų","%n valanda"],
- "_%n minute_::_%n minutes_" : ["%n minutė","%n minutės","%n minučių","%n minutė"],
- "%s (in %s)" : "%s (po %s)",
- "%s (%s ago)" : "%s (prieš %s)",
- "Calendar: %s" : "Kalendorius: %s",
- "Date: %s" : "Data: %s",
- "Description: %s" : "Aprašas: %s",
- "Where: %s" : "Kur: %s",
- "%1$s via %2$s" : "%1$s per %2$s",
- "Invitation canceled" : "Pakvietimo atsisakyta",
- "Invitation updated" : "Pakvietimas atnaujintas",
- "Invitation: %1$s" : "Pakvietimas: %1$s",
- "Invitation" : "Pakvietimas",
- "Title:" : "Pavadinimas:",
- "Time:" : "Laikas:",
- "Location:" : "Vieta:",
- "Link:" : "Nuoroda:",
- "Organizer:" : "Organizatorius:",
- "Attendees:" : "Kviestiniai:",
- "Accept" : "Priimti",
- "Decline" : "Atmesti",
- "More options …" : "Daugiau parinkčių…",
- "Contacts" : "Adresatai",
- "{actor} created address book {addressbook}" : "{actor} sukūrė adresų knygą {addressbook}",
- "You created address book {addressbook}" : "Jūs sukūrėte adresų knygą {addressbook}",
- "{actor} deleted address book {addressbook}" : "{actor} ištrynė adresų knygą {addressbook}",
- "You deleted address book {addressbook}" : "Jūs ištrynėte adresų knygą {addressbook}",
- "{actor} updated address book {addressbook}" : "{actor} atnaujino adresų knygą {addressbook}",
- "You updated address book {addressbook}" : "Jūs atnaujinote adresų knygą {addressbook}",
- "System is in maintenance mode." : "Sistema yra techninės priežiūros veiksenoje.",
- "Upgrade needed" : "Reikalingas naujinimas",
- "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Norint naudoti CalDAV ir CardDAV su iOS/macOS, jūsų %s turi būti sukonfigūruota taip, kad naudotų HTTPS.",
- "Configures a CalDAV account" : "Konfigūruoja CalDAV paskyrą",
- "Configures a CardDAV account" : "Konfigūruoja CardDAV paskyrą",
- "Events" : "Įvykiai",
- "Tasks" : "Užduotys",
- "Untitled task" : "Užduotis be pavadinimo",
- "Contacts and groups" : "Adresatai ir grupės",
- "WebDAV" : "WebDAV",
- "Availability" : "Pasiekiamumas",
- "Time zone:" : "Laiko juosta:",
- "to" : "iki",
- "No working hours set" : "Nenustatyta jokių darbo valandų",
- "Monday" : "Pirmadienis",
- "Tuesday" : "Antradienis",
- "Wednesday" : "Trečiadienis",
- "Thursday" : "Ketvirtadienis",
- "Friday" : "Penktadienis",
- "Saturday" : "Šeštadienis",
- "Sunday" : "Sekmadienis",
- "Save" : "Įrašyti",
- "Calendar server" : "Kalendoriaus serveris",
- "Send invitations to attendees" : "Siųsti pakvietimus kviestiniams",
- "Automatically generate a birthday calendar" : "Automatiškai sukurti gimtadienių kalendorių",
- "Birthday calendars will be generated by a background job." : "Gimtadienių kalendoriai bus sukurti foninės užduoties.",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Tai reiškia, kad jie nebus matomi iš karto įjungus, o pasirodys tik po kurio laiko.",
- "Send notifications for events" : "Siųsti įvykių pranešimus",
- "Notifications are sent via background jobs, so these must occur often enough." : "Pranešimai yra siunčiami per fonines užduotis, todėl jos privalo būti vykdomos gana dažnai.",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Taip pat įsidiekite {calendarappstoreopen}Kalendoriaus programėlę{linkclose} arba sinchronizavimui {calendardocopen}prijunkite savo darbalaukį ir mobilųjį ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Įsitikinkite, kad tinkamai nusistatėte {emailopen}el. pašto serverį{linkclose}.",
- "Please contact the organizer directly." : "Prašome susisiekti su organizatoriumi tiesiogiai.",
- "Are you accepting the invitation?" : "Ar priimate pakvietimą?",
- "Tentative" : "Preliminarus",
- "Number of guests" : "Svečių skaičius",
- "Comment" : "Komentaras",
- "Calendar and tasks" : "Kalendorius ir užduotys"
-},"pluralForm" :"nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/lv.js b/apps/dav/l10n/lv.js
deleted file mode 100644
index 0a8e585fbd1..00000000000
--- a/apps/dav/l10n/lv.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Contact birthdays" : "Kontaktu dzimšanas dienas",
- "Personal" : "Personīgi",
- "Contacts" : "Kontakti",
- "Technical details" : "Tehniskās detaļas",
- "Remote Address: %s" : "Attālinātā adrese: %s",
- "Request ID: %s" : "Pieprasījuma ID: %s"
-},
-"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);");
diff --git a/apps/dav/l10n/lv.json b/apps/dav/l10n/lv.json
deleted file mode 100644
index c28aad665dd..00000000000
--- a/apps/dav/l10n/lv.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Contact birthdays" : "Kontaktu dzimšanas dienas",
- "Personal" : "Personīgi",
- "Contacts" : "Kontakti",
- "Technical details" : "Tehniskās detaļas",
- "Remote Address: %s" : "Attālinātā adrese: %s",
- "Request ID: %s" : "Pieprasījuma ID: %s"
-},"pluralForm" :"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/mk.js b/apps/dav/l10n/mk.js
index 61ee84170bc..aee9a41896f 100644
--- a/apps/dav/l10n/mk.js
+++ b/apps/dav/l10n/mk.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Календар",
- "Todos" : "Задачи",
+ "Tasks" : "Задачи",
"Personal" : "Лично",
"{actor} created calendar {calendar}" : "{actor} креираше календар {calendar}",
"You created calendar {calendar}" : "Креиравте календар {calendar}",
@@ -10,6 +10,8 @@ OC.L10N.register(
"You deleted calendar {calendar}" : "Избришавте календар {calendar}",
"{actor} updated calendar {calendar}" : "{actor} ажурираше календар {calendar}",
"You updated calendar {calendar}" : "Ажуриравте календар {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} врати календар {calendar}",
+ "You restored calendar {calendar}" : "Вративте календар {calendar}",
"You shared calendar {calendar} as public link" : "Споделивте календар {calendar} како јавен линк",
"You removed public link for calendar {calendar}" : "Отстранивте јавен линк за календар {calendar}",
"{actor} shared calendar {calendar} with you" : "{actor} сподели календар {calendar} со вас",
@@ -23,38 +25,43 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} сподели календар {calendar} со група {group}",
"You unshared calendar {calendar} from group {group}" : "Отстранивте споделување на календар {calendar} од група {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} отстрани споделување на календар {calendar} од група {group}",
+ "Untitled event" : "Неименуван настан",
"{actor} created event {event} in calendar {calendar}" : "{actor} креираше настан {event} во календар {calendar}",
"You created event {event} in calendar {calendar}" : "Креиравте настан {event} во календар {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} избриша настан {event} од календар {calendar}",
"You deleted event {event} from calendar {calendar}" : "Избришавте настан {event} од календар {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} ажурираше настан {event} во календар {calendar}",
"You updated event {event} in calendar {calendar}" : "Ажуриравте настан {event} во календар {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} премести настан {event} од календар {sourceCalendar} во календар {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Преместивте настан {event} од календар {sourceCalendar} во календар {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} врати настан {event} во календарот {calendar}",
"You restored event {event} of calendar {calendar}" : "Вративте настан {event} во календарот {calendar}",
"Busy" : "Зафатен",
- "{actor} created todo {todo} in list {calendar}" : "{actor} креираше задолжение {todo} во листата {calendar}",
- "You created todo {todo} in list {calendar}" : "Креиравте задолжение {todo} во листата {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} избриша задолжение {todo} од листата {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Избришавте задолжение {todo} од листата {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} ажурираше задолжение {todo} во листата {calendar}",
- "You updated todo {todo} in list {calendar}" : "Ажуриравте задолжение {todo} во листата {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} го реши задолжението {todo} во листата {calendar}",
- "You solved todo {todo} in list {calendar}" : "Го решивте задолжението {todo} во листата {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} повторно го отвори задолжението {todo} во листата {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Повторно го отворивте задолжението {todo} во листата {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} креираше задолжение {todo} во листата {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Креиравте задолжение {todo} во листата {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} избриша задолжение {todo} од листата {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Избришавте задолжение {todo} од листата {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} ажурираше задолжение {todo} во листата {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Ажуриравте задолжение {todo} во листата {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} го заврши задолжението {todo} во листата {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Го завршивте задолжението {todo} во листата {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} повторно го отвори задолжението {todo} во листата {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Повторно го отворивте задолжението {todo} во листата {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} премести задолжение {todo} од листа {sourceCalendar} во листа {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Преместивте задолжение {todo} од листа {sourceCalendar} во листа {targetCalendar}",
"Calendar, contacts and tasks" : "Календар, контакти и задачи",
"A <strong>calendar</strong> was modified" : "<strong>Календарот</strong> е променет",
"A calendar <strong>event</strong> was modified" : "Изменет е <strong>настан</strong> во календарот",
- "A calendar <strong>todo</strong> was modified" : "Изменета е <strong>задача</strong> во календарот",
+ "A calendar <strong>to-do</strong> was modified" : "Изменета е <strong>задача</strong> во календарот",
"Contact birthdays" : "Родендени на контактите",
"Death of %s" : "Смрт на %s",
+ "Untitled calendar" : "Неименуван календар",
"Calendar:" : "Календар:",
"Date:" : "Датум:",
"Where:" : "Каде: ",
"Description:" : "Опис:",
- "Untitled event" : "Неименуван настан",
"_%n year_::_%n years_" : ["една година","%n години"],
- "_%n month_::_%n months_" : ["еден месец","%n месеци"],
+ "_%n month_::_%n months_" : ["%n месец","%n месеци"],
"_%n day_::_%n days_" : ["еден ден","%n дена"],
"_%n hour_::_%n hours_" : ["еден час","%n часа"],
"_%n minute_::_%n minutes_" : ["една минута","%n минути"],
@@ -66,48 +73,123 @@ OC.L10N.register(
"Where: %s" : "Каде: %s",
"%1$s via %2$s" : "%1$s преку %2$s",
"Cancelled: %1$s" : "Откажано: %1$s",
- "Invitation canceled" : "Поканата е откажана",
+ "\"%1$s\" has been canceled" : "\"%1$s\" е отакажана",
"Re: %1$s" : "Одг: %1$s",
- "Invitation updated" : "Поканата е ажурирана",
+ "%1$s has accepted your invitation" : "%1$s ја прифати вашата покана",
+ "%1$s has tentatively accepted your invitation" : "%1$s привремено ја прифати нафаша покана",
+ "%1$s has declined your invitation" : "%1$s ја одби вашата покана",
+ "%1$s has responded to your invitation" : "%1$s одговори на вашата покана",
+ "Invitation updated: %1$s" : "Поканата е ажурирана: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s го ажурираше настанот \"%2$s\"",
"Invitation: %1$s" : "Покана: %1$s",
- "Invitation" : "Покани",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s сака да те покани во \"%2$s\"",
+ "Organizer:" : "Организатор:",
+ "Attendees:" : "Присутни:",
"Title:" : "Наслов:",
- "Time:" : "Време:",
+ "When:" : "Кога:",
"Location:" : "Локација:",
"Link:" : "Линк:",
- "Organizer:" : "Организатор:",
- "Attendees:" : "Присутни:",
"Accept" : "Прифати",
"Decline" : "Одбиј",
"More options …" : "Повеќе опции ...",
"More options at %s" : "Повеќе опции на %s",
+ "Monday" : "Понеделник",
+ "Tuesday" : "Вторник",
+ "Wednesday" : "Среда",
+ "Thursday" : "Четврток",
+ "Friday" : "Петок",
+ "Saturday" : "Сабота",
+ "Sunday" : "Недела",
+ "January" : "Јануари",
+ "February" : "Февруари",
+ "March" : "Март",
+ "April" : "Април",
+ "May" : "Мај",
+ "June" : "Јуни",
+ "July" : "Јули",
+ "August" : "Август",
+ "September" : "Септември",
+ "October" : "Октомври",
+ "November" : "Ноември",
+ "December" : "Декември",
+ "First" : "Прва",
+ "Last" : "Последна",
"Contacts" : "Контакти",
+ "{actor} created address book {addressbook}" : "{actor} креираше адресар {addressbook}",
+ "You created address book {addressbook}" : "Креиравте адресар {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} избриша адресар {addressbook}",
+ "You deleted address book {addressbook}" : "Избришавте адресар {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} ажурирање адресар {addressbook}",
+ "You updated address book {addressbook}" : "Ажуриравте {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} сподели адресар {addressbook} со вас",
+ "You shared address book {addressbook} with {user}" : "Споделивте адресар {addressbook} со {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} сподели адресар {addressbook} со {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} не го споделува адресар {addressbook} со вас",
+ "You unshared address book {addressbook} from {user}" : "Не го споделиувате адресар {addressbook} со {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} не го споделува адресар {addressbook} со {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} не го споделува адресар {addressbook} со себе",
+ "You shared address book {addressbook} with group {group}" : "Споделивте адресар {addressbook} со група {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} сподели адресар {addressbook} со група {group}",
+ "You unshared address book {addressbook} from group {group}" : "Остранивте од споделување адресар {addressbook} со група{group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} острани од споделување адресар {addressbook} од група {group}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>Контракт</strong> или <strong>адресар</strong> е променет",
+ "Accounts" : "Сметки",
+ "System address book which holds all accounts" : "Системски адресар кој ги содржи сите сметки",
+ "File is not updatable: %1$s" : "Датотека што не се ажурира: %1$s",
+ "_%n byte_::_%n bytes_" : ["%n бајт","%n бајти"],
+ "Failed to open file: %1$s" : "Неуспешно отварање на датотека: %1$s",
+ "File not found: %1$s" : "Датотеката не е пронајдена: %1$s",
"System is in maintenance mode." : "Системот е во мод за одржување.",
"Upgrade needed" : "Потребна е надградба",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Вашиот %s треба да биде конфигуриран за да користи HTTPS за да може да се користи CalDAV и CardDAV на iOS/macOS.",
"Configures a CalDAV account" : "Конфигурирај CalDAV сметка",
"Configures a CardDAV account" : "Конфигурирај CardDAV сметка",
"Events" : "Настани",
- "Tasks" : "Задачи",
"Untitled task" : "Неименувана задача",
"Completed on %s" : "Завршена на %s",
"Due on %s by %s" : "Истекува на %s од %s",
"Due on %s" : "Истекува на %s",
+ "WebDAV endpoint" : "WebDAV крајна точка",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Вашиот веб опслужувач сеуште не е точно подесен да овозможува синхронизација на датотеки бидејќи интерфејсот за WebDAV изгледа дека е расипан. ",
+ "Migrated calendar (%1$s)" : "мигриран календар (%1$s)",
+ "Calendars including events, details and attendees" : "Календари вклучувајќи настани, детали и присутни",
"Contacts and groups" : "Контакти и групи",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV крајна точка",
- "Availability" : "Достапност",
+ "Absence saved" : "Отсуството е зачувано",
+ "Failed to save your absence settings" : "Неуспешно зачувување на поставките за отсуство",
+ "Absence cleared" : "Отсуството е избришано",
+ "Failed to clear your absence settings" : "Неуспешно бришење на поставките за отсуство",
+ "First day" : "Прв ден",
+ "Last day (inclusive)" : "Последен ден (вклучително)",
+ "Out of office replacement (optional)" : "Замена надвор од канцеларија (опционално)",
+ "Name of the replacement" : "Има на замената",
+ "No results." : "Нема резултати.",
+ "Start typing." : "Започни со пишување.",
+ "Short absence status" : "Статус на кратко отсуство",
+ "Long absence Message" : "Порака за долго отсуство",
+ "Save" : "Зачувај",
+ "Disable absence" : "Оневозможи отсуство",
+ "Failed to load availability" : "Неуспешно вчитување на достапноста",
+ "Saved availability" : "Достапноста е зачувана",
+ "Failed to save availability" : "Неуспешно зачувување на достапноста",
+ "Time zone:" : "Временска зона:",
"to" : "до",
"Delete slot" : "Избриши слот",
+ "No working hours set" : "Не се поставени работни часови",
"Add slot" : "Додади слот",
- "Monday" : "Понеделник",
- "Tuesday" : "Вторник",
- "Wednesday" : "Среда",
- "Thursday" : "Четврток",
- "Friday" : "Петок",
- "Saturday" : "Сабота",
- "Sunday" : "Недела",
- "Save" : "Зачувај",
+ "Weekdays" : "Работни денови",
+ "Pick a start time for {dayName}" : "Избери почетно време за {dayName}",
+ "Pick a end time for {dayName}" : "Избери крајно време за {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Автоматско поставување на статус во \"Не вознемирувај\" недостапен за да ги занемите сите известувања.",
+ "Cancel" : "Откажи",
+ "Import" : "Увези",
+ "Reset to default" : "Ресетирај на стандардно",
+ "Availability" : "Достапност",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Ако ги поставите работните часови, другите корисници ќе можат да видат кога сте слободни за да можат да закажат состанок.",
+ "Absence" : "Отсуство",
+ "Configure your next absence period." : "Поставете го вашиот следен период на отсуство.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Исто така инсталирајте ја {calendarappstoreopen}Календар апликацијата{linkclose}, или {calendardocopen}поврзете го вашиот компјутер & мобилен за синхронизација ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Бидете сигурни дека правилно се поставени {emailopen}параметрите за Е-пошта{linkclose}.",
"Calendar server" : "Календар сервер",
"Send invitations to attendees" : "Испрати покани на учесниците",
"Automatically generate a birthday calendar" : "Автоматско генерирање на календар со родендени",
@@ -115,16 +197,13 @@ OC.L10N.register(
"Hence they will not be available immediately after enabling but will show up after some time." : "Оттука, тие нема да бидат достапни веднаш по овозможувањето, но ќе се појават по некое време.",
"Send notifications for events" : "Испрати известувања за настани",
"Notifications are sent via background jobs, so these must occur often enough." : "Известувањата ќе бидат испраќани преку задачите што се извршуваат во позадина, така што тие треба да се случуваат доволно често.",
+ "Send reminder notifications to calendar sharees as well" : "Испратете известувања за потсетници и до споделувањата на календарот",
+ "Reminders are always sent to organizers and attendees." : "Секогаш се испраќаат потсетници до организаторите и до присутните.",
"Enable notifications for events via push" : "Овозможи известувања за настани преку push",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Исто така инсталирајте ја {calendarappstoreopen}Календар апликацијата{linkclose}, или {calendardocopen}поврзете го вашиот компјутер & мобилен за синхронизација ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Бидете сигурни дека правилно се поставени {emailopen}параметрите за Е-пошта{linkclose}.",
"There was an error updating your attendance status." : "Настана грешка при ажурирање на вашето присуство.",
"Please contact the organizer directly." : "Контактирајте го организаторот директно.",
"Are you accepting the invitation?" : "Дали ја прифаќате поканата?",
"Tentative" : "Прелиминарно",
- "Number of guests" : "Број на гости",
- "Comment" : "Коментар",
- "Your attendance was updated successfully." : "Вашето присуство е успешно ажурирано.",
- "Calendar and tasks" : "Календар и задачи"
+ "Your attendance was updated successfully." : "Вашето присуство е успешно ажурирано."
},
"nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;");
diff --git a/apps/dav/l10n/mk.json b/apps/dav/l10n/mk.json
index c68b0878d56..50e9e168496 100644
--- a/apps/dav/l10n/mk.json
+++ b/apps/dav/l10n/mk.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Календар",
- "Todos" : "Задачи",
+ "Tasks" : "Задачи",
"Personal" : "Лично",
"{actor} created calendar {calendar}" : "{actor} креираше календар {calendar}",
"You created calendar {calendar}" : "Креиравте календар {calendar}",
@@ -8,6 +8,8 @@
"You deleted calendar {calendar}" : "Избришавте календар {calendar}",
"{actor} updated calendar {calendar}" : "{actor} ажурираше календар {calendar}",
"You updated calendar {calendar}" : "Ажуриравте календар {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} врати календар {calendar}",
+ "You restored calendar {calendar}" : "Вративте календар {calendar}",
"You shared calendar {calendar} as public link" : "Споделивте календар {calendar} како јавен линк",
"You removed public link for calendar {calendar}" : "Отстранивте јавен линк за календар {calendar}",
"{actor} shared calendar {calendar} with you" : "{actor} сподели календар {calendar} со вас",
@@ -21,38 +23,43 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} сподели календар {calendar} со група {group}",
"You unshared calendar {calendar} from group {group}" : "Отстранивте споделување на календар {calendar} од група {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} отстрани споделување на календар {calendar} од група {group}",
+ "Untitled event" : "Неименуван настан",
"{actor} created event {event} in calendar {calendar}" : "{actor} креираше настан {event} во календар {calendar}",
"You created event {event} in calendar {calendar}" : "Креиравте настан {event} во календар {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} избриша настан {event} од календар {calendar}",
"You deleted event {event} from calendar {calendar}" : "Избришавте настан {event} од календар {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} ажурираше настан {event} во календар {calendar}",
"You updated event {event} in calendar {calendar}" : "Ажуриравте настан {event} во календар {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} премести настан {event} од календар {sourceCalendar} во календар {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Преместивте настан {event} од календар {sourceCalendar} во календар {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} врати настан {event} во календарот {calendar}",
"You restored event {event} of calendar {calendar}" : "Вративте настан {event} во календарот {calendar}",
"Busy" : "Зафатен",
- "{actor} created todo {todo} in list {calendar}" : "{actor} креираше задолжение {todo} во листата {calendar}",
- "You created todo {todo} in list {calendar}" : "Креиравте задолжение {todo} во листата {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} избриша задолжение {todo} од листата {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Избришавте задолжение {todo} од листата {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} ажурираше задолжение {todo} во листата {calendar}",
- "You updated todo {todo} in list {calendar}" : "Ажуриравте задолжение {todo} во листата {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} го реши задолжението {todo} во листата {calendar}",
- "You solved todo {todo} in list {calendar}" : "Го решивте задолжението {todo} во листата {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} повторно го отвори задолжението {todo} во листата {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Повторно го отворивте задолжението {todo} во листата {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} креираше задолжение {todo} во листата {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Креиравте задолжение {todo} во листата {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} избриша задолжение {todo} од листата {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Избришавте задолжение {todo} од листата {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} ажурираше задолжение {todo} во листата {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Ажуриравте задолжение {todo} во листата {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} го заврши задолжението {todo} во листата {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Го завршивте задолжението {todo} во листата {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} повторно го отвори задолжението {todo} во листата {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Повторно го отворивте задолжението {todo} во листата {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} премести задолжение {todo} од листа {sourceCalendar} во листа {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Преместивте задолжение {todo} од листа {sourceCalendar} во листа {targetCalendar}",
"Calendar, contacts and tasks" : "Календар, контакти и задачи",
"A <strong>calendar</strong> was modified" : "<strong>Календарот</strong> е променет",
"A calendar <strong>event</strong> was modified" : "Изменет е <strong>настан</strong> во календарот",
- "A calendar <strong>todo</strong> was modified" : "Изменета е <strong>задача</strong> во календарот",
+ "A calendar <strong>to-do</strong> was modified" : "Изменета е <strong>задача</strong> во календарот",
"Contact birthdays" : "Родендени на контактите",
"Death of %s" : "Смрт на %s",
+ "Untitled calendar" : "Неименуван календар",
"Calendar:" : "Календар:",
"Date:" : "Датум:",
"Where:" : "Каде: ",
"Description:" : "Опис:",
- "Untitled event" : "Неименуван настан",
"_%n year_::_%n years_" : ["една година","%n години"],
- "_%n month_::_%n months_" : ["еден месец","%n месеци"],
+ "_%n month_::_%n months_" : ["%n месец","%n месеци"],
"_%n day_::_%n days_" : ["еден ден","%n дена"],
"_%n hour_::_%n hours_" : ["еден час","%n часа"],
"_%n minute_::_%n minutes_" : ["една минута","%n минути"],
@@ -64,48 +71,123 @@
"Where: %s" : "Каде: %s",
"%1$s via %2$s" : "%1$s преку %2$s",
"Cancelled: %1$s" : "Откажано: %1$s",
- "Invitation canceled" : "Поканата е откажана",
+ "\"%1$s\" has been canceled" : "\"%1$s\" е отакажана",
"Re: %1$s" : "Одг: %1$s",
- "Invitation updated" : "Поканата е ажурирана",
+ "%1$s has accepted your invitation" : "%1$s ја прифати вашата покана",
+ "%1$s has tentatively accepted your invitation" : "%1$s привремено ја прифати нафаша покана",
+ "%1$s has declined your invitation" : "%1$s ја одби вашата покана",
+ "%1$s has responded to your invitation" : "%1$s одговори на вашата покана",
+ "Invitation updated: %1$s" : "Поканата е ажурирана: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s го ажурираше настанот \"%2$s\"",
"Invitation: %1$s" : "Покана: %1$s",
- "Invitation" : "Покани",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s сака да те покани во \"%2$s\"",
+ "Organizer:" : "Организатор:",
+ "Attendees:" : "Присутни:",
"Title:" : "Наслов:",
- "Time:" : "Време:",
+ "When:" : "Кога:",
"Location:" : "Локација:",
"Link:" : "Линк:",
- "Organizer:" : "Организатор:",
- "Attendees:" : "Присутни:",
"Accept" : "Прифати",
"Decline" : "Одбиј",
"More options …" : "Повеќе опции ...",
"More options at %s" : "Повеќе опции на %s",
+ "Monday" : "Понеделник",
+ "Tuesday" : "Вторник",
+ "Wednesday" : "Среда",
+ "Thursday" : "Четврток",
+ "Friday" : "Петок",
+ "Saturday" : "Сабота",
+ "Sunday" : "Недела",
+ "January" : "Јануари",
+ "February" : "Февруари",
+ "March" : "Март",
+ "April" : "Април",
+ "May" : "Мај",
+ "June" : "Јуни",
+ "July" : "Јули",
+ "August" : "Август",
+ "September" : "Септември",
+ "October" : "Октомври",
+ "November" : "Ноември",
+ "December" : "Декември",
+ "First" : "Прва",
+ "Last" : "Последна",
"Contacts" : "Контакти",
+ "{actor} created address book {addressbook}" : "{actor} креираше адресар {addressbook}",
+ "You created address book {addressbook}" : "Креиравте адресар {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} избриша адресар {addressbook}",
+ "You deleted address book {addressbook}" : "Избришавте адресар {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} ажурирање адресар {addressbook}",
+ "You updated address book {addressbook}" : "Ажуриравте {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} сподели адресар {addressbook} со вас",
+ "You shared address book {addressbook} with {user}" : "Споделивте адресар {addressbook} со {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} сподели адресар {addressbook} со {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} не го споделува адресар {addressbook} со вас",
+ "You unshared address book {addressbook} from {user}" : "Не го споделиувате адресар {addressbook} со {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} не го споделува адресар {addressbook} со {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} не го споделува адресар {addressbook} со себе",
+ "You shared address book {addressbook} with group {group}" : "Споделивте адресар {addressbook} со група {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} сподели адресар {addressbook} со група {group}",
+ "You unshared address book {addressbook} from group {group}" : "Остранивте од споделување адресар {addressbook} со група{group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} острани од споделување адресар {addressbook} од група {group}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>Контракт</strong> или <strong>адресар</strong> е променет",
+ "Accounts" : "Сметки",
+ "System address book which holds all accounts" : "Системски адресар кој ги содржи сите сметки",
+ "File is not updatable: %1$s" : "Датотека што не се ажурира: %1$s",
+ "_%n byte_::_%n bytes_" : ["%n бајт","%n бајти"],
+ "Failed to open file: %1$s" : "Неуспешно отварање на датотека: %1$s",
+ "File not found: %1$s" : "Датотеката не е пронајдена: %1$s",
"System is in maintenance mode." : "Системот е во мод за одржување.",
"Upgrade needed" : "Потребна е надградба",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Вашиот %s треба да биде конфигуриран за да користи HTTPS за да може да се користи CalDAV и CardDAV на iOS/macOS.",
"Configures a CalDAV account" : "Конфигурирај CalDAV сметка",
"Configures a CardDAV account" : "Конфигурирај CardDAV сметка",
"Events" : "Настани",
- "Tasks" : "Задачи",
"Untitled task" : "Неименувана задача",
"Completed on %s" : "Завршена на %s",
"Due on %s by %s" : "Истекува на %s од %s",
"Due on %s" : "Истекува на %s",
+ "WebDAV endpoint" : "WebDAV крајна точка",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Вашиот веб опслужувач сеуште не е точно подесен да овозможува синхронизација на датотеки бидејќи интерфејсот за WebDAV изгледа дека е расипан. ",
+ "Migrated calendar (%1$s)" : "мигриран календар (%1$s)",
+ "Calendars including events, details and attendees" : "Календари вклучувајќи настани, детали и присутни",
"Contacts and groups" : "Контакти и групи",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV крајна точка",
- "Availability" : "Достапност",
+ "Absence saved" : "Отсуството е зачувано",
+ "Failed to save your absence settings" : "Неуспешно зачувување на поставките за отсуство",
+ "Absence cleared" : "Отсуството е избришано",
+ "Failed to clear your absence settings" : "Неуспешно бришење на поставките за отсуство",
+ "First day" : "Прв ден",
+ "Last day (inclusive)" : "Последен ден (вклучително)",
+ "Out of office replacement (optional)" : "Замена надвор од канцеларија (опционално)",
+ "Name of the replacement" : "Има на замената",
+ "No results." : "Нема резултати.",
+ "Start typing." : "Започни со пишување.",
+ "Short absence status" : "Статус на кратко отсуство",
+ "Long absence Message" : "Порака за долго отсуство",
+ "Save" : "Зачувај",
+ "Disable absence" : "Оневозможи отсуство",
+ "Failed to load availability" : "Неуспешно вчитување на достапноста",
+ "Saved availability" : "Достапноста е зачувана",
+ "Failed to save availability" : "Неуспешно зачувување на достапноста",
+ "Time zone:" : "Временска зона:",
"to" : "до",
"Delete slot" : "Избриши слот",
+ "No working hours set" : "Не се поставени работни часови",
"Add slot" : "Додади слот",
- "Monday" : "Понеделник",
- "Tuesday" : "Вторник",
- "Wednesday" : "Среда",
- "Thursday" : "Четврток",
- "Friday" : "Петок",
- "Saturday" : "Сабота",
- "Sunday" : "Недела",
- "Save" : "Зачувај",
+ "Weekdays" : "Работни денови",
+ "Pick a start time for {dayName}" : "Избери почетно време за {dayName}",
+ "Pick a end time for {dayName}" : "Избери крајно време за {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Автоматско поставување на статус во \"Не вознемирувај\" недостапен за да ги занемите сите известувања.",
+ "Cancel" : "Откажи",
+ "Import" : "Увези",
+ "Reset to default" : "Ресетирај на стандардно",
+ "Availability" : "Достапност",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Ако ги поставите работните часови, другите корисници ќе можат да видат кога сте слободни за да можат да закажат состанок.",
+ "Absence" : "Отсуство",
+ "Configure your next absence period." : "Поставете го вашиот следен период на отсуство.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Исто така инсталирајте ја {calendarappstoreopen}Календар апликацијата{linkclose}, или {calendardocopen}поврзете го вашиот компјутер & мобилен за синхронизација ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Бидете сигурни дека правилно се поставени {emailopen}параметрите за Е-пошта{linkclose}.",
"Calendar server" : "Календар сервер",
"Send invitations to attendees" : "Испрати покани на учесниците",
"Automatically generate a birthday calendar" : "Автоматско генерирање на календар со родендени",
@@ -113,16 +195,13 @@
"Hence they will not be available immediately after enabling but will show up after some time." : "Оттука, тие нема да бидат достапни веднаш по овозможувањето, но ќе се појават по некое време.",
"Send notifications for events" : "Испрати известувања за настани",
"Notifications are sent via background jobs, so these must occur often enough." : "Известувањата ќе бидат испраќани преку задачите што се извршуваат во позадина, така што тие треба да се случуваат доволно често.",
+ "Send reminder notifications to calendar sharees as well" : "Испратете известувања за потсетници и до споделувањата на календарот",
+ "Reminders are always sent to organizers and attendees." : "Секогаш се испраќаат потсетници до организаторите и до присутните.",
"Enable notifications for events via push" : "Овозможи известувања за настани преку push",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Исто така инсталирајте ја {calendarappstoreopen}Календар апликацијата{linkclose}, или {calendardocopen}поврзете го вашиот компјутер & мобилен за синхронизација ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Бидете сигурни дека правилно се поставени {emailopen}параметрите за Е-пошта{linkclose}.",
"There was an error updating your attendance status." : "Настана грешка при ажурирање на вашето присуство.",
"Please contact the organizer directly." : "Контактирајте го организаторот директно.",
"Are you accepting the invitation?" : "Дали ја прифаќате поканата?",
"Tentative" : "Прелиминарно",
- "Number of guests" : "Број на гости",
- "Comment" : "Коментар",
- "Your attendance was updated successfully." : "Вашето присуство е успешно ажурирано.",
- "Calendar and tasks" : "Календар и задачи"
+ "Your attendance was updated successfully." : "Вашето присуство е успешно ажурирано."
},"pluralForm" :"nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;"
} \ No newline at end of file
diff --git a/apps/dav/l10n/nb.js b/apps/dav/l10n/nb.js
index 4d9fdab30d4..d10ba88149b 100644
--- a/apps/dav/l10n/nb.js
+++ b/apps/dav/l10n/nb.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Kalender",
- "Todos" : "Gjøremål",
+ "Tasks" : "Oppgaver",
"Personal" : "Personlig",
"{actor} created calendar {calendar}" : "{actor} opprettet kalenderen {calendar}",
"You created calendar {calendar}" : "Du opprettet kalenderen {calendar}",
@@ -10,46 +10,56 @@ OC.L10N.register(
"You deleted calendar {calendar}" : "Du slettet kalenderen {calendar}",
"{actor} updated calendar {calendar}" : "{actor} oppdaterte kalenderen {calendar}",
"You updated calendar {calendar}" : "Du oppdaterte kalenderen {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} gjenopprettet kalenderen {calendar}",
+ "You restored calendar {calendar}" : "Du gjenopprettet kalenderen {calendar}",
"You shared calendar {calendar} as public link" : "Du delte kalenderen {calendar} som offentlig lenke",
"You removed public link for calendar {calendar}" : "Du fjernet offentlig lenke for kalenderen {calendar}",
"{actor} shared calendar {calendar} with you" : "{actor} delte kalenderen {calendar} med deg",
"You shared calendar {calendar} with {user}" : "Du delte kalenderen {calendar} med {user}",
"{actor} shared calendar {calendar} with {user}" : "{actor} delte kalenderen {calendar} med {user}",
"{actor} unshared calendar {calendar} from you" : "{actor} opphevde delingen av kalenderen {calendar} med deg",
- "You unshared calendar {calendar} from {user}" : "Du opphevde delingen av kalender {calendar} med {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} opphevde delingen av kalender {calendar} med {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} opphevde delingen av kalender {calendar} med seg selv",
+ "You unshared calendar {calendar} from {user}" : "Du opphevde delingen av kalenderen {calendar} med {user}",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor} opphevde delingen av kalenderen {calendar} med {user}",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} opphevde delingen av kalenderen {calendar} med seg selv",
"You shared calendar {calendar} with group {group}" : "Du delte kalender {calendar} med gruppe {group}",
"{actor} shared calendar {calendar} with group {group}" : "{actor} delte kalenderen {calendar} med gruppe {group}",
"You unshared calendar {calendar} from group {group}" : "Du opphevde deling av kalenderen {calendar} med gruppe {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} opphevde deling av kalenderen {calendar} med gruppe {group}",
+ "Untitled event" : "Hendelse uten tittel",
"{actor} created event {event} in calendar {calendar}" : "{actor} opprettet en hendelse {event} i kalenderen {calendar}",
"You created event {event} in calendar {calendar}" : "Du opprettet en hendelse {event} i kalenderen {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} slettet hendelsen {event} fra kalenderen {calendar}",
"You deleted event {event} from calendar {calendar}" : "Du slettet hendelsen {event} fra kalenderen {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} oppdaterte hendelsen {event} i kalenderen {calendar}",
"You updated event {event} in calendar {calendar}" : "Du oppdaterte hendelsen {event} i kalenderen {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} flyttet hendelsen {event} fra kalenderen {sourceCalendar} til kalenderen {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Du flyttet hendelsen {event} fra kalenderen {sourceCalendar} til kalenderen {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} opprettet hendelsen {event} i kalenderen {calendar}",
+ "You restored event {event} of calendar {calendar}" : "Du gjenopprettet hendelsen {event} i kalenderen {calendar}",
"Busy" : "Opptatt",
- "{actor} created todo {todo} in list {calendar}" : "{actor} opprettet en oppgaven {todo} i listen {calendar}",
- "You created todo {todo} in list {calendar}" : "Du opprettet en oppgave {todo} i listen {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} slettet gjøremålet {todo} fra listen {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Du slettet gjøremålet {todo} fra listen {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} oppdaterte gjøremålet {todo} i listen {calendar}",
- "You updated todo {todo} in list {calendar}" : "Du oppdaterte gjøremålet {todo} i listen {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} ferdigstilte gjøremålet {todo} i listen {calendar}",
- "You solved todo {todo} in list {calendar}" : "Du ferdigstilte gjøremålet {todo} i listen {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} gjenåpnet gjøremålet {todo} i listen {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Du gjenåpnet oppgaven {todo} i listen {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} opprettet gjøremålet {todo} i listen {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Du opprettet gjøremålet {todo} i listen {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} slettet gjøremålet {todo} fra listen {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Du slettet gjøremålet {todo} fra listen {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} oppdaterte gjøremålet {todo} i listen {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Du oppdaterte gjøremålet {todo} i listen {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} ferdigstilte gjøremålet {todo} i listen {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Du ferdigstilte gjøremålet {todo} i listen {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} gjenåpnet gjøremålet {todo} i listen {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Du gjenåpnet gjøremålet {todo} i listen {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} flyttet gjøremålet {todo} fra listen {sourceCalendar} til listen {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Du flyttet gjøremålet {todo} fra listen {sourceCalendar} til listen {targetCalendar}",
+ "Calendar, contacts and tasks" : "Kalender, kontakter og oppgaver",
"A <strong>calendar</strong> was modified" : "En <strong>kalender</strong> ble endret",
"A calendar <strong>event</strong> was modified" : "En kalender <strong>hendelse</strong> ble endret",
- "A calendar <strong>todo</strong> was modified" : "En kalende <strong>gjøremål</strong> ble endret",
+ "A calendar <strong>to-do</strong> was modified" : "Ett kalender- <strong>gjøremål</strong> ble endret",
"Contact birthdays" : "Kontakters fødelsdag",
"Death of %s" : "Død av %s",
+ "Untitled calendar" : "Kalender uten tittel",
"Calendar:" : "Kalender:",
"Date:" : "Dato:",
"Where:" : "Hvor:",
"Description:" : "Beskrivelse:",
- "Untitled event" : "Hendelse uten tittel",
"_%n year_::_%n years_" : ["%n år","%n år"],
"_%n month_::_%n months_" : ["%n måned","%n måneder"],
"_%n day_::_%n days_" : ["%n dag","%n dager"],
@@ -62,57 +72,205 @@ OC.L10N.register(
"Description: %s" : "Beskrivelse: %s",
"Where: %s" : "Hvor: %s",
"%1$s via %2$s" : "%1$s via %2$s",
- "Invitation canceled" : "Invitasjon tilbakekalt",
- "Invitation updated" : "Invitasjon oppdatert",
- "Invitation" : "Invitasjon",
+ "Could not generate when statement" : "Kunne ikke generere når-beskrivelse",
+ "Every Day for the entire day" : "Hver dag for hele dagen",
+ "Every Day for the entire day until %1$s" : "Hver dag for hele dagen til %1$s",
+ "Every Day between %1$s - %2$s" : "Hver dag mellom %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Hver dag mellom %1$s - %2$s til %3$s",
+ "Every %1$d Days for the entire day" : "Hver %1$d dag for hele dagen",
+ "Every %1$d Days for the entire day until %2$s" : "Hver %1$d dag for hele dagen til %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Hver %1$d dag mellom %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Hver %1$d dag mellom %2$s - %3$s til %4$s",
+ "Could not generate event recurrence statement" : "Kunne ikke generere gjentakelse-beskrivelse",
+ "Every Week on %1$s for the entire day" : "Hver uke på %1$s for hele dagen",
+ "Every Week on %1$s for the entire day until %2$s" : "Hver uke på %1$s for hele dagen til %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Hver uke på %1$s mellom %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Hver uke på %1$s mellom %2$s - %3$s til %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Hver %1$d uke på %2$s for hele dagen",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Hver %1$d uke på %2$s for hele dagen til %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Hver %1$d uke på %2$s mellom %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Hver %1$d uke på %2$s mellom %3$s - %4$s til %5$s",
+ "Every Month on the %1$s for the entire day" : "Hver måned på den %1$s for hele dagen",
+ "Every Month on the %1$s for the entire day until %2$s" : "Hver måned på den %1$s for hele dagen til %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Hver måned på den %1$s mellom %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Hver måned på den %1$s mellom %2$s - %3$s til %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Hver %1$d måned på den %2$s for hele dagen",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Hver %1$d måned på den %2$s for hele dagen til %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Hver %1$d måned på den %2$s mellom %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Hver %1$d måned på den %2$s mellom %3$s - %4$s til %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Årlig i %1$s på den %2$s for hele dagen",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Årlig i %1$s på den %2$s for hele dagen til %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Årlig i %1$s på den %2$s mellom %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Årlig i %1$s på den %2$s mellom %3$s - %4$s til %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Hvert %1$d år i %2$s på den %3$s for hele dagen",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Hvert %1$d år i %2$s på den %3$s for hele dagen til %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Hvert %1$d år i %2$s på den %3$s mellom %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Hvert %1$d år i %2$s på den %3$s mellom %4$s - %5$s til %6$s",
+ "On specific dates for the entire day until %1$s" : "På bestemte datoer for hele dagen til %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "På bestemte datoer mellom %1$s - %2$s til %3$s",
+ "Could not generate next recurrence statement" : "Kan ikke generere neste gjentakelse-beskrivelse",
+ "Cancelled: %1$s" : "Kansellerte: %1$s",
+ "\"%1$s\" has been canceled" : "\"%1$s\" har blitt kansellert",
+ "Re: %1$s" : "Sv: %1$s",
+ "%1$s has accepted your invitation" : "%1$s har godtatt invitasjonen din",
+ "%1$s has tentatively accepted your invitation" : "%1$s har foreløpig godtatt invitasjonen din",
+ "%1$s has declined your invitation" : "%1$s har avvist invitasjonen din",
+ "%1$s has responded to your invitation" : "%1$s har svart på invitasjonen din",
+ "Invitation updated: %1$s" : "Invitasjon oppdatert: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s oppdaterte hendelsen \"%2$s\"",
+ "Invitation: %1$s" : "Invitasjon: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s vil invitere deg til \"%2$s\"",
+ "Organizer:" : "Arrangør:",
+ "Attendees:" : "Deltakere:",
"Title:" : "Tittel:",
- "Time:" : "Tid:",
+ "When:" : "Når:",
"Location:" : "Sted:",
"Link:" : "Lenke:",
- "Organizer:" : "Organisator:",
- "Attendees:" : "Deltakere:",
+ "Occurring:" : "Forekommer:",
"Accept" : "Aksepter",
"Decline" : "Avslå",
"More options …" : "Flere alternativer ...",
"More options at %s" : "Flere alternativer ved %s",
+ "Monday" : "Mandag",
+ "Tuesday" : "Tirsdag",
+ "Wednesday" : "Onsdag",
+ "Thursday" : "Torsdag",
+ "Friday" : "Fredag",
+ "Saturday" : "Lørdag",
+ "Sunday" : "Søndag",
+ "January" : "Januar",
+ "February" : "Februar",
+ "March" : "Mars",
+ "April" : "April",
+ "May" : "Mai",
+ "June" : "Juni",
+ "July" : "Juli",
+ "August" : "August",
+ "September" : "September",
+ "October" : "Oktober",
+ "November" : "November",
+ "December" : "Desember",
+ "First" : "Først",
+ "Second" : "Andre",
+ "Third" : "Tredje",
+ "Fourth" : "Fjerde",
+ "Last" : "Siste",
+ "Second Last" : "Nest sist",
+ "Third Last" : "Tredje sist",
+ "Fourth Last" : "Fjerde sist",
"Contacts" : "Kontakter",
+ "{actor} created address book {addressbook}" : "{actor} opprettet adresseboken {addressbook}",
+ "You created address book {addressbook}" : "Du opprettet adresseboken {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} slettet adresseboken {addressbook}",
+ "You deleted address book {addressbook}" : "Du slettet adresseboken {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} oppdaterte adresseboken {addressbook}",
+ "You updated address book {addressbook}" : "Du oppdaterte adresseboken {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} delte adresseboken {addressbook} med deg",
+ "You shared address book {addressbook} with {user}" : "Du delte adresseboken {addressbook} med {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} delte adresseboken {addressbook} med {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} opphevde deling av adresseboken {addressbook} med deg",
+ "You unshared address book {addressbook} from {user}" : "Du opphevde deling av adresseboken {addressbook} med {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} opphevde deling av adresseboken {addressbook} med {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} opphevde deling av adresseboken {addressbook} med seg selv",
+ "You shared address book {addressbook} with group {group}" : "Du delte adresseboken {addressbook} med gruppen {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} delte adresseboken {addressbook} med gruppen {group}",
+ "You unshared address book {addressbook} from group {group}" : "Du har opphevde deling av adresseboken {addressbook} med gruppen {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} opphevde deling av adresseboken {addressbook} med gruppen {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} opprettet kontakten {card} i adresseboken {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Du opprettet kontakten {card} i adresseboken {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} slettet kontakten {card} i adresseboken {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Du slettet kontakten {card} i adresseboken {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} oppdaterte kontakten {card} i adresseboken {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Du oppdaterte kontakten {card} i adresseboken {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "En <strong>kontakt</strong> eller <strong>adressebok</strong> ble endret",
+ "Accounts" : "Kontoer",
+ "System address book which holds all accounts" : "Systemadressebok som inneholder alle kontoer",
+ "File is not updatable: %1$s" : "Filen kan ikke oppdateres: %1$s",
+ "Could not write to final file, canceled by hook" : "Kunne ikke skrive til den endelige filen, kansellert av hook",
+ "Could not write file contents" : "Kunne ikke skrive filinnhold",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Feil under kopiering av fil til målplassering (kopiert: %1$s, forventet filstørrelse: %2$s)",
+ "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." : "Forventet filstørrelse på %1$s, men lest (fra Nextcloud-klienten) og skrev (til Nextcloud-lagring) %2$s. Kan enten være et nettverksproblem på avsendersiden eller et problem med å skrive til lagringen på serversiden.",
+ "Could not rename part file to final file, canceled by hook" : "Kunne ikke endre navn på delfilen til endelig fil, kansellert av hook",
+ "Could not rename part file to final file" : "Kunne ikke endre navn på delfil til endelig fil",
+ "Failed to check file size: %1$s" : "Kunne ikke kontrollere filstørrelsen: %1$s",
+ "Encryption not ready: %1$s" : "Kryptering ikke klar: %1$s",
+ "Failed to open file: %1$s" : "Kunne ikke åpne filen: %1$s",
+ "Failed to unlink: %1$s" : "Kunne ikke fjerne tilknytningen: %1$s",
+ "Failed to write file contents: %1$s" : "Kunne ikke skrive filinnhold: %1$s",
+ "File not found: %1$s" : "Filen ble ikke funnet: %1$s",
+ "Invalid target path" : "Ugyldig målbane",
+ "System is in maintenance mode." : "System i vedlikeholdsmodus.",
"Upgrade needed" : "Oppgradering er nødvendig",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Din %s må konfigureres til å bruke HTTPS for å kunne bruke CalDAV og CardDAV med iOS/macOS.",
"Configures a CalDAV account" : "Konfigurerer en CalDAV konto",
"Configures a CardDAV account" : "Konfigurerer en CardDAV konto",
"Events" : "Hendelser",
- "Tasks" : "Oppgaver",
"Untitled task" : "Oppgave uten tittel",
"Completed on %s" : "Ferdig på %s",
"Due on %s by %s" : "Forfaller på %s ved %s",
"Due on %s" : "Forfaller på %s",
- "WebDAV" : "WebDAV",
+ "DAV system address book" : "DAV-systemadressebok",
+ "No outstanding DAV system address book sync." : "Ingen utestående DAV-systemadressebok synkronisering.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV-systemadresseboksynkroniseringen har ikke kjørt enda fordi forekomsten har mer enn 1000 brukere, eller fordi det oppstod en feil. Kjør den manuelt ved å kalle \"occ dav: sync-system-addressbook\".",
"WebDAV endpoint" : "WebDAV endepunkt",
- "to" : "til",
- "Monday" : "Mandag",
- "Tuesday" : "Tirsdag",
- "Wednesday" : "Onsdag",
- "Thursday" : "Torsdag",
- "Friday" : "Fredag",
- "Saturday" : "Lørdag",
- "Sunday" : "Søndag",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Kunne ikke kontrollere at webserveren er riktig konfigurert for å tillate filsynkronisering over WebDAV. Vennligst sjekk manuelt.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Webserveren din er ikke satt opp til å tillate synkronisering av filer ennå, fordi WebDAV-grensesnittet ikke ser ut til å virke.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Webserveren din er riktig konfigurert for å tillate filsynkronisering over WebDAV.",
+ "Migrated calendar (%1$s)" : "Migrerte kalenderen (%1$s)",
+ "Calendars including events, details and attendees" : "Kalendere inkludert hendelser, detaljer og deltakere",
+ "Contacts and groups" : "Kontakter og grupper",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "Fravær lagret",
+ "Failed to save your absence settings" : "Lagring av dine fraværsinnstillinger feilet",
+ "Absence cleared" : "Fravær fjernet",
+ "Failed to clear your absence settings" : "Fjerning av dine fraværsinnstillinger feilet",
+ "First day" : "Første dag",
+ "Last day (inclusive)" : "Siste dag (inklusive)",
+ "Out of office replacement (optional)" : "Ikke til stede erstatter (valgfritt)",
+ "Name of the replacement" : "Navn på erstatteren",
+ "No results." : "Ingen resultater.",
+ "Start typing." : "Begynn å skrive.",
+ "Short absence status" : "Kort fraværsstatus",
+ "Long absence Message" : "Lang fraværsmelding",
"Save" : "Lagre",
+ "Disable absence" : "Deaktiver fravær",
+ "Failed to load availability" : "Feilet med laste inn arbeidstid",
+ "Saved availability" : "Lagret arbeidstid",
+ "Failed to save availability" : "Feilet ved lagring av arbeidstid",
+ "Time zone:" : "Tidssone:",
+ "to" : "til",
+ "Delete slot" : "Slett tidsluke",
+ "No working hours set" : "Ingen arbeidstid satt",
+ "Add slot" : "Legg til tidsrom",
+ "Weekdays" : "Ukedager",
+ "Pick a start time for {dayName}" : "Velg et starttidspunkt for {dayName}",
+ "Pick a end time for {dayName}" : "Velg et sluttidspunkt for {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Sett brukerstatus automatisk til «Ikke forstyrr» utenfor arbeidstid for å dempe alle varsler.",
+ "Cancel" : "Avbryt",
+ "Import" : "Importer",
+ "Error while saving settings" : "Feil ved lagring av innstillinger",
+ "Reset to default" : "Tilbakestill til standard",
+ "Availability" : "Arbeidstid",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Hvis du konfigurerer arbeidstiden din, vil andre personer se når du ikke er på kontoret når de bestiller et møte.",
+ "Absence" : "Fravær",
+ "Configure your next absence period." : "Konfigurer din neste fraværsperiode.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installer også {calendarappstoreopen} Kalender-appen {linkclose}, eller {calendardocopen} knytt datamaskinen og mobilen din for synkronisering ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Vennligst sørg for å sette opp {emailopen}e-postinnstillingene{linkclose} på riktig måte.",
"Calendar server" : "Kalenderserver",
- "Send invitations to attendees" : "Send invitasjoner til oppmøtte",
+ "Send invitations to attendees" : "Send invitasjoner til deltakere",
"Automatically generate a birthday calendar" : "Lag fødelsdagskalender automatisk",
"Birthday calendars will be generated by a background job." : "Fødselsdagskalender lages automatisk av en bakgrunnsjobb.",
"Hence they will not be available immediately after enabling but will show up after some time." : "Derav vil de ikke være tilgjengelige umiddelbart etter at du har skrudd dem på, men vil vises etter en stund.",
"Send notifications for events" : "Send varsler om hendelser",
"Notifications are sent via background jobs, so these must occur often enough." : "Varsler sendes via bakgrunnsjobber, så disse må forekomme ofte nok.",
+ "Send reminder notifications to calendar sharees as well" : "Send påminnelsesvarsler til kalenderdelinger også",
+ "Reminders are always sent to organizers and attendees." : "Reminders are always sent to organizers and attendees.",
"Enable notifications for events via push" : "Aktiver varsler for hendelser via push",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installer også {calendarappstoreopen} Kalender-appen {linkclose}, eller {calendardocopen} knytt datamaskinen og mobilen din for synkronisering ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Vennligst sørg for å sette opp {emailopen}e-postinnstillingene{linkclose} på riktig måte.",
"There was an error updating your attendance status." : "Det oppsto en feil under oppdateringen av oppmøtestatusen din.",
"Please contact the organizer directly." : "Ta kontakt med arrangøren direkte.",
"Are you accepting the invitation?" : "Aksepterer du invitasjonen?",
"Tentative" : "Foreløpig",
- "Comment" : "Kommentar",
- "Your attendance was updated successfully." : "Deltakelsen din ble oppdatert.",
- "Calendar and tasks" : "Kalender og oppgaver"
+ "Your attendance was updated successfully." : "Deltakelsen din ble oppdatert."
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/nb.json b/apps/dav/l10n/nb.json
index 0261627c154..d33852e0013 100644
--- a/apps/dav/l10n/nb.json
+++ b/apps/dav/l10n/nb.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Kalender",
- "Todos" : "Gjøremål",
+ "Tasks" : "Oppgaver",
"Personal" : "Personlig",
"{actor} created calendar {calendar}" : "{actor} opprettet kalenderen {calendar}",
"You created calendar {calendar}" : "Du opprettet kalenderen {calendar}",
@@ -8,46 +8,56 @@
"You deleted calendar {calendar}" : "Du slettet kalenderen {calendar}",
"{actor} updated calendar {calendar}" : "{actor} oppdaterte kalenderen {calendar}",
"You updated calendar {calendar}" : "Du oppdaterte kalenderen {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} gjenopprettet kalenderen {calendar}",
+ "You restored calendar {calendar}" : "Du gjenopprettet kalenderen {calendar}",
"You shared calendar {calendar} as public link" : "Du delte kalenderen {calendar} som offentlig lenke",
"You removed public link for calendar {calendar}" : "Du fjernet offentlig lenke for kalenderen {calendar}",
"{actor} shared calendar {calendar} with you" : "{actor} delte kalenderen {calendar} med deg",
"You shared calendar {calendar} with {user}" : "Du delte kalenderen {calendar} med {user}",
"{actor} shared calendar {calendar} with {user}" : "{actor} delte kalenderen {calendar} med {user}",
"{actor} unshared calendar {calendar} from you" : "{actor} opphevde delingen av kalenderen {calendar} med deg",
- "You unshared calendar {calendar} from {user}" : "Du opphevde delingen av kalender {calendar} med {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} opphevde delingen av kalender {calendar} med {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} opphevde delingen av kalender {calendar} med seg selv",
+ "You unshared calendar {calendar} from {user}" : "Du opphevde delingen av kalenderen {calendar} med {user}",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor} opphevde delingen av kalenderen {calendar} med {user}",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} opphevde delingen av kalenderen {calendar} med seg selv",
"You shared calendar {calendar} with group {group}" : "Du delte kalender {calendar} med gruppe {group}",
"{actor} shared calendar {calendar} with group {group}" : "{actor} delte kalenderen {calendar} med gruppe {group}",
"You unshared calendar {calendar} from group {group}" : "Du opphevde deling av kalenderen {calendar} med gruppe {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} opphevde deling av kalenderen {calendar} med gruppe {group}",
+ "Untitled event" : "Hendelse uten tittel",
"{actor} created event {event} in calendar {calendar}" : "{actor} opprettet en hendelse {event} i kalenderen {calendar}",
"You created event {event} in calendar {calendar}" : "Du opprettet en hendelse {event} i kalenderen {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} slettet hendelsen {event} fra kalenderen {calendar}",
"You deleted event {event} from calendar {calendar}" : "Du slettet hendelsen {event} fra kalenderen {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} oppdaterte hendelsen {event} i kalenderen {calendar}",
"You updated event {event} in calendar {calendar}" : "Du oppdaterte hendelsen {event} i kalenderen {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} flyttet hendelsen {event} fra kalenderen {sourceCalendar} til kalenderen {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Du flyttet hendelsen {event} fra kalenderen {sourceCalendar} til kalenderen {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} opprettet hendelsen {event} i kalenderen {calendar}",
+ "You restored event {event} of calendar {calendar}" : "Du gjenopprettet hendelsen {event} i kalenderen {calendar}",
"Busy" : "Opptatt",
- "{actor} created todo {todo} in list {calendar}" : "{actor} opprettet en oppgaven {todo} i listen {calendar}",
- "You created todo {todo} in list {calendar}" : "Du opprettet en oppgave {todo} i listen {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} slettet gjøremålet {todo} fra listen {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Du slettet gjøremålet {todo} fra listen {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} oppdaterte gjøremålet {todo} i listen {calendar}",
- "You updated todo {todo} in list {calendar}" : "Du oppdaterte gjøremålet {todo} i listen {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} ferdigstilte gjøremålet {todo} i listen {calendar}",
- "You solved todo {todo} in list {calendar}" : "Du ferdigstilte gjøremålet {todo} i listen {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} gjenåpnet gjøremålet {todo} i listen {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Du gjenåpnet oppgaven {todo} i listen {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} opprettet gjøremålet {todo} i listen {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Du opprettet gjøremålet {todo} i listen {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} slettet gjøremålet {todo} fra listen {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Du slettet gjøremålet {todo} fra listen {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} oppdaterte gjøremålet {todo} i listen {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Du oppdaterte gjøremålet {todo} i listen {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} ferdigstilte gjøremålet {todo} i listen {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Du ferdigstilte gjøremålet {todo} i listen {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} gjenåpnet gjøremålet {todo} i listen {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Du gjenåpnet gjøremålet {todo} i listen {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} flyttet gjøremålet {todo} fra listen {sourceCalendar} til listen {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Du flyttet gjøremålet {todo} fra listen {sourceCalendar} til listen {targetCalendar}",
+ "Calendar, contacts and tasks" : "Kalender, kontakter og oppgaver",
"A <strong>calendar</strong> was modified" : "En <strong>kalender</strong> ble endret",
"A calendar <strong>event</strong> was modified" : "En kalender <strong>hendelse</strong> ble endret",
- "A calendar <strong>todo</strong> was modified" : "En kalende <strong>gjøremål</strong> ble endret",
+ "A calendar <strong>to-do</strong> was modified" : "Ett kalender- <strong>gjøremål</strong> ble endret",
"Contact birthdays" : "Kontakters fødelsdag",
"Death of %s" : "Død av %s",
+ "Untitled calendar" : "Kalender uten tittel",
"Calendar:" : "Kalender:",
"Date:" : "Dato:",
"Where:" : "Hvor:",
"Description:" : "Beskrivelse:",
- "Untitled event" : "Hendelse uten tittel",
"_%n year_::_%n years_" : ["%n år","%n år"],
"_%n month_::_%n months_" : ["%n måned","%n måneder"],
"_%n day_::_%n days_" : ["%n dag","%n dager"],
@@ -60,57 +70,205 @@
"Description: %s" : "Beskrivelse: %s",
"Where: %s" : "Hvor: %s",
"%1$s via %2$s" : "%1$s via %2$s",
- "Invitation canceled" : "Invitasjon tilbakekalt",
- "Invitation updated" : "Invitasjon oppdatert",
- "Invitation" : "Invitasjon",
+ "Could not generate when statement" : "Kunne ikke generere når-beskrivelse",
+ "Every Day for the entire day" : "Hver dag for hele dagen",
+ "Every Day for the entire day until %1$s" : "Hver dag for hele dagen til %1$s",
+ "Every Day between %1$s - %2$s" : "Hver dag mellom %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Hver dag mellom %1$s - %2$s til %3$s",
+ "Every %1$d Days for the entire day" : "Hver %1$d dag for hele dagen",
+ "Every %1$d Days for the entire day until %2$s" : "Hver %1$d dag for hele dagen til %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Hver %1$d dag mellom %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Hver %1$d dag mellom %2$s - %3$s til %4$s",
+ "Could not generate event recurrence statement" : "Kunne ikke generere gjentakelse-beskrivelse",
+ "Every Week on %1$s for the entire day" : "Hver uke på %1$s for hele dagen",
+ "Every Week on %1$s for the entire day until %2$s" : "Hver uke på %1$s for hele dagen til %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Hver uke på %1$s mellom %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Hver uke på %1$s mellom %2$s - %3$s til %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Hver %1$d uke på %2$s for hele dagen",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Hver %1$d uke på %2$s for hele dagen til %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Hver %1$d uke på %2$s mellom %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Hver %1$d uke på %2$s mellom %3$s - %4$s til %5$s",
+ "Every Month on the %1$s for the entire day" : "Hver måned på den %1$s for hele dagen",
+ "Every Month on the %1$s for the entire day until %2$s" : "Hver måned på den %1$s for hele dagen til %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Hver måned på den %1$s mellom %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Hver måned på den %1$s mellom %2$s - %3$s til %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Hver %1$d måned på den %2$s for hele dagen",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Hver %1$d måned på den %2$s for hele dagen til %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Hver %1$d måned på den %2$s mellom %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Hver %1$d måned på den %2$s mellom %3$s - %4$s til %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Årlig i %1$s på den %2$s for hele dagen",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Årlig i %1$s på den %2$s for hele dagen til %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Årlig i %1$s på den %2$s mellom %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Årlig i %1$s på den %2$s mellom %3$s - %4$s til %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Hvert %1$d år i %2$s på den %3$s for hele dagen",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Hvert %1$d år i %2$s på den %3$s for hele dagen til %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Hvert %1$d år i %2$s på den %3$s mellom %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Hvert %1$d år i %2$s på den %3$s mellom %4$s - %5$s til %6$s",
+ "On specific dates for the entire day until %1$s" : "På bestemte datoer for hele dagen til %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "På bestemte datoer mellom %1$s - %2$s til %3$s",
+ "Could not generate next recurrence statement" : "Kan ikke generere neste gjentakelse-beskrivelse",
+ "Cancelled: %1$s" : "Kansellerte: %1$s",
+ "\"%1$s\" has been canceled" : "\"%1$s\" har blitt kansellert",
+ "Re: %1$s" : "Sv: %1$s",
+ "%1$s has accepted your invitation" : "%1$s har godtatt invitasjonen din",
+ "%1$s has tentatively accepted your invitation" : "%1$s har foreløpig godtatt invitasjonen din",
+ "%1$s has declined your invitation" : "%1$s har avvist invitasjonen din",
+ "%1$s has responded to your invitation" : "%1$s har svart på invitasjonen din",
+ "Invitation updated: %1$s" : "Invitasjon oppdatert: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s oppdaterte hendelsen \"%2$s\"",
+ "Invitation: %1$s" : "Invitasjon: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s vil invitere deg til \"%2$s\"",
+ "Organizer:" : "Arrangør:",
+ "Attendees:" : "Deltakere:",
"Title:" : "Tittel:",
- "Time:" : "Tid:",
+ "When:" : "Når:",
"Location:" : "Sted:",
"Link:" : "Lenke:",
- "Organizer:" : "Organisator:",
- "Attendees:" : "Deltakere:",
+ "Occurring:" : "Forekommer:",
"Accept" : "Aksepter",
"Decline" : "Avslå",
"More options …" : "Flere alternativer ...",
"More options at %s" : "Flere alternativer ved %s",
+ "Monday" : "Mandag",
+ "Tuesday" : "Tirsdag",
+ "Wednesday" : "Onsdag",
+ "Thursday" : "Torsdag",
+ "Friday" : "Fredag",
+ "Saturday" : "Lørdag",
+ "Sunday" : "Søndag",
+ "January" : "Januar",
+ "February" : "Februar",
+ "March" : "Mars",
+ "April" : "April",
+ "May" : "Mai",
+ "June" : "Juni",
+ "July" : "Juli",
+ "August" : "August",
+ "September" : "September",
+ "October" : "Oktober",
+ "November" : "November",
+ "December" : "Desember",
+ "First" : "Først",
+ "Second" : "Andre",
+ "Third" : "Tredje",
+ "Fourth" : "Fjerde",
+ "Last" : "Siste",
+ "Second Last" : "Nest sist",
+ "Third Last" : "Tredje sist",
+ "Fourth Last" : "Fjerde sist",
"Contacts" : "Kontakter",
+ "{actor} created address book {addressbook}" : "{actor} opprettet adresseboken {addressbook}",
+ "You created address book {addressbook}" : "Du opprettet adresseboken {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} slettet adresseboken {addressbook}",
+ "You deleted address book {addressbook}" : "Du slettet adresseboken {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} oppdaterte adresseboken {addressbook}",
+ "You updated address book {addressbook}" : "Du oppdaterte adresseboken {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} delte adresseboken {addressbook} med deg",
+ "You shared address book {addressbook} with {user}" : "Du delte adresseboken {addressbook} med {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} delte adresseboken {addressbook} med {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} opphevde deling av adresseboken {addressbook} med deg",
+ "You unshared address book {addressbook} from {user}" : "Du opphevde deling av adresseboken {addressbook} med {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} opphevde deling av adresseboken {addressbook} med {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} opphevde deling av adresseboken {addressbook} med seg selv",
+ "You shared address book {addressbook} with group {group}" : "Du delte adresseboken {addressbook} med gruppen {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} delte adresseboken {addressbook} med gruppen {group}",
+ "You unshared address book {addressbook} from group {group}" : "Du har opphevde deling av adresseboken {addressbook} med gruppen {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} opphevde deling av adresseboken {addressbook} med gruppen {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} opprettet kontakten {card} i adresseboken {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Du opprettet kontakten {card} i adresseboken {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} slettet kontakten {card} i adresseboken {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Du slettet kontakten {card} i adresseboken {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} oppdaterte kontakten {card} i adresseboken {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Du oppdaterte kontakten {card} i adresseboken {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "En <strong>kontakt</strong> eller <strong>adressebok</strong> ble endret",
+ "Accounts" : "Kontoer",
+ "System address book which holds all accounts" : "Systemadressebok som inneholder alle kontoer",
+ "File is not updatable: %1$s" : "Filen kan ikke oppdateres: %1$s",
+ "Could not write to final file, canceled by hook" : "Kunne ikke skrive til den endelige filen, kansellert av hook",
+ "Could not write file contents" : "Kunne ikke skrive filinnhold",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Feil under kopiering av fil til målplassering (kopiert: %1$s, forventet filstørrelse: %2$s)",
+ "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." : "Forventet filstørrelse på %1$s, men lest (fra Nextcloud-klienten) og skrev (til Nextcloud-lagring) %2$s. Kan enten være et nettverksproblem på avsendersiden eller et problem med å skrive til lagringen på serversiden.",
+ "Could not rename part file to final file, canceled by hook" : "Kunne ikke endre navn på delfilen til endelig fil, kansellert av hook",
+ "Could not rename part file to final file" : "Kunne ikke endre navn på delfil til endelig fil",
+ "Failed to check file size: %1$s" : "Kunne ikke kontrollere filstørrelsen: %1$s",
+ "Encryption not ready: %1$s" : "Kryptering ikke klar: %1$s",
+ "Failed to open file: %1$s" : "Kunne ikke åpne filen: %1$s",
+ "Failed to unlink: %1$s" : "Kunne ikke fjerne tilknytningen: %1$s",
+ "Failed to write file contents: %1$s" : "Kunne ikke skrive filinnhold: %1$s",
+ "File not found: %1$s" : "Filen ble ikke funnet: %1$s",
+ "Invalid target path" : "Ugyldig målbane",
+ "System is in maintenance mode." : "System i vedlikeholdsmodus.",
"Upgrade needed" : "Oppgradering er nødvendig",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Din %s må konfigureres til å bruke HTTPS for å kunne bruke CalDAV og CardDAV med iOS/macOS.",
"Configures a CalDAV account" : "Konfigurerer en CalDAV konto",
"Configures a CardDAV account" : "Konfigurerer en CardDAV konto",
"Events" : "Hendelser",
- "Tasks" : "Oppgaver",
"Untitled task" : "Oppgave uten tittel",
"Completed on %s" : "Ferdig på %s",
"Due on %s by %s" : "Forfaller på %s ved %s",
"Due on %s" : "Forfaller på %s",
- "WebDAV" : "WebDAV",
+ "DAV system address book" : "DAV-systemadressebok",
+ "No outstanding DAV system address book sync." : "Ingen utestående DAV-systemadressebok synkronisering.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV-systemadresseboksynkroniseringen har ikke kjørt enda fordi forekomsten har mer enn 1000 brukere, eller fordi det oppstod en feil. Kjør den manuelt ved å kalle \"occ dav: sync-system-addressbook\".",
"WebDAV endpoint" : "WebDAV endepunkt",
- "to" : "til",
- "Monday" : "Mandag",
- "Tuesday" : "Tirsdag",
- "Wednesday" : "Onsdag",
- "Thursday" : "Torsdag",
- "Friday" : "Fredag",
- "Saturday" : "Lørdag",
- "Sunday" : "Søndag",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Kunne ikke kontrollere at webserveren er riktig konfigurert for å tillate filsynkronisering over WebDAV. Vennligst sjekk manuelt.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Webserveren din er ikke satt opp til å tillate synkronisering av filer ennå, fordi WebDAV-grensesnittet ikke ser ut til å virke.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Webserveren din er riktig konfigurert for å tillate filsynkronisering over WebDAV.",
+ "Migrated calendar (%1$s)" : "Migrerte kalenderen (%1$s)",
+ "Calendars including events, details and attendees" : "Kalendere inkludert hendelser, detaljer og deltakere",
+ "Contacts and groups" : "Kontakter og grupper",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "Fravær lagret",
+ "Failed to save your absence settings" : "Lagring av dine fraværsinnstillinger feilet",
+ "Absence cleared" : "Fravær fjernet",
+ "Failed to clear your absence settings" : "Fjerning av dine fraværsinnstillinger feilet",
+ "First day" : "Første dag",
+ "Last day (inclusive)" : "Siste dag (inklusive)",
+ "Out of office replacement (optional)" : "Ikke til stede erstatter (valgfritt)",
+ "Name of the replacement" : "Navn på erstatteren",
+ "No results." : "Ingen resultater.",
+ "Start typing." : "Begynn å skrive.",
+ "Short absence status" : "Kort fraværsstatus",
+ "Long absence Message" : "Lang fraværsmelding",
"Save" : "Lagre",
+ "Disable absence" : "Deaktiver fravær",
+ "Failed to load availability" : "Feilet med laste inn arbeidstid",
+ "Saved availability" : "Lagret arbeidstid",
+ "Failed to save availability" : "Feilet ved lagring av arbeidstid",
+ "Time zone:" : "Tidssone:",
+ "to" : "til",
+ "Delete slot" : "Slett tidsluke",
+ "No working hours set" : "Ingen arbeidstid satt",
+ "Add slot" : "Legg til tidsrom",
+ "Weekdays" : "Ukedager",
+ "Pick a start time for {dayName}" : "Velg et starttidspunkt for {dayName}",
+ "Pick a end time for {dayName}" : "Velg et sluttidspunkt for {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Sett brukerstatus automatisk til «Ikke forstyrr» utenfor arbeidstid for å dempe alle varsler.",
+ "Cancel" : "Avbryt",
+ "Import" : "Importer",
+ "Error while saving settings" : "Feil ved lagring av innstillinger",
+ "Reset to default" : "Tilbakestill til standard",
+ "Availability" : "Arbeidstid",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Hvis du konfigurerer arbeidstiden din, vil andre personer se når du ikke er på kontoret når de bestiller et møte.",
+ "Absence" : "Fravær",
+ "Configure your next absence period." : "Konfigurer din neste fraværsperiode.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installer også {calendarappstoreopen} Kalender-appen {linkclose}, eller {calendardocopen} knytt datamaskinen og mobilen din for synkronisering ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Vennligst sørg for å sette opp {emailopen}e-postinnstillingene{linkclose} på riktig måte.",
"Calendar server" : "Kalenderserver",
- "Send invitations to attendees" : "Send invitasjoner til oppmøtte",
+ "Send invitations to attendees" : "Send invitasjoner til deltakere",
"Automatically generate a birthday calendar" : "Lag fødelsdagskalender automatisk",
"Birthday calendars will be generated by a background job." : "Fødselsdagskalender lages automatisk av en bakgrunnsjobb.",
"Hence they will not be available immediately after enabling but will show up after some time." : "Derav vil de ikke være tilgjengelige umiddelbart etter at du har skrudd dem på, men vil vises etter en stund.",
"Send notifications for events" : "Send varsler om hendelser",
"Notifications are sent via background jobs, so these must occur often enough." : "Varsler sendes via bakgrunnsjobber, så disse må forekomme ofte nok.",
+ "Send reminder notifications to calendar sharees as well" : "Send påminnelsesvarsler til kalenderdelinger også",
+ "Reminders are always sent to organizers and attendees." : "Reminders are always sent to organizers and attendees.",
"Enable notifications for events via push" : "Aktiver varsler for hendelser via push",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installer også {calendarappstoreopen} Kalender-appen {linkclose}, eller {calendardocopen} knytt datamaskinen og mobilen din for synkronisering ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Vennligst sørg for å sette opp {emailopen}e-postinnstillingene{linkclose} på riktig måte.",
"There was an error updating your attendance status." : "Det oppsto en feil under oppdateringen av oppmøtestatusen din.",
"Please contact the organizer directly." : "Ta kontakt med arrangøren direkte.",
"Are you accepting the invitation?" : "Aksepterer du invitasjonen?",
"Tentative" : "Foreløpig",
- "Comment" : "Kommentar",
- "Your attendance was updated successfully." : "Deltakelsen din ble oppdatert.",
- "Calendar and tasks" : "Kalender og oppgaver"
+ "Your attendance was updated successfully." : "Deltakelsen din ble oppdatert."
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/dav/l10n/nl.js b/apps/dav/l10n/nl.js
index 89ae8e33768..9c6d1e049e9 100644
--- a/apps/dav/l10n/nl.js
+++ b/apps/dav/l10n/nl.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Agenda",
- "Todos" : "Te doen",
+ "Tasks" : "Taken",
"Personal" : "Persoonlijk",
"{actor} created calendar {calendar}" : "{actor} creëerde agenda {calendar}",
"You created calendar {calendar}" : "Jij creëerde agenda {calendar}",
@@ -25,36 +25,41 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} deelde agenda {calendar} met groep {group}",
"You unshared calendar {calendar} from group {group}" : "Je stopte het delen van agenda {calendar} van groep {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} stopte het delen van agenda {calendar} met groep {group}",
+ "Untitled event" : "Afspraken zonder naam",
"{actor} created event {event} in calendar {calendar}" : "{actor} creëerde afspraak {event} in agenda {calendar}",
"You created event {event} in calendar {calendar}" : "Je creëerde afspraak {event} in agenda {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} verwijderde afspraak {event} uit agenda {calendar}",
"You deleted event {event} from calendar {calendar}" : "Je verwijderde afspraak {event} uit agenda {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} heeft afspraak {event} in agenda {calendar} bijgewerkt",
"You updated event {event} in calendar {calendar}" : "Je hebt afspraak {event} in agenda {calendar} bijgewerkt",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} heeft de gebeurtenis {event}verplaatst van de agenda {sourceCalendar} naar de agenda {targetCalendar}.",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Je hebt evenement {event} verplaatst van agenda {sourceCalendar} naar agenda {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} herstelde afspraak {event} in agenda {calendar}",
"You restored event {event} of calendar {calendar}" : "Je herstelde afspraak {event} in agenda {calendar}",
"Busy" : "Bezig",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creëerde taak {todo} in lijst {calendar}",
- "You created todo {todo} in list {calendar}" : "Jij creëerde taak {todo} in lijst {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} heeft de taak {todo} uit lijst {calendar} verwijderd",
- "You deleted todo {todo} from list {calendar}" : "Je verwijderde taak {todo} uit lijst {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} heeft taak {todo} bijgewerkt in lijst {calendar}",
- "You updated todo {todo} in list {calendar}" : "Je hebt taak {todo} bijgewerkt in lijst {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} heeft taak {todo} in lijst {calendar} afgewerkt",
- "You solved todo {todo} in list {calendar}" : "Je hebt taak {todo} in lijst {calendar} afgewerkt",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} heropende taak {todo} in lijst {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Je heropende taak {todo} in lijst {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} voegde de taak {todo} to aan de lijst {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Je voegde de taak {todo} toe aan de lijst {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} verwijderde de taak {todo} van de lijst {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Jij verwijderde de taak {todo} van de lijst {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} paste de taak {todo} in de lijst {calendar} aan",
+ "You updated to-do {todo} in list {calendar}" : "Jij paste de taak {todo} in de lijst {calendar} aan",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} voltooide de taak {todo} in de lijst {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Jij voltooide de taak {todo} in de lijst {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} heropende taak {todo} in lijst {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Je heropende taak {todo} in lijst {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} heeft de taak {todo} van lijst {sourceCalendar} naar lijst {targetCalendar} verplaatst",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Je verplaatste taak {todo} van lijst {sourceCalendar} naar lijst {targetCalendar}",
"Calendar, contacts and tasks" : "Agenda, contactpersonen en taken",
"A <strong>calendar</strong> was modified" : "Een <strong>agenda</strong> is aangepast",
"A calendar <strong>event</strong> was modified" : "Een agenda <strong>gebeurtenis</strong> is aangepast",
- "A calendar <strong>todo</strong> was modified" : "Een agenda <strong>Te doen</strong> was aangepast",
+ "A calendar <strong>to-do</strong> was modified" : "Een agenda <strong>to-do</strong> was aangepast",
"Contact birthdays" : "Verjaardagen",
"Death of %s" : "Sterfdatum van %s",
+ "Untitled calendar" : "Naamloze kalender",
"Calendar:" : "Agenda",
"Date:" : "Datum",
"Where:" : "Waar:",
"Description:" : "Omschrijving:",
- "Untitled event" : "Afspraken zonder naam",
"_%n year_::_%n years_" : ["%n jaar","%n jaar"],
"_%n month_::_%n months_" : ["%n maand","%n maanden"],
"_%n day_::_%n days_" : ["%n dag","%n dagen"],
@@ -67,29 +72,116 @@ OC.L10N.register(
"Description: %s" : "Beschrijving: %s",
"Where: %s" : "Waar: %s",
"%1$s via %2$s" : "%1$s via %2$s",
+ "In the past on %1$s for the entire day" : "In het verleden op %1$s gedurende de hele dag",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Over één minuut op %1$s gedurende de hele dag","Over %n minuten op %1$s gedurende de hele dag"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Over een uur op %1$s gedurende de hele dag","Over %n uur op %1$s gedurende de hele dag"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Over een dag op %1$s gedurende de hele dag","Over %n dagen op %1$s gedurende de hele dag"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Over een week op %1$s gedurende de hele dag","Over %n weken op %1$s gedurende de hele dag"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Over een maand op %1$s gedurende de hele dag","Over %n maanden op %1$s gedurende de hele dag"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Over een jaar op %1$s gedurende de hele dag","Over %n jaar op %1$s gedurende de hele dag"],
+ "In the past on %1$s between %2$s - %3$s" : "In het verleden op %1$s van %2$s tot %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Over één minuut op %1$s van %2$s tot %3$s","Over %n minuten op %1$s van %2$s tot %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Over een uur op %1$s van %2$s tot%3$s","Over %n uur op %1$s van %2$s tot %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Over één dag op %1$s van %2$s tot %3$s","Over %n dagen op %1$s van %2$s tot %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Over een week op %1$s van %2$s tot %3$s","Over %n weken op %1$s van %2$s tot %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Over een maand op %1$s van %2$s tot %3$s","Over %n maanden op %1$s van %2$s tot %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Over een jaar op %1$s van %2$s tot %3$s","Over %n jaar op %1$s van %2$s tot %3$s"],
+ "Every Day for the entire day" : "Elke dag gedurende de hele dag",
+ "Every Day for the entire day until %1$s" : "Elke dag gedurende de hele dag tot %1$s",
+ "Every Day between %1$s - %2$s" : "Elke dag van %1$s tot %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Elke dag van %1$s tot %2$s, tot %3$s ",
+ "Every %1$d Days for the entire day" : "Elke %1$d dagen gedurende de hele dag",
+ "Every %1$d Days for the entire day until %2$s" : "Elke %1$d dagen gedurende de hele dag tot %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Elke %1$d dagen van %2$s tot %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Elke %1$d dagen van %2$s tot %3$s, tot %4$s ",
+ "Every Week on %1$s for the entire day" : "Elke week op %1$s gedurende de hele dag",
+ "Every Week on %1$s for the entire day until %2$s" : "Elke week op %1$s gedurende de hele dag tot %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Elke week op %1$s van %2$s tot %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Elke week op %1$s van %2$s tot %3$s, tot %4$s ",
+ "Every %1$d Weeks on %2$s for the entire day" : "Elke %1$d weken op %2$s gedurende de hele dag",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Elke %1$d weken op %2$s gedurende de hele dag tot %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Elke %1$d weken op %2$s van %3$s tot %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Elke %1$d weken op %2$s van %3$s tot %4$s, tot %5$s",
+ "Every Month on the %1$s for the entire day" : "Elke %1$s van de maand gedurende de hele dag",
+ "Every Month on the %1$s for the entire day until %2$s" : "Elke %1$s van de maand gedurende de hele dag tot %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Elke %1$s van de maand van %2$s tot %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Elke %1$s van de maand van %2$s tot %3$s, tot %4$s ",
+ "Every %1$d Months on the %2$s for the entire day" : "Op de %2$s elke %1$d maanden gedurende de hele dag",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Op de %2$s elke %1$d maanden gedurende de hele dag tot %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Op de %2$s elke %1$d maanden van %3$s tot %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Op de %2$s elke %1$d maanden van %3$s tot %4$s tot %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Elk jaar in %1$s op de %2$s gedurende de hele dag",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Elk jaar in %1$s op de %2$s gedurende de hele dag tot %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Elk jaar in %1$s op de %2$s van %3$s tot %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Elk jaar in %1$s op de %2$s van %3$s tot %4$s, tot %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Elke %1$d jaar in %2$s op de %3$s gedurende de hele dag",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Elke %1$d jaar in %2$s op de %3$s gedurende de hele dag tot%4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Elke %1$d jaar in %2$s op de %3$s van %4$s tot %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Elke %1$d jaar in %2$s op de %3$s van %4$s tot %5$s, tot %6$s ",
+ "On specific dates for the entire day until %1$s" : "Op specifieke dagen gedurende de hele dag tot %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "Op specifieke dagen van %1$s tot %2$s, tot %3$s",
+ "In the past on %1$s" : "In het verleden op %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Over een minuut om %1$s","Over %n minuten om %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Over een uur om %1$s","Over %n uur om %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Over een dag op %1$s","Over %n dagen op %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Over een week op %1$s","Over %n weken op %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Over een maand op %1$s","Over %n maanden op %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Over een jaar op %1$s","Over %n jaar op %1$s"],
"Cancelled: %1$s" : "Geannuleerd: %1$s",
- "Invitation canceled" : "Uitnodiging geannuleerd",
+ "\"%1$s\" has been canceled" : "\"%1$s\" is geannuleerd",
"Re: %1$s" : "Re: %1$s",
- "Invitation updated" : "Uitnodiging bijgewerkt",
+ "%1$s has accepted your invitation" : "%1$s heeft uw uitnodiging aanvaard",
+ "%1$s has tentatively accepted your invitation" : "%1$s heeft je uitnodiging voorlopig geaccepteerd",
+ "%1$s has declined your invitation" : "%1$s heeft je uitnodiging afgewezen",
+ "%1$s has responded to your invitation" : "%1$s heeft op je uitnodiging gereageerd",
+ "Invitation updated: %1$s" : "Uitnodiging bijgewerkt: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s heeft de afspraak \"%2$s\" aangepast",
"Invitation: %1$s" : "Uitnodiging: %1$s",
- "Invitation" : "Uitnodiging",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s heeft je uitgenodigd voor \"%2$s\"",
+ "Organizer:" : "Organisator:",
+ "Attendees:" : "Deelnemers:",
"Title:" : "Titel:",
- "Time:" : "Tijd:",
+ "When:" : "Wanneer:",
"Location:" : "Locatie:",
"Link:" : "Link:",
- "Organizer:" : "Organisator:",
- "Attendees:" : "Deelnemers:",
+ "Occurring:" : "Elke:",
"Accept" : "Accepteren",
"Decline" : "Afwijzen",
"More options …" : "Meer opties …",
"More options at %s" : "Meer opties op %s",
+ "Monday" : "maandag",
+ "Tuesday" : "dinsdag",
+ "Wednesday" : "woensdag",
+ "Thursday" : "donderdag",
+ "Friday" : "vrijdag",
+ "Saturday" : "zaterdag",
+ "Sunday" : "zondag",
+ "January" : "Januari",
+ "February" : "Februari",
+ "March" : "Maart",
+ "April" : "April",
+ "May" : "Mei",
+ "June" : "Juni",
+ "July" : "Juli",
+ "August" : "Augustus",
+ "September" : "September",
+ "October" : "Oktober",
+ "November" : "November",
+ "December" : "December",
+ "First" : "Eerste",
+ "Second" : "Tweede",
+ "Third" : "Derde",
+ "Fourth" : "Vierde",
+ "Fifth" : "Vijfde",
+ "Last" : "Laatste",
+ "Second Last" : "Voorlaatste",
"Contacts" : "Contactpersonen",
"{actor} created address book {addressbook}" : "{actor} creëerde adresboek {addressbook}",
- "You created address book {addressbook}" : "Je creëere adresboek {addressbook}",
+ "You created address book {addressbook}" : "Je creëerde adresboek {addressbook}",
"{actor} deleted address book {addressbook}" : "{actor} verwijdede adresboek {addressbook}",
"You deleted address book {addressbook}" : "Je verwijderde adresboek {addressbook}",
"{actor} updated address book {addressbook}" : "{actor} wijzigde adresboek {addressbook}",
- "You updated address book {addressbook}" : "Je wijzigde adresboek {addressbook}",
+ "You updated address book {addressbook}" : "Je wijzigde adresboek {addressbook}",
"{actor} shared address book {addressbook} with you" : "{actor} deelde adresboek {addressbook} met jou",
"You shared address book {addressbook} with {user}" : "Jij deelde adresboek {addressbook} met {user}",
"{actor} shared address book {addressbook} with {user}" : "{actor} deelde adresboek {addressbook} met {user}",
@@ -97,7 +189,7 @@ OC.L10N.register(
"You unshared address book {addressbook} from {user}" : "Je stopte met delen adresboek {addressbook} van {user}",
"{actor} unshared address book {addressbook} from {user}" : " stopte met delen adresboek {addressbook} van {user}",
"{actor} unshared address book {addressbook} from themselves" : "{actor} stopte het delen van het adresboek {addressbook} van zichzelf",
- "You shared address book {addressbook} with group {group}" : "Je deelde adresboek {addressbook } met groep {group}",
+ "You shared address book {addressbook} with group {group}" : "Je deelde adresboek {addressbook} met groep {group}",
"{actor} shared address book {addressbook} with group {group}" : "{actor} deelde adresboek {addressbook} met groep {group}",
"You unshared address book {addressbook} from group {group}" : "Je stopte het delen van adresboek {addressbook} van groep {group}",
"{actor} unshared address book {addressbook} from group {group}" : "{actor} stopte het delen van adresboek {addressbook} van groep {group}",
@@ -107,36 +199,65 @@ OC.L10N.register(
"You deleted contact {card} from address book {addressbook}" : "Je verwijderde contact {card} uit adresboek {addressbook}",
"{actor} updated contact {card} in address book {addressbook}" : "{actor} wijzigde contact {card} in adresboek {addressbook}",
"You updated contact {card} in address book {addressbook}" : "Je wijzigde contact {card} in adresboek {addressbook}",
- "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Een <strong>contact</strong> uit adresboek </strong> is gewijzigd",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Een <strong>contactpersoon</strong> of <strong>adresboek</strong> is gewijzigd",
+ "Accounts" : "Accounts",
+ "System address book which holds all accounts" : "Systeem-adresboek met daarin alle accounts",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "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." : "Verwachte bestandsgrootte van %1$s maar gelezen (van Nextcloud-client) en geschreven (naar Nextcloud-opslag) %2$s. Dit kan een netwerkprobleem zijn aan de verzendende kant of een probleem met het schrijven naar de opslag aan de serverkant.",
+ "Could not rename part file to final file, canceled by hook" : "Kon bestandsdeel niet hernoemen naar definitief bestand, geannuleerd door hook",
+ "Could not rename part file to final file" : "Kon bestandsdeel niet hernoemen naar definitief bestand",
+ "Failed to check file size: %1$s" : "Kon bestandsomvang niet controleren: %1$s",
+ "Encryption not ready: %1$s" : "Versleuteling niet gereed: %1$s",
+ "Failed to open file: %1$s" : "Kon het bestand %1$s niet openen",
+ "Failed to unlink: %1$s" : "Kon link niet verwijderen: %1$s",
+ "Failed to write file contents: %1$s" : "Kon bestandsinhoud niet wegschrijven: %1$s",
+ "File not found: %1$s" : "Bestand niet gevonden: %1$s",
"System is in maintenance mode." : "Systeem in onderhoudsmodus.",
"Upgrade needed" : "Upgrade vereist",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Je %s moet worden geconfigureerd voor gebruik van HTTPS om CalDAV en CardDAV met iOS/macOS te kunnen gebruiken.",
"Configures a CalDAV account" : "Configureert een CalDAV account",
"Configures a CardDAV account" : "Configureert een CardDAV account",
"Events" : "Evenementen",
- "Tasks" : "Taken",
"Untitled task" : "Taak zonder titel",
"Completed on %s" : "Voltooid op %s",
"Due on %s by %s" : "Verwacht op %s door %s",
"Due on %s" : "Verwacht op %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Welkom bij Nextcloud Calendar!\n\nDit is een voorbeeldafspraak - ontdek de flexibiliteit van plannen met Nextcloud Calendar door elke aanpassing te maken die je maar wilt!\n\nMet Nextcloud Calendar kun je:\n- Moeiteloos afspraken maken, bewerken en beheren.\n- Meerdere kalenders maken en delen met teamgenoten, vrienden of familie.\n- Beschikbaarheid controleren en aan anderen laten zien wanneer je al bezet bent.\n- Naadloos integreren met apps en apparaten via CalDAV.\n- Je ervaring aanpassen: terugkerende afspraken plannen, meldingen en andere instellingen aanpassen.",
+ "Example event - open me!" : "Voorbeeldafspraak - open me!",
+ "System Address Book" : "Systeem-adresboek",
+ "The system address book contains contact information for all users in your instance." : "Het systeem-adresboek bevat contactgegevens van alle gebruikers op jouw server.",
+ "Enable System Address Book" : "Systeem-adresboek inschakelen",
+ "WebDAV endpoint" : "WebDAV eindpunt",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Je webserver is nog niet goed ingesteld voor bestandssynchronisatie, omdat de WebDAV interface niet goed lijkt te werken.",
"Contacts and groups" : "Contactpersonen en groepen",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV eindpunt",
- "Availability" : "Beschikbaarheid",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Als je je werkuren instelt kunnen andere gebruikers zien wanneer je niet beschikbaar bent als ze een meeting willen plannen.",
+ "Out of office replacement (optional)" : "Vervanger bij afwezigheid (optioneel)",
+ "Name of the replacement" : "Naam van de vervanger",
+ "No results." : "Geen resultaten.",
+ "Start typing." : "Start met typen.",
+ "Short absence status" : "Korte afwezigheidsstatus",
+ "Long absence Message" : "Lang afwezigheidsbericht",
+ "Save" : "Opslaan",
+ "Disable absence" : "Afwezigheid uitschakelen",
+ "Failed to load availability" : "Kon beschikbaarheid niet laden",
+ "Saved availability" : "Beschikbaarheid opgeslagen",
+ "Failed to save availability" : "Opslaan beschikbaarheid mislukt",
"Time zone:" : "Tijdzone:",
"to" : "aan",
"Delete slot" : "Verwijder slot",
"No working hours set" : "Geen werkuren ingesteld",
"Add slot" : "Voeg slot toe",
- "Monday" : "maandag",
- "Tuesday" : "dinsdag",
- "Wednesday" : "woensdag",
- "Thursday" : "donderdag",
- "Friday" : "vrijdag",
- "Saturday" : "zaterdag",
- "Sunday" : "zondag",
- "Save" : "Opslaan",
+ "Weekdays" : "Weekdagen",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Stel de gebruikersstatus automatisch in op \"Niet storen\" buiten de beschikbaarheid om alle meldingen te dempen.",
+ "Cancel" : "Annuleren",
+ "Import" : "Import",
+ "Error while saving settings" : "Probleem tijdens het opslaan van instellingen",
+ "Reset to default" : "Herstellen naar standaard",
+ "Availability" : "Beschikbaarheid",
+ "Absence" : "Afwezigheid",
+ "Configure your next absence period." : "Configureer uw volgende afwezigheidsperiode.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installeer ook de {calendarappstoreopen}Agenda app{linkclose}, of {calendardocopen}verbind je desktop & mobiel voor synchronisatie ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Zorg ervoor dat je de {emailopen}de e-mailserver{linkclose} correct instelt.",
"Calendar server" : "Agendaserver",
"Send invitations to attendees" : "Verzend uitnodigingen naar deelnemers",
"Automatically generate a birthday calendar" : "Genereer verjaardagskalender automatisch",
@@ -145,15 +266,10 @@ OC.L10N.register(
"Send notifications for events" : "Versturen meldingen voor afspraken",
"Notifications are sent via background jobs, so these must occur often enough." : "Meldingen worden via achtergrondtaken verstuurd, dus die moeten vaak genoeg plaatsvinden.",
"Enable notifications for events via push" : "Inschakelen push-melding voor afspraken",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installeer ook de {calendarappstoreopen}Agenda app{linkclose}, of {calendardocopen}verbind je desktop & mobiel voor synchronisatie ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Zorg ervoor dat je de {emailopen}de e-mailserver{linkclose} correct instelt.",
"There was an error updating your attendance status." : "Er trad een fout op bij het bijwerken van je deelnamestatus.",
"Please contact the organizer directly." : "Neem rechtstreeks contact op met de organisator.",
"Are you accepting the invitation?" : "Neem je de uitnodiging aan?",
"Tentative" : "Onder voorbehoud",
- "Number of guests" : "Aantal gasten",
- "Comment" : "Notitie",
- "Your attendance was updated successfully." : "Je deelname is succesvol bijgewerkt.",
- "Calendar and tasks" : "Agenda en taken"
+ "Your attendance was updated successfully." : "Je deelname is succesvol bijgewerkt."
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/nl.json b/apps/dav/l10n/nl.json
index dbf1469b8f0..5886b756d62 100644
--- a/apps/dav/l10n/nl.json
+++ b/apps/dav/l10n/nl.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Agenda",
- "Todos" : "Te doen",
+ "Tasks" : "Taken",
"Personal" : "Persoonlijk",
"{actor} created calendar {calendar}" : "{actor} creëerde agenda {calendar}",
"You created calendar {calendar}" : "Jij creëerde agenda {calendar}",
@@ -23,36 +23,41 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} deelde agenda {calendar} met groep {group}",
"You unshared calendar {calendar} from group {group}" : "Je stopte het delen van agenda {calendar} van groep {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} stopte het delen van agenda {calendar} met groep {group}",
+ "Untitled event" : "Afspraken zonder naam",
"{actor} created event {event} in calendar {calendar}" : "{actor} creëerde afspraak {event} in agenda {calendar}",
"You created event {event} in calendar {calendar}" : "Je creëerde afspraak {event} in agenda {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} verwijderde afspraak {event} uit agenda {calendar}",
"You deleted event {event} from calendar {calendar}" : "Je verwijderde afspraak {event} uit agenda {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} heeft afspraak {event} in agenda {calendar} bijgewerkt",
"You updated event {event} in calendar {calendar}" : "Je hebt afspraak {event} in agenda {calendar} bijgewerkt",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} heeft de gebeurtenis {event}verplaatst van de agenda {sourceCalendar} naar de agenda {targetCalendar}.",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Je hebt evenement {event} verplaatst van agenda {sourceCalendar} naar agenda {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} herstelde afspraak {event} in agenda {calendar}",
"You restored event {event} of calendar {calendar}" : "Je herstelde afspraak {event} in agenda {calendar}",
"Busy" : "Bezig",
- "{actor} created todo {todo} in list {calendar}" : "{actor} creëerde taak {todo} in lijst {calendar}",
- "You created todo {todo} in list {calendar}" : "Jij creëerde taak {todo} in lijst {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} heeft de taak {todo} uit lijst {calendar} verwijderd",
- "You deleted todo {todo} from list {calendar}" : "Je verwijderde taak {todo} uit lijst {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} heeft taak {todo} bijgewerkt in lijst {calendar}",
- "You updated todo {todo} in list {calendar}" : "Je hebt taak {todo} bijgewerkt in lijst {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} heeft taak {todo} in lijst {calendar} afgewerkt",
- "You solved todo {todo} in list {calendar}" : "Je hebt taak {todo} in lijst {calendar} afgewerkt",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} heropende taak {todo} in lijst {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Je heropende taak {todo} in lijst {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} voegde de taak {todo} to aan de lijst {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Je voegde de taak {todo} toe aan de lijst {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} verwijderde de taak {todo} van de lijst {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Jij verwijderde de taak {todo} van de lijst {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} paste de taak {todo} in de lijst {calendar} aan",
+ "You updated to-do {todo} in list {calendar}" : "Jij paste de taak {todo} in de lijst {calendar} aan",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} voltooide de taak {todo} in de lijst {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Jij voltooide de taak {todo} in de lijst {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} heropende taak {todo} in lijst {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Je heropende taak {todo} in lijst {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} heeft de taak {todo} van lijst {sourceCalendar} naar lijst {targetCalendar} verplaatst",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Je verplaatste taak {todo} van lijst {sourceCalendar} naar lijst {targetCalendar}",
"Calendar, contacts and tasks" : "Agenda, contactpersonen en taken",
"A <strong>calendar</strong> was modified" : "Een <strong>agenda</strong> is aangepast",
"A calendar <strong>event</strong> was modified" : "Een agenda <strong>gebeurtenis</strong> is aangepast",
- "A calendar <strong>todo</strong> was modified" : "Een agenda <strong>Te doen</strong> was aangepast",
+ "A calendar <strong>to-do</strong> was modified" : "Een agenda <strong>to-do</strong> was aangepast",
"Contact birthdays" : "Verjaardagen",
"Death of %s" : "Sterfdatum van %s",
+ "Untitled calendar" : "Naamloze kalender",
"Calendar:" : "Agenda",
"Date:" : "Datum",
"Where:" : "Waar:",
"Description:" : "Omschrijving:",
- "Untitled event" : "Afspraken zonder naam",
"_%n year_::_%n years_" : ["%n jaar","%n jaar"],
"_%n month_::_%n months_" : ["%n maand","%n maanden"],
"_%n day_::_%n days_" : ["%n dag","%n dagen"],
@@ -65,29 +70,116 @@
"Description: %s" : "Beschrijving: %s",
"Where: %s" : "Waar: %s",
"%1$s via %2$s" : "%1$s via %2$s",
+ "In the past on %1$s for the entire day" : "In het verleden op %1$s gedurende de hele dag",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Over één minuut op %1$s gedurende de hele dag","Over %n minuten op %1$s gedurende de hele dag"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Over een uur op %1$s gedurende de hele dag","Over %n uur op %1$s gedurende de hele dag"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Over een dag op %1$s gedurende de hele dag","Over %n dagen op %1$s gedurende de hele dag"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Over een week op %1$s gedurende de hele dag","Over %n weken op %1$s gedurende de hele dag"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Over een maand op %1$s gedurende de hele dag","Over %n maanden op %1$s gedurende de hele dag"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Over een jaar op %1$s gedurende de hele dag","Over %n jaar op %1$s gedurende de hele dag"],
+ "In the past on %1$s between %2$s - %3$s" : "In het verleden op %1$s van %2$s tot %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Over één minuut op %1$s van %2$s tot %3$s","Over %n minuten op %1$s van %2$s tot %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Over een uur op %1$s van %2$s tot%3$s","Over %n uur op %1$s van %2$s tot %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Over één dag op %1$s van %2$s tot %3$s","Over %n dagen op %1$s van %2$s tot %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Over een week op %1$s van %2$s tot %3$s","Over %n weken op %1$s van %2$s tot %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Over een maand op %1$s van %2$s tot %3$s","Over %n maanden op %1$s van %2$s tot %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Over een jaar op %1$s van %2$s tot %3$s","Over %n jaar op %1$s van %2$s tot %3$s"],
+ "Every Day for the entire day" : "Elke dag gedurende de hele dag",
+ "Every Day for the entire day until %1$s" : "Elke dag gedurende de hele dag tot %1$s",
+ "Every Day between %1$s - %2$s" : "Elke dag van %1$s tot %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Elke dag van %1$s tot %2$s, tot %3$s ",
+ "Every %1$d Days for the entire day" : "Elke %1$d dagen gedurende de hele dag",
+ "Every %1$d Days for the entire day until %2$s" : "Elke %1$d dagen gedurende de hele dag tot %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Elke %1$d dagen van %2$s tot %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Elke %1$d dagen van %2$s tot %3$s, tot %4$s ",
+ "Every Week on %1$s for the entire day" : "Elke week op %1$s gedurende de hele dag",
+ "Every Week on %1$s for the entire day until %2$s" : "Elke week op %1$s gedurende de hele dag tot %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Elke week op %1$s van %2$s tot %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Elke week op %1$s van %2$s tot %3$s, tot %4$s ",
+ "Every %1$d Weeks on %2$s for the entire day" : "Elke %1$d weken op %2$s gedurende de hele dag",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Elke %1$d weken op %2$s gedurende de hele dag tot %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Elke %1$d weken op %2$s van %3$s tot %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Elke %1$d weken op %2$s van %3$s tot %4$s, tot %5$s",
+ "Every Month on the %1$s for the entire day" : "Elke %1$s van de maand gedurende de hele dag",
+ "Every Month on the %1$s for the entire day until %2$s" : "Elke %1$s van de maand gedurende de hele dag tot %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Elke %1$s van de maand van %2$s tot %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Elke %1$s van de maand van %2$s tot %3$s, tot %4$s ",
+ "Every %1$d Months on the %2$s for the entire day" : "Op de %2$s elke %1$d maanden gedurende de hele dag",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Op de %2$s elke %1$d maanden gedurende de hele dag tot %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Op de %2$s elke %1$d maanden van %3$s tot %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Op de %2$s elke %1$d maanden van %3$s tot %4$s tot %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Elk jaar in %1$s op de %2$s gedurende de hele dag",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Elk jaar in %1$s op de %2$s gedurende de hele dag tot %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Elk jaar in %1$s op de %2$s van %3$s tot %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Elk jaar in %1$s op de %2$s van %3$s tot %4$s, tot %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Elke %1$d jaar in %2$s op de %3$s gedurende de hele dag",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Elke %1$d jaar in %2$s op de %3$s gedurende de hele dag tot%4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Elke %1$d jaar in %2$s op de %3$s van %4$s tot %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Elke %1$d jaar in %2$s op de %3$s van %4$s tot %5$s, tot %6$s ",
+ "On specific dates for the entire day until %1$s" : "Op specifieke dagen gedurende de hele dag tot %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "Op specifieke dagen van %1$s tot %2$s, tot %3$s",
+ "In the past on %1$s" : "In het verleden op %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Over een minuut om %1$s","Over %n minuten om %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Over een uur om %1$s","Over %n uur om %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Over een dag op %1$s","Over %n dagen op %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Over een week op %1$s","Over %n weken op %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Over een maand op %1$s","Over %n maanden op %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Over een jaar op %1$s","Over %n jaar op %1$s"],
"Cancelled: %1$s" : "Geannuleerd: %1$s",
- "Invitation canceled" : "Uitnodiging geannuleerd",
+ "\"%1$s\" has been canceled" : "\"%1$s\" is geannuleerd",
"Re: %1$s" : "Re: %1$s",
- "Invitation updated" : "Uitnodiging bijgewerkt",
+ "%1$s has accepted your invitation" : "%1$s heeft uw uitnodiging aanvaard",
+ "%1$s has tentatively accepted your invitation" : "%1$s heeft je uitnodiging voorlopig geaccepteerd",
+ "%1$s has declined your invitation" : "%1$s heeft je uitnodiging afgewezen",
+ "%1$s has responded to your invitation" : "%1$s heeft op je uitnodiging gereageerd",
+ "Invitation updated: %1$s" : "Uitnodiging bijgewerkt: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s heeft de afspraak \"%2$s\" aangepast",
"Invitation: %1$s" : "Uitnodiging: %1$s",
- "Invitation" : "Uitnodiging",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s heeft je uitgenodigd voor \"%2$s\"",
+ "Organizer:" : "Organisator:",
+ "Attendees:" : "Deelnemers:",
"Title:" : "Titel:",
- "Time:" : "Tijd:",
+ "When:" : "Wanneer:",
"Location:" : "Locatie:",
"Link:" : "Link:",
- "Organizer:" : "Organisator:",
- "Attendees:" : "Deelnemers:",
+ "Occurring:" : "Elke:",
"Accept" : "Accepteren",
"Decline" : "Afwijzen",
"More options …" : "Meer opties …",
"More options at %s" : "Meer opties op %s",
+ "Monday" : "maandag",
+ "Tuesday" : "dinsdag",
+ "Wednesday" : "woensdag",
+ "Thursday" : "donderdag",
+ "Friday" : "vrijdag",
+ "Saturday" : "zaterdag",
+ "Sunday" : "zondag",
+ "January" : "Januari",
+ "February" : "Februari",
+ "March" : "Maart",
+ "April" : "April",
+ "May" : "Mei",
+ "June" : "Juni",
+ "July" : "Juli",
+ "August" : "Augustus",
+ "September" : "September",
+ "October" : "Oktober",
+ "November" : "November",
+ "December" : "December",
+ "First" : "Eerste",
+ "Second" : "Tweede",
+ "Third" : "Derde",
+ "Fourth" : "Vierde",
+ "Fifth" : "Vijfde",
+ "Last" : "Laatste",
+ "Second Last" : "Voorlaatste",
"Contacts" : "Contactpersonen",
"{actor} created address book {addressbook}" : "{actor} creëerde adresboek {addressbook}",
- "You created address book {addressbook}" : "Je creëere adresboek {addressbook}",
+ "You created address book {addressbook}" : "Je creëerde adresboek {addressbook}",
"{actor} deleted address book {addressbook}" : "{actor} verwijdede adresboek {addressbook}",
"You deleted address book {addressbook}" : "Je verwijderde adresboek {addressbook}",
"{actor} updated address book {addressbook}" : "{actor} wijzigde adresboek {addressbook}",
- "You updated address book {addressbook}" : "Je wijzigde adresboek {addressbook}",
+ "You updated address book {addressbook}" : "Je wijzigde adresboek {addressbook}",
"{actor} shared address book {addressbook} with you" : "{actor} deelde adresboek {addressbook} met jou",
"You shared address book {addressbook} with {user}" : "Jij deelde adresboek {addressbook} met {user}",
"{actor} shared address book {addressbook} with {user}" : "{actor} deelde adresboek {addressbook} met {user}",
@@ -95,7 +187,7 @@
"You unshared address book {addressbook} from {user}" : "Je stopte met delen adresboek {addressbook} van {user}",
"{actor} unshared address book {addressbook} from {user}" : " stopte met delen adresboek {addressbook} van {user}",
"{actor} unshared address book {addressbook} from themselves" : "{actor} stopte het delen van het adresboek {addressbook} van zichzelf",
- "You shared address book {addressbook} with group {group}" : "Je deelde adresboek {addressbook } met groep {group}",
+ "You shared address book {addressbook} with group {group}" : "Je deelde adresboek {addressbook} met groep {group}",
"{actor} shared address book {addressbook} with group {group}" : "{actor} deelde adresboek {addressbook} met groep {group}",
"You unshared address book {addressbook} from group {group}" : "Je stopte het delen van adresboek {addressbook} van groep {group}",
"{actor} unshared address book {addressbook} from group {group}" : "{actor} stopte het delen van adresboek {addressbook} van groep {group}",
@@ -105,36 +197,65 @@
"You deleted contact {card} from address book {addressbook}" : "Je verwijderde contact {card} uit adresboek {addressbook}",
"{actor} updated contact {card} in address book {addressbook}" : "{actor} wijzigde contact {card} in adresboek {addressbook}",
"You updated contact {card} in address book {addressbook}" : "Je wijzigde contact {card} in adresboek {addressbook}",
- "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Een <strong>contact</strong> uit adresboek </strong> is gewijzigd",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Een <strong>contactpersoon</strong> of <strong>adresboek</strong> is gewijzigd",
+ "Accounts" : "Accounts",
+ "System address book which holds all accounts" : "Systeem-adresboek met daarin alle accounts",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "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." : "Verwachte bestandsgrootte van %1$s maar gelezen (van Nextcloud-client) en geschreven (naar Nextcloud-opslag) %2$s. Dit kan een netwerkprobleem zijn aan de verzendende kant of een probleem met het schrijven naar de opslag aan de serverkant.",
+ "Could not rename part file to final file, canceled by hook" : "Kon bestandsdeel niet hernoemen naar definitief bestand, geannuleerd door hook",
+ "Could not rename part file to final file" : "Kon bestandsdeel niet hernoemen naar definitief bestand",
+ "Failed to check file size: %1$s" : "Kon bestandsomvang niet controleren: %1$s",
+ "Encryption not ready: %1$s" : "Versleuteling niet gereed: %1$s",
+ "Failed to open file: %1$s" : "Kon het bestand %1$s niet openen",
+ "Failed to unlink: %1$s" : "Kon link niet verwijderen: %1$s",
+ "Failed to write file contents: %1$s" : "Kon bestandsinhoud niet wegschrijven: %1$s",
+ "File not found: %1$s" : "Bestand niet gevonden: %1$s",
"System is in maintenance mode." : "Systeem in onderhoudsmodus.",
"Upgrade needed" : "Upgrade vereist",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Je %s moet worden geconfigureerd voor gebruik van HTTPS om CalDAV en CardDAV met iOS/macOS te kunnen gebruiken.",
"Configures a CalDAV account" : "Configureert een CalDAV account",
"Configures a CardDAV account" : "Configureert een CardDAV account",
"Events" : "Evenementen",
- "Tasks" : "Taken",
"Untitled task" : "Taak zonder titel",
"Completed on %s" : "Voltooid op %s",
"Due on %s by %s" : "Verwacht op %s door %s",
"Due on %s" : "Verwacht op %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Welkom bij Nextcloud Calendar!\n\nDit is een voorbeeldafspraak - ontdek de flexibiliteit van plannen met Nextcloud Calendar door elke aanpassing te maken die je maar wilt!\n\nMet Nextcloud Calendar kun je:\n- Moeiteloos afspraken maken, bewerken en beheren.\n- Meerdere kalenders maken en delen met teamgenoten, vrienden of familie.\n- Beschikbaarheid controleren en aan anderen laten zien wanneer je al bezet bent.\n- Naadloos integreren met apps en apparaten via CalDAV.\n- Je ervaring aanpassen: terugkerende afspraken plannen, meldingen en andere instellingen aanpassen.",
+ "Example event - open me!" : "Voorbeeldafspraak - open me!",
+ "System Address Book" : "Systeem-adresboek",
+ "The system address book contains contact information for all users in your instance." : "Het systeem-adresboek bevat contactgegevens van alle gebruikers op jouw server.",
+ "Enable System Address Book" : "Systeem-adresboek inschakelen",
+ "WebDAV endpoint" : "WebDAV eindpunt",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Je webserver is nog niet goed ingesteld voor bestandssynchronisatie, omdat de WebDAV interface niet goed lijkt te werken.",
"Contacts and groups" : "Contactpersonen en groepen",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV eindpunt",
- "Availability" : "Beschikbaarheid",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Als je je werkuren instelt kunnen andere gebruikers zien wanneer je niet beschikbaar bent als ze een meeting willen plannen.",
+ "Out of office replacement (optional)" : "Vervanger bij afwezigheid (optioneel)",
+ "Name of the replacement" : "Naam van de vervanger",
+ "No results." : "Geen resultaten.",
+ "Start typing." : "Start met typen.",
+ "Short absence status" : "Korte afwezigheidsstatus",
+ "Long absence Message" : "Lang afwezigheidsbericht",
+ "Save" : "Opslaan",
+ "Disable absence" : "Afwezigheid uitschakelen",
+ "Failed to load availability" : "Kon beschikbaarheid niet laden",
+ "Saved availability" : "Beschikbaarheid opgeslagen",
+ "Failed to save availability" : "Opslaan beschikbaarheid mislukt",
"Time zone:" : "Tijdzone:",
"to" : "aan",
"Delete slot" : "Verwijder slot",
"No working hours set" : "Geen werkuren ingesteld",
"Add slot" : "Voeg slot toe",
- "Monday" : "maandag",
- "Tuesday" : "dinsdag",
- "Wednesday" : "woensdag",
- "Thursday" : "donderdag",
- "Friday" : "vrijdag",
- "Saturday" : "zaterdag",
- "Sunday" : "zondag",
- "Save" : "Opslaan",
+ "Weekdays" : "Weekdagen",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Stel de gebruikersstatus automatisch in op \"Niet storen\" buiten de beschikbaarheid om alle meldingen te dempen.",
+ "Cancel" : "Annuleren",
+ "Import" : "Import",
+ "Error while saving settings" : "Probleem tijdens het opslaan van instellingen",
+ "Reset to default" : "Herstellen naar standaard",
+ "Availability" : "Beschikbaarheid",
+ "Absence" : "Afwezigheid",
+ "Configure your next absence period." : "Configureer uw volgende afwezigheidsperiode.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installeer ook de {calendarappstoreopen}Agenda app{linkclose}, of {calendardocopen}verbind je desktop & mobiel voor synchronisatie ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Zorg ervoor dat je de {emailopen}de e-mailserver{linkclose} correct instelt.",
"Calendar server" : "Agendaserver",
"Send invitations to attendees" : "Verzend uitnodigingen naar deelnemers",
"Automatically generate a birthday calendar" : "Genereer verjaardagskalender automatisch",
@@ -143,15 +264,10 @@
"Send notifications for events" : "Versturen meldingen voor afspraken",
"Notifications are sent via background jobs, so these must occur often enough." : "Meldingen worden via achtergrondtaken verstuurd, dus die moeten vaak genoeg plaatsvinden.",
"Enable notifications for events via push" : "Inschakelen push-melding voor afspraken",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installeer ook de {calendarappstoreopen}Agenda app{linkclose}, of {calendardocopen}verbind je desktop & mobiel voor synchronisatie ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Zorg ervoor dat je de {emailopen}de e-mailserver{linkclose} correct instelt.",
"There was an error updating your attendance status." : "Er trad een fout op bij het bijwerken van je deelnamestatus.",
"Please contact the organizer directly." : "Neem rechtstreeks contact op met de organisator.",
"Are you accepting the invitation?" : "Neem je de uitnodiging aan?",
"Tentative" : "Onder voorbehoud",
- "Number of guests" : "Aantal gasten",
- "Comment" : "Notitie",
- "Your attendance was updated successfully." : "Je deelname is succesvol bijgewerkt.",
- "Calendar and tasks" : "Agenda en taken"
+ "Your attendance was updated successfully." : "Je deelname is succesvol bijgewerkt."
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/dav/l10n/nn_NO.js b/apps/dav/l10n/nn_NO.js
deleted file mode 100644
index 5dee7e46749..00000000000
--- a/apps/dav/l10n/nn_NO.js
+++ /dev/null
@@ -1,43 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Kalendar",
- "Todos" : "Å gjere",
- "Personal" : "Personleg",
- "{actor} created calendar {calendar}" : "{actor} lagde kalendaren {calendar}",
- "You created calendar {calendar}" : "Du lagde kalendaren {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} sletta kalendaren {calendar}",
- "You deleted calendar {calendar}" : "Du sletta kalendaren {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} oppdaterte kalendaren {calendar}",
- "You updated calendar {calendar}" : "Du oppdaterte kalendaren {calendar}",
- "You shared calendar {calendar} as public link" : "Du delte kalendaren {calendar} som en offentleg lenke",
- "You removed public link for calendar {calendar}" : "Du fjerna den offentlege lenka for kalendaren {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} delte kalendaren {calendar} med deg",
- "You shared calendar {calendar} with {user}" : "Du delte kalendaren {calendar} med {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} delte kalendaren {calendar} med {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} stoppa å dele kalendaren {calendar} med deg",
- "You unshared calendar {calendar} from {user}" : "Du stoppa å dele kalendaren {calendar} med {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} stoppa å dele kalendaren {calendar} med {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} stoppa å dele kalendaren {calendar} med segsjølv",
- "You shared calendar {calendar} with group {group}" : "Du delte kalendaren {calendar} med gruppa {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} delte kalendaren {calendar} med gruppa {group}",
- "You unshared calendar {calendar} from group {group}" : "Du stoppa å dele {calendar} med gruppa {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} stoppa å dele kalendaren {calendar} med gruppa {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} oppretta ein hending {event} i kalendaren {calendar}",
- "You created event {event} in calendar {calendar}" : "Du oppretta ei hending {event} i kalendaren {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} seltta ei hending {event} frå kalendaren {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Du sletta ei hending {event} frå kalendaren {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} oppdaterte hendinga {event} i kalendaren {calendar}",
- "You updated event {event} in calendar {calendar}" : "Du oppdaterte hendinga {event} i kalendaren {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} oppretta å gjere {todo} i lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Du oppretta å gjere {todo} i lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} sletta å gjere {todo} frå lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Du sletta å gjere {todo} frå lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} oppdaterte å gjere {todo} i lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Du oppdaterte å gjere {todo} i lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} løyste å gjere {todo} i lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Du løyste å gjere {todo} i lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} igjen-opna å gjere {todo} i lista {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Du igjen-opna å gjere {todo} i lista {calendar}"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/nn_NO.json b/apps/dav/l10n/nn_NO.json
deleted file mode 100644
index 3323f9f7088..00000000000
--- a/apps/dav/l10n/nn_NO.json
+++ /dev/null
@@ -1,41 +0,0 @@
-{ "translations": {
- "Calendar" : "Kalendar",
- "Todos" : "Å gjere",
- "Personal" : "Personleg",
- "{actor} created calendar {calendar}" : "{actor} lagde kalendaren {calendar}",
- "You created calendar {calendar}" : "Du lagde kalendaren {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} sletta kalendaren {calendar}",
- "You deleted calendar {calendar}" : "Du sletta kalendaren {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} oppdaterte kalendaren {calendar}",
- "You updated calendar {calendar}" : "Du oppdaterte kalendaren {calendar}",
- "You shared calendar {calendar} as public link" : "Du delte kalendaren {calendar} som en offentleg lenke",
- "You removed public link for calendar {calendar}" : "Du fjerna den offentlege lenka for kalendaren {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} delte kalendaren {calendar} med deg",
- "You shared calendar {calendar} with {user}" : "Du delte kalendaren {calendar} med {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} delte kalendaren {calendar} med {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} stoppa å dele kalendaren {calendar} med deg",
- "You unshared calendar {calendar} from {user}" : "Du stoppa å dele kalendaren {calendar} med {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} stoppa å dele kalendaren {calendar} med {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} stoppa å dele kalendaren {calendar} med segsjølv",
- "You shared calendar {calendar} with group {group}" : "Du delte kalendaren {calendar} med gruppa {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} delte kalendaren {calendar} med gruppa {group}",
- "You unshared calendar {calendar} from group {group}" : "Du stoppa å dele {calendar} med gruppa {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} stoppa å dele kalendaren {calendar} med gruppa {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} oppretta ein hending {event} i kalendaren {calendar}",
- "You created event {event} in calendar {calendar}" : "Du oppretta ei hending {event} i kalendaren {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} seltta ei hending {event} frå kalendaren {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Du sletta ei hending {event} frå kalendaren {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} oppdaterte hendinga {event} i kalendaren {calendar}",
- "You updated event {event} in calendar {calendar}" : "Du oppdaterte hendinga {event} i kalendaren {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} oppretta å gjere {todo} i lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Du oppretta å gjere {todo} i lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} sletta å gjere {todo} frå lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Du sletta å gjere {todo} frå lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} oppdaterte å gjere {todo} i lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Du oppdaterte å gjere {todo} i lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} løyste å gjere {todo} i lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Du løyste å gjere {todo} i lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} igjen-opna å gjere {todo} i lista {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Du igjen-opna å gjere {todo} i lista {calendar}"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/pl.js b/apps/dav/l10n/pl.js
index e9fd91268fc..3c9929cc943 100644
--- a/apps/dav/l10n/pl.js
+++ b/apps/dav/l10n/pl.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Kalendarz",
- "Todos" : "Zadania",
+ "Tasks" : "Zadania",
"Personal" : "Osobiste",
"{actor} created calendar {calendar}" : "{actor} utworzył kalendarz {calendar}",
"You created calendar {calendar}" : "Utworzyłeś kalendarz {calendar}",
@@ -25,36 +25,41 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} udostępnił kalendarz {calendar} dla grupy {group}",
"You unshared calendar {calendar} from group {group}" : "Zakończyłeś udostępnianie kalendarza {calendar} dla grupy {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} zakończył udostępnianie kalendarza {calendar} dla grupy {group} .",
+ "Untitled event" : "Wydarzenie bez tytułu",
"{actor} created event {event} in calendar {calendar}" : "{actor} utworzył wydarzenie {event} w kalendarzu {calendar}",
"You created event {event} in calendar {calendar}" : "Utworzyłeś wydarzenie {event} w kalendarzu {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} usunął wydarzenie {event} z kalendarza {calendar}",
"You deleted event {event} from calendar {calendar}" : "Usunąłeś wydarzenie {event} z kalendarza {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} zaktualizował wydarzenie {event} z kalendarza {calendar}",
"You updated event {event} in calendar {calendar}" : "Zaktualizowałeś wydarzenie {event} w kalendarzu {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} przeniósł wydarzenie {event} z kalendarza {sourceCalendar} do kalendarza {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Przeniosłeś wydarzenie {event} z kalendarza {sourceCalendar} do kalendarza {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} przywrócił wydarzenie {event} z kalendarza {calendar}",
"You restored event {event} of calendar {calendar}" : "Przywróciłeś wydarzenie {event} z kalendarza {calendar}",
- "Busy" : "Czekaj",
- "{actor} created todo {todo} in list {calendar}" : "{actor} utworzył zadanie {todo} na liście {calendar}",
- "You created todo {todo} in list {calendar}" : "Utworzyłeś zadanie {todo} na liście {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} usunął zadanie {todo} z listy {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Usunąłeś zadanie {todo} z listy {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} zaktualizował zadanie {todo} na liście {calendar}",
- "You updated todo {todo} in list {calendar}" : "Zaktualizowałeś zadanie {todo} na liście {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} zakończył zadanie {todo} na liście {calendar}",
- "You solved todo {todo} in list {calendar}" : "Zakończyłeś zadanie {todo} na liście {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} otworzył ponownie zadanie {todo} na liście {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Otworzyłeś ponownie zadanie {todo} na liście {calendar}",
+ "Busy" : "Zajęty",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} utworzył zadanie {todo} na liście {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Utworzyłeś zadanie {todo} na liście {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} usunął zadanie {todo} z listy {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Usunąłeś zadanie {todo} z listy {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} zaktualizował zadanie {todo} na liście {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Zaktualizowałeś zadanie {todo} na liście {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} zakończył zadanie {todo} na liście {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Zakończyłeś zadanie {todo} na liście {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} otworzył ponownie zadanie {todo} na liście {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Otworzyłeś ponownie zadanie {todo} na liście {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} przeniósł zadanie {todo} z listy {sourceCalendar} na listę {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Przeniosłeś zadanie {todo} z listy {sourceCalendar} na listę {targetCalendar}",
"Calendar, contacts and tasks" : "Kalendarz, kontakty i zadania",
"A <strong>calendar</strong> was modified" : "<strong>Kalendarz</strong> został zmodyfikowany",
"A calendar <strong>event</strong> was modified" : "<strong>Zdarzenie</strong> kalendarza zostało zmodyfikowane",
- "A calendar <strong>todo</strong> was modified" : "Kalendarz <strong>zadań</strong> został zmieniony",
+ "A calendar <strong>to-do</strong> was modified" : "Kalendarz <strong>zadań</strong> został zmieniony",
"Contact birthdays" : "Urodziny kontaktu",
"Death of %s" : "Śmierć %s",
+ "Untitled calendar" : "Kalendarz bez tytułu",
"Calendar:" : "Kalendarz:",
"Date:" : "Data:",
"Where:" : "Gdzie:",
"Description:" : "Opis:",
- "Untitled event" : "Wydarzenie bez tytułu",
"_%n year_::_%n years_" : ["%n rok","%n lata","%n lat","%n lat"],
"_%n month_::_%n months_" : ["%n miesiąc","%n miesiące","%n miesięcy","%n miesięcy"],
"_%n day_::_%n days_" : ["%n dzień","%n dni","%n dni","%n dni"],
@@ -67,22 +72,94 @@ OC.L10N.register(
"Description: %s" : "Opis: %s",
"Where: %s" : "Gdzie: %s",
"%1$s via %2$s" : "%1$s przez %2$s",
+ "In the past on %1$s for the entire day" : "W przeszłości w %1$s na cały dzień",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Za minutę, dnia %1$s przez cały dzień","Za %nminuty, dnia %1$s, przez cały dzień","Za %nminut, dnia %1$s, przez cały dzień","Za %nminut, dnia %1$s, przez cały dzień"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Za godzinę, dnia %1$s, przez cały dzień","Za %n godziny, dnia %1$s, przez cały dzień","Za %n godzin, dnia %1$s, przez cały dzień","Za %n godzin, dnia %1$s, przez cały dzień"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Za jeden dzień, dnia %1$s, przez cały dzień","Za %n dni, dnia %1$s, przez cały dzień","Za %n dni, dnia %1$s, przez cały dzień","Za %n dni, dnia %1$s, przez cały dzień"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Za tydzień, dnia %1$s, przez cały dzień","Za %n tygodnie, dnia %1$s, przez cały dzień","Za %n tygodni, dnia %1$s, przez cały dzień","Za %n tygodni, dnia %1$s, przez cały dzień"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["W ciągu miesiąca, dnia %1$s przez cały dzień","W ciągu %n miesięcy, dnia %1$s przez cały dzień","W ciągu %n miesięcy, dnia %1$s przez cały dzień","W ciągu %n miesięcy, dnia %1$s przez cały dzień"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Za rok dnia %1$s przez cały dzień","Za %n lata dnia %1$s przez cały dzień","Za %n lat dnia %1$s przez cały dzień","Za %n lat dnia %1$s przez cały dzień"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Za dzień, dnia %1$s między %2$s - %3$s","Za %n dni dnia %1$s między %2$s - %3$s","Za %n dni dnia %1$s między %2$s - %3$s","Za %n dni dnia %1$s między %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Za tydzień dnia %1$s między %2$s - %3$s","Za %n tygodnie dnia %1$s między %2$s - %3$s","Za %n tygodni dnia %1$s między %2$s - %3$s","Za %n tygodni dnia %1$s między %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Za miesiąc dnia %1$s między %2$s - %3$s","Za %n miesięcy dnia %1$s między %2$s - %3$s","Za %n miesięcy dnia %1$s między %2$s - %3$s","Za %n miesięcy dnia %1$s między %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Za rok dnia %1$s między %2$s - %3$s","Za %n lata dnia %1$s między %2$s - %3$s","Za %n lat dnia %1$s między %2$s - %3$s","Za %n lat dnia %1$s między %2$s - %3$s"],
+ "Could not generate when statement" : "Nie można wygenerować instrukcji when",
+ "Every Day for the entire day" : "Codziennie przez cały dzień",
+ "Every Day for the entire day until %1$s" : "Codziennie przez cały dzień do %1$s",
+ "Every Day between %1$s - %2$s" : "Codziennie między %1$s – %2$s",
+ "Every %1$d Days for the entire day" : "Co %1$d dni przez cały dzień",
+ "Every %1$d Days for the entire day until %2$s" : "Co %1$d dni przez cały dzień aż do %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Co %1$d dni pomiędzy %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Co %1$d dni, pomiędzy %2$s - %3$s aż do %4$s",
+ "Could not generate event recurrence statement" : "Nie można wygenerować zestawienia powtórzeń zdarzenia",
+ "Every Week on %1$s for the entire day" : "Każdego tygodnia w %1$s przez cały dzień",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Każdego %1$d miesiąca dnia %2$s między %3$s - %4$s do %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Co rok w %1$s dnia %2$s przez cały dzień",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Co rok w %1$s dnia %2$s przez cały dzień do %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Co roku za %1$s dnia %2$s między %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Co roku za %1$s dnia %2$s między %3$s - %4$s do %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Co %1$d lat dnia %2$s o %3$s przez cały dzień",
+ "In the past on %1$s" : "W przeszłości dnia %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Za minutę dnia %1$s","Za %n minut dnia %1$s","Za %n minut dnia %1$s","Za %n minut dnia %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Za godzinę dnia %1$s","Za %n godziny dnia %1$s","Za %n godzin dnia %1$s","Za %n godzin dnia %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Za dzień, dnia %1$s","Za %n dni dnia %1$s","Za %n dni dnia %1$s","Za %n dni dnia %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Za tydzień dnia %1$s","Za %n tygodnie dnia %1$s","Za %n tygodni dnia %1$s","Za %n tygodni dnia %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Za miesiąc dnia %1$s","W ciągu %n miesięcy dnia %1$s","W ciągu %n miesięcy dnia %1$s","W ciągu %n miesięcy dnia %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Za rok dnia %1$s","Za %n lat dnia %1$s","Za %n lat dnia %1$s","Za %n lat dnia %1$s"],
+ "In the past on %1$s then on %2$s" : "W przeszłości dnia %1$s, a następnie %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Za minutę dnia %1$s, a następnie %2$s","Za %n minuty dnia %1$s, a następnie %2$s","Za %n minut dnia %1$s, a następnie %2$s","Za %n minut dnia %1$s, a następnie %2$s"],
+ "Could not generate next recurrence statement" : "Nie można wygenerować następnej instrukcji powtarzania",
"Cancelled: %1$s" : "Anulowane: %1$s",
- "Invitation canceled" : "Zaproszenie anulowane",
+ "\"%1$s\" has been canceled" : "\"%1$s\" zostało anulowane",
"Re: %1$s" : "Odp: %1$s",
- "Invitation updated" : "Zaproszenie zaktualizowane",
+ "%1$s has accepted your invitation" : "Twoje zaproszenie zostało zaakceptowane przez %1$s",
+ "%1$s has tentatively accepted your invitation" : "Twoje zaproszenie zostało wstępnie zaakceptowane przez %1$s",
+ "%1$s has declined your invitation" : "Twoje zaproszenie zostało odrzucone przez %1$s",
+ "%1$s has responded to your invitation" : "%1$s odpowiedział/a na Twoje zaproszenie",
+ "Invitation updated: %1$s" : "Zaktualizowano zaproszenie: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s zaktualizował/a wydarzenie \"%2$s\"",
"Invitation: %1$s" : "Zaproszenie: %1$s",
- "Invitation" : "Zaproszenie",
- "Title:" : "Tytuł:",
- "Time:" : "Czas:",
- "Location:" : "Lokalizacja:",
- "Link:" : "Link: ",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s chce cię zaprosić na \"%2$s\"",
"Organizer:" : "Organizator:",
"Attendees:" : "Uczestnicy:",
+ "Title:" : "Tytuł:",
+ "When:" : "Kiedy:",
+ "Location:" : "Lokalizacja:",
+ "Link:" : "Odnośnik: ",
+ "Occurring:" : "Występujący:",
"Accept" : "Akceptuj",
"Decline" : "Odrzuć",
"More options …" : "Więcej opcji…",
"More options at %s" : "Więcej opcji na %s",
+ "Monday" : "Poniedziałek",
+ "Tuesday" : "Wtorek",
+ "Wednesday" : "Środa",
+ "Thursday" : "Czwartek",
+ "Friday" : "Piątek",
+ "Saturday" : "Sobota",
+ "Sunday" : "Niedziela",
+ "January" : "Styczeń",
+ "February" : "Luty",
+ "March" : "Marzec",
+ "April" : "Kwiecień",
+ "May" : "Maj",
+ "June" : "Czerwiec",
+ "July" : "Lipiec",
+ "August" : "Sierpień",
+ "September" : "Wrzesień",
+ "October" : "Październik",
+ "November" : "Listopad",
+ "December" : "Grudzień",
+ "First" : "Pierwsza",
+ "Second" : "Druga",
+ "Third" : "Trzecia",
+ "Fourth" : "Czwarta",
+ "Fifth" : "Piąty",
+ "Last" : "Ostatnia",
+ "Second Last" : "Druga ostatnia",
+ "Third Last" : "Trzecia ostatnia",
+ "Fourth Last" : "Czwarty ostatni",
+ "Fifth Last" : "Piąty od końca",
"Contacts" : "Kontakty",
"{actor} created address book {addressbook}" : "{actor} utworzył książkę adresową {addressbook}",
"You created address book {addressbook}" : "Utworzyłeś książkę adresową {addressbook}",
@@ -108,7 +185,10 @@ OC.L10N.register(
"{actor} updated contact {card} in address book {addressbook}" : "{actor} zaktualizował kontakt {card} w książce adresowej {addressbook}",
"You updated contact {card} in address book {addressbook}" : "Zaktualizowałeś kontakt {card} w książce adresowej {addressbook}",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "Zmodyfikowano <strong>kontakt</strong> lub <strong>książkę adresową</strong>",
+ "Accounts" : "Konta",
+ "System address book which holds all accounts" : "Systemowa książka adresowa, która przechowuje wszystkie konta",
"File is not updatable: %1$s" : "Plik nie podlega aktualizacji: %1$s",
+ "Failed to get storage for file" : "Nie udało się uzyskać miejsca na plik",
"Could not write to final file, canceled by hook" : "Nie można zapisać do pliku końcowego, anulowane przez hook",
"Could not write file contents" : "Nie można zapisać zawartości pliku",
"_%n byte_::_%n bytes_" : ["%n bajt","%n bajty","%n bajtów","%n bajtów"],
@@ -117,45 +197,91 @@ OC.L10N.register(
"Could not rename part file to final file, canceled by hook" : "Nie można zmienić nazwy pliku podzielonego na plik końcowy, anulowane przez hook",
"Could not rename part file to final file" : "Nie można zmienić nazwy pliku podzielonego na plik końcowy",
"Failed to check file size: %1$s" : "Nie udało się sprawdzić rozmiaru pliku: %1$s",
- "Could not open file" : "Nie można otworzyć pliku",
+ "Could not open file: %1$s, file does seem to exist" : "Nie można otworzyć: %1$s, wygląda na to, że plik istnieje",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Nie można otworzyć: %1$s, wygląda na to, że plik nie istnieje",
"Encryption not ready: %1$s" : "Szyfrowanie nie jest gotowe: %1$s",
"Failed to open file: %1$s" : "Nie udało się otworzyć pliku: %1$s",
"Failed to unlink: %1$s" : "Nie udało się odłączyć: %1$s",
- "Invalid chunk name" : "Nieprawidłowa nazwa fragmentu",
- "Could not rename part file assembled from chunks" : "Nie można zmienić nazwy pliku podzielonego złożonego z kawałków",
"Failed to write file contents: %1$s" : "Nie udało się zapisać zawartości pliku: %1$s",
"File not found: %1$s" : "Nie znaleziono pliku: %1$s",
+ "Invalid target path" : "Nieprawidłowa ścieżka docelowa",
"System is in maintenance mode." : "Serwer jest w trybie konserwacji.",
"Upgrade needed" : "Wymagana aktualizacja",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "%s musisz używać protokołu HTTPS aby móc korzystać z CalDAV i CardDAV w systemach iOS/macOS.",
"Configures a CalDAV account" : "Konfiguruje konto CalDAV",
"Configures a CardDAV account" : "Konfiguruje konto CardDAV",
"Events" : "Wydarzenia",
- "Tasks" : "Zadania",
"Untitled task" : "Zadanie bez tytułu",
"Completed on %s" : "Ukończono %s",
"Due on %s by %s" : "Na dzień %s w %s",
"Due on %s" : "Na dzień %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Witamy w Kalendarzu Nextcloud!\n\nTo przykładowe wydarzenie – poznaj elastyczność planowania z Kalendarzem Nextcloud, edytując je dowolnie!\n\nZ Kalendarzem Nextcloud możesz:\n– Tworzyć, edytować i zarządzać wydarzeniami z łatwością.\n– Tworzyć wiele kalendarzy i udostępniać je współpracownikom, znajomym lub rodzinie.\n– Sprawdzać dostępność i pokazywać innym, kiedy jesteś zajęty.\n– Łatwo integrować się z aplikacjami i urządzeniami przez CalDAV.\n– Dostosować wszystko do siebie: ustawiać wydarzenia cykliczne, powiadomienia i inne opcje.",
+ "Example event - open me!" : "Przykładowe wydarzenie – kliknij, aby otworzyć!",
+ "System Address Book" : "Systemowa książka adresowa",
+ "The system address book contains contact information for all users in your instance." : "Systemowa książka adresowa zawiera informacje kontaktowe wszystkich użytkowników w Twojej instancji",
+ "Enable System Address Book" : "Włącz systemową książkę adresową",
+ "DAV system address book" : "Książka adresowa systemu DAV",
+ "No outstanding DAV system address book sync." : "Brak zaległej synchronizacji książki adresowej systemu DAV.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Synchronizacja książki adresowej systemu DAV nie została jeszcze uruchomiona, ponieważ Twoja instancja ma ponad 1000 użytkowników lub wystąpił błąd. Uruchom go ręcznie, wywołując \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "Adres WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Nie można sprawdzić, czy serwer WWW jest prawidłowo skonfigurowany, aby umożliwić synchronizację plików przez WebDAV. Sprawdź ręcznie.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Serwer WWW nie jest jeszcze na tyle poprawnie skonfigurowany, aby umożliwić synchronizację plików, ponieważ interfejs WebDAV może być uszkodzony.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Twój serwer internetowy jest prawidłowo skonfigurowany, aby umożliwić synchronizację plików przez WebDAV.",
"Migrated calendar (%1$s)" : "Przeniesiony kalendarz (%1$s)",
"Calendars including events, details and attendees" : "Kalendarze zawierające wydarzenia, szczegóły i uczestników",
"Contacts and groups" : "Kontakty i grupy",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "Adres WebDAV",
- "Availability" : "Dostępność",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Jeśli skonfigurujesz godziny pracy, inni użytkownicy będą widzieć, kiedy jesteś poza biurem, gdy będą rezerwować spotkanie.",
+ "Absence saved" : "Nieobecność zapisana",
+ "Failed to save your absence settings" : "Niedane zapisanie ustawień nieobecności",
+ "Absence cleared" : "Nieobecność została usunięta",
+ "Failed to clear your absence settings" : "Nieudane kasowanie ustawień nieobecności",
+ "First day" : "Pierwszy dzień",
+ "Last day (inclusive)" : "Ostatni dzień (inclusive)",
+ "Out of office replacement (optional)" : "Zamiennik poza biurem (opcjonalnie)",
+ "Name of the replacement" : "Nazwa zamiennika",
+ "No results." : "Brak wyników.",
+ "Start typing." : "Zacznij pisać.",
+ "Short absence status" : "Powiadomienie o krótkiej nieobecności ",
+ "Long absence Message" : "Powiadomienie o długiej nieobecności ",
+ "Save" : "Zapisz",
+ "Disable absence" : "Wyłącz nieobecność",
+ "Failed to load availability" : "Nie udało się wczytać dostępności",
+ "Saved availability" : "Zapisana dostępność",
+ "Failed to save availability" : "Nie udało się zapisać dostępności",
"Time zone:" : "Strefa czasowa:",
"to" : "od",
"Delete slot" : "Usuń przedział czasu",
"No working hours set" : "Nie ustawiono godzin pracy",
"Add slot" : "Dodaj przedział czasu",
- "Monday" : "Poniedziałek",
- "Tuesday" : "Wtorek",
- "Wednesday" : "Środa",
- "Thursday" : "Czwartek",
- "Friday" : "Piątek",
- "Saturday" : "Sobota",
- "Sunday" : "Niedziela",
- "Save" : "Zapisz",
+ "Weekdays" : "Dni powszednie",
+ "Pick a start time for {dayName}" : "Wybierz dzień rozpoczęcia {dayName}",
+ "Pick a end time for {dayName}" : "Wybierz dzień zakończenia {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Automatycznie ustaw status użytkownika na \"Nie przeszkadzać\" poza dostępnością, aby wyciszyć wszystkie powiadomienia.",
+ "Cancel" : "Anuluj",
+ "Import" : "Importuj",
+ "Error while saving settings" : "Błąd podczas zapisywania ustawień",
+ "Contact reset successfully" : "Kontakt został pomyślnie zresetowany",
+ "Error while resetting contact" : "Błąd podczas resetowania kontaktu",
+ "Contact imported successfully" : "Kontakt został pomyślnie zaimportowany",
+ "Error while importing contact" : "Błąd podczas importowania kontaktu",
+ "Import contact" : "Importuj kontakt",
+ "Reset to default" : "Przywróć domyślne",
+ "Import contacts" : "Importuj kontakty",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Importowanie nowego pliku .vcf usunie domyślny kontakt i zastąpi go nowym. Czy chcesz kontynuować?",
+ "Failed to save example event creation setting" : "Nie udało się zapisać ustawień tworzenia przykładowego wydarzenia",
+ "Failed to upload the example event" : "Nie udało się przesłać przykładowego wydarzenia",
+ "Custom example event was saved successfully" : "Niestandardowe przykładowe wydarzenie zostało pomyślnie zapisane",
+ "Failed to delete the custom example event" : "Nie udało się usunąć niestandardowego przykładowego wydarzenia",
+ "Custom example event was deleted successfully" : "Niestandardowe przykładowe wydarzenie zostało pomyślnie usunięte",
+ "Import calendar event" : "Importuj wydarzenie z kalendarza",
+ "Uploading a new event will overwrite the existing one." : "Przesłanie nowego wydarzenia zastąpi istniejące",
+ "Upload event" : "Prześlij wydarzenie",
+ "Availability" : "Dostępność",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Jeśli skonfigurujesz godziny pracy, inne osoby będą widzieć, kiedy jesteś poza biurem, rezerwując spotkanie.",
+ "Absence" : "Nieobecność",
+ "Configure your next absence period." : "Ustaw czas swojej nieobecności ",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Zainstaluj także {calendarappstoreopen}aplikację Kalendarz{linkclose} lub {calendardocopen}połącz swój komputer i telefon komórkowy, aby zsynchronizować ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Upewnij się, że poprawnie skonfigurowałeś {emailopen}serwer poczty{linkclose}.",
"Calendar server" : "Serwer kalendarza",
"Send invitations to attendees" : "Wyślij zaproszenia do uczestników",
"Automatically generate a birthday calendar" : "Automatycznie generuj kalendarz urodzin",
@@ -166,15 +292,12 @@ OC.L10N.register(
"Send reminder notifications to calendar sharees as well" : "Wysyłaj przypomnienia również do udostępnionych kalendarzy",
"Reminders are always sent to organizers and attendees." : "Przypomnienia są zawsze wysyłane do organizatorów i uczestników.",
"Enable notifications for events via push" : "Włącz powiadomienia o zdarzeniach poprzez Push",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Zainstaluj także {calendarappstoreopen}aplikację Kalendarz{linkclose} lub {calendardocopen}połącz swój komputer i telefon komórkowy, aby zsynchronizować ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Upewnij się, że poprawnie skonfigurowałeś {emailopen}serwer poczty{linkclose}.",
+ "Example content" : "Przykładowa zawartość",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Przykładowa zawartość służy do prezentacji funkcji Nextcloud. Domyślna zawartość jest dostarczana z Nextcloud i może zostać zastąpiona zawartością niestandardową",
"There was an error updating your attendance status." : "Wystąpił błąd zmiany stanu uczestnictwa",
"Please contact the organizer directly." : "Skontaktuj się bezpośrednio z orgnizatorem.",
"Are you accepting the invitation?" : "Czy akceptujesz zaproszenie?",
"Tentative" : "Niepewne",
- "Number of guests" : "Liczba gości",
- "Comment" : "Komentarz",
- "Your attendance was updated successfully." : "Twoja obecność została pomyślnie zaktualizowana.",
- "Calendar and tasks" : "Kalendarz i zadania"
+ "Your attendance was updated successfully." : "Twoja obecność została pomyślnie zaktualizowana."
},
"nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);");
diff --git a/apps/dav/l10n/pl.json b/apps/dav/l10n/pl.json
index 4ea51401e82..4b2ffc40dc0 100644
--- a/apps/dav/l10n/pl.json
+++ b/apps/dav/l10n/pl.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Kalendarz",
- "Todos" : "Zadania",
+ "Tasks" : "Zadania",
"Personal" : "Osobiste",
"{actor} created calendar {calendar}" : "{actor} utworzył kalendarz {calendar}",
"You created calendar {calendar}" : "Utworzyłeś kalendarz {calendar}",
@@ -23,36 +23,41 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} udostępnił kalendarz {calendar} dla grupy {group}",
"You unshared calendar {calendar} from group {group}" : "Zakończyłeś udostępnianie kalendarza {calendar} dla grupy {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} zakończył udostępnianie kalendarza {calendar} dla grupy {group} .",
+ "Untitled event" : "Wydarzenie bez tytułu",
"{actor} created event {event} in calendar {calendar}" : "{actor} utworzył wydarzenie {event} w kalendarzu {calendar}",
"You created event {event} in calendar {calendar}" : "Utworzyłeś wydarzenie {event} w kalendarzu {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} usunął wydarzenie {event} z kalendarza {calendar}",
"You deleted event {event} from calendar {calendar}" : "Usunąłeś wydarzenie {event} z kalendarza {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} zaktualizował wydarzenie {event} z kalendarza {calendar}",
"You updated event {event} in calendar {calendar}" : "Zaktualizowałeś wydarzenie {event} w kalendarzu {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} przeniósł wydarzenie {event} z kalendarza {sourceCalendar} do kalendarza {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Przeniosłeś wydarzenie {event} z kalendarza {sourceCalendar} do kalendarza {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} przywrócił wydarzenie {event} z kalendarza {calendar}",
"You restored event {event} of calendar {calendar}" : "Przywróciłeś wydarzenie {event} z kalendarza {calendar}",
- "Busy" : "Czekaj",
- "{actor} created todo {todo} in list {calendar}" : "{actor} utworzył zadanie {todo} na liście {calendar}",
- "You created todo {todo} in list {calendar}" : "Utworzyłeś zadanie {todo} na liście {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} usunął zadanie {todo} z listy {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Usunąłeś zadanie {todo} z listy {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} zaktualizował zadanie {todo} na liście {calendar}",
- "You updated todo {todo} in list {calendar}" : "Zaktualizowałeś zadanie {todo} na liście {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} zakończył zadanie {todo} na liście {calendar}",
- "You solved todo {todo} in list {calendar}" : "Zakończyłeś zadanie {todo} na liście {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} otworzył ponownie zadanie {todo} na liście {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Otworzyłeś ponownie zadanie {todo} na liście {calendar}",
+ "Busy" : "Zajęty",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} utworzył zadanie {todo} na liście {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Utworzyłeś zadanie {todo} na liście {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} usunął zadanie {todo} z listy {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Usunąłeś zadanie {todo} z listy {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} zaktualizował zadanie {todo} na liście {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Zaktualizowałeś zadanie {todo} na liście {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} zakończył zadanie {todo} na liście {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Zakończyłeś zadanie {todo} na liście {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} otworzył ponownie zadanie {todo} na liście {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Otworzyłeś ponownie zadanie {todo} na liście {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} przeniósł zadanie {todo} z listy {sourceCalendar} na listę {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Przeniosłeś zadanie {todo} z listy {sourceCalendar} na listę {targetCalendar}",
"Calendar, contacts and tasks" : "Kalendarz, kontakty i zadania",
"A <strong>calendar</strong> was modified" : "<strong>Kalendarz</strong> został zmodyfikowany",
"A calendar <strong>event</strong> was modified" : "<strong>Zdarzenie</strong> kalendarza zostało zmodyfikowane",
- "A calendar <strong>todo</strong> was modified" : "Kalendarz <strong>zadań</strong> został zmieniony",
+ "A calendar <strong>to-do</strong> was modified" : "Kalendarz <strong>zadań</strong> został zmieniony",
"Contact birthdays" : "Urodziny kontaktu",
"Death of %s" : "Śmierć %s",
+ "Untitled calendar" : "Kalendarz bez tytułu",
"Calendar:" : "Kalendarz:",
"Date:" : "Data:",
"Where:" : "Gdzie:",
"Description:" : "Opis:",
- "Untitled event" : "Wydarzenie bez tytułu",
"_%n year_::_%n years_" : ["%n rok","%n lata","%n lat","%n lat"],
"_%n month_::_%n months_" : ["%n miesiąc","%n miesiące","%n miesięcy","%n miesięcy"],
"_%n day_::_%n days_" : ["%n dzień","%n dni","%n dni","%n dni"],
@@ -65,22 +70,94 @@
"Description: %s" : "Opis: %s",
"Where: %s" : "Gdzie: %s",
"%1$s via %2$s" : "%1$s przez %2$s",
+ "In the past on %1$s for the entire day" : "W przeszłości w %1$s na cały dzień",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Za minutę, dnia %1$s przez cały dzień","Za %nminuty, dnia %1$s, przez cały dzień","Za %nminut, dnia %1$s, przez cały dzień","Za %nminut, dnia %1$s, przez cały dzień"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Za godzinę, dnia %1$s, przez cały dzień","Za %n godziny, dnia %1$s, przez cały dzień","Za %n godzin, dnia %1$s, przez cały dzień","Za %n godzin, dnia %1$s, przez cały dzień"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Za jeden dzień, dnia %1$s, przez cały dzień","Za %n dni, dnia %1$s, przez cały dzień","Za %n dni, dnia %1$s, przez cały dzień","Za %n dni, dnia %1$s, przez cały dzień"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Za tydzień, dnia %1$s, przez cały dzień","Za %n tygodnie, dnia %1$s, przez cały dzień","Za %n tygodni, dnia %1$s, przez cały dzień","Za %n tygodni, dnia %1$s, przez cały dzień"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["W ciągu miesiąca, dnia %1$s przez cały dzień","W ciągu %n miesięcy, dnia %1$s przez cały dzień","W ciągu %n miesięcy, dnia %1$s przez cały dzień","W ciągu %n miesięcy, dnia %1$s przez cały dzień"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Za rok dnia %1$s przez cały dzień","Za %n lata dnia %1$s przez cały dzień","Za %n lat dnia %1$s przez cały dzień","Za %n lat dnia %1$s przez cały dzień"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Za dzień, dnia %1$s między %2$s - %3$s","Za %n dni dnia %1$s między %2$s - %3$s","Za %n dni dnia %1$s między %2$s - %3$s","Za %n dni dnia %1$s między %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Za tydzień dnia %1$s między %2$s - %3$s","Za %n tygodnie dnia %1$s między %2$s - %3$s","Za %n tygodni dnia %1$s między %2$s - %3$s","Za %n tygodni dnia %1$s między %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Za miesiąc dnia %1$s między %2$s - %3$s","Za %n miesięcy dnia %1$s między %2$s - %3$s","Za %n miesięcy dnia %1$s między %2$s - %3$s","Za %n miesięcy dnia %1$s między %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Za rok dnia %1$s między %2$s - %3$s","Za %n lata dnia %1$s między %2$s - %3$s","Za %n lat dnia %1$s między %2$s - %3$s","Za %n lat dnia %1$s między %2$s - %3$s"],
+ "Could not generate when statement" : "Nie można wygenerować instrukcji when",
+ "Every Day for the entire day" : "Codziennie przez cały dzień",
+ "Every Day for the entire day until %1$s" : "Codziennie przez cały dzień do %1$s",
+ "Every Day between %1$s - %2$s" : "Codziennie między %1$s – %2$s",
+ "Every %1$d Days for the entire day" : "Co %1$d dni przez cały dzień",
+ "Every %1$d Days for the entire day until %2$s" : "Co %1$d dni przez cały dzień aż do %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Co %1$d dni pomiędzy %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Co %1$d dni, pomiędzy %2$s - %3$s aż do %4$s",
+ "Could not generate event recurrence statement" : "Nie można wygenerować zestawienia powtórzeń zdarzenia",
+ "Every Week on %1$s for the entire day" : "Każdego tygodnia w %1$s przez cały dzień",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Każdego %1$d miesiąca dnia %2$s między %3$s - %4$s do %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Co rok w %1$s dnia %2$s przez cały dzień",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Co rok w %1$s dnia %2$s przez cały dzień do %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Co roku za %1$s dnia %2$s między %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Co roku za %1$s dnia %2$s między %3$s - %4$s do %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Co %1$d lat dnia %2$s o %3$s przez cały dzień",
+ "In the past on %1$s" : "W przeszłości dnia %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Za minutę dnia %1$s","Za %n minut dnia %1$s","Za %n minut dnia %1$s","Za %n minut dnia %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Za godzinę dnia %1$s","Za %n godziny dnia %1$s","Za %n godzin dnia %1$s","Za %n godzin dnia %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Za dzień, dnia %1$s","Za %n dni dnia %1$s","Za %n dni dnia %1$s","Za %n dni dnia %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Za tydzień dnia %1$s","Za %n tygodnie dnia %1$s","Za %n tygodni dnia %1$s","Za %n tygodni dnia %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Za miesiąc dnia %1$s","W ciągu %n miesięcy dnia %1$s","W ciągu %n miesięcy dnia %1$s","W ciągu %n miesięcy dnia %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Za rok dnia %1$s","Za %n lat dnia %1$s","Za %n lat dnia %1$s","Za %n lat dnia %1$s"],
+ "In the past on %1$s then on %2$s" : "W przeszłości dnia %1$s, a następnie %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Za minutę dnia %1$s, a następnie %2$s","Za %n minuty dnia %1$s, a następnie %2$s","Za %n minut dnia %1$s, a następnie %2$s","Za %n minut dnia %1$s, a następnie %2$s"],
+ "Could not generate next recurrence statement" : "Nie można wygenerować następnej instrukcji powtarzania",
"Cancelled: %1$s" : "Anulowane: %1$s",
- "Invitation canceled" : "Zaproszenie anulowane",
+ "\"%1$s\" has been canceled" : "\"%1$s\" zostało anulowane",
"Re: %1$s" : "Odp: %1$s",
- "Invitation updated" : "Zaproszenie zaktualizowane",
+ "%1$s has accepted your invitation" : "Twoje zaproszenie zostało zaakceptowane przez %1$s",
+ "%1$s has tentatively accepted your invitation" : "Twoje zaproszenie zostało wstępnie zaakceptowane przez %1$s",
+ "%1$s has declined your invitation" : "Twoje zaproszenie zostało odrzucone przez %1$s",
+ "%1$s has responded to your invitation" : "%1$s odpowiedział/a na Twoje zaproszenie",
+ "Invitation updated: %1$s" : "Zaktualizowano zaproszenie: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s zaktualizował/a wydarzenie \"%2$s\"",
"Invitation: %1$s" : "Zaproszenie: %1$s",
- "Invitation" : "Zaproszenie",
- "Title:" : "Tytuł:",
- "Time:" : "Czas:",
- "Location:" : "Lokalizacja:",
- "Link:" : "Link: ",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s chce cię zaprosić na \"%2$s\"",
"Organizer:" : "Organizator:",
"Attendees:" : "Uczestnicy:",
+ "Title:" : "Tytuł:",
+ "When:" : "Kiedy:",
+ "Location:" : "Lokalizacja:",
+ "Link:" : "Odnośnik: ",
+ "Occurring:" : "Występujący:",
"Accept" : "Akceptuj",
"Decline" : "Odrzuć",
"More options …" : "Więcej opcji…",
"More options at %s" : "Więcej opcji na %s",
+ "Monday" : "Poniedziałek",
+ "Tuesday" : "Wtorek",
+ "Wednesday" : "Środa",
+ "Thursday" : "Czwartek",
+ "Friday" : "Piątek",
+ "Saturday" : "Sobota",
+ "Sunday" : "Niedziela",
+ "January" : "Styczeń",
+ "February" : "Luty",
+ "March" : "Marzec",
+ "April" : "Kwiecień",
+ "May" : "Maj",
+ "June" : "Czerwiec",
+ "July" : "Lipiec",
+ "August" : "Sierpień",
+ "September" : "Wrzesień",
+ "October" : "Październik",
+ "November" : "Listopad",
+ "December" : "Grudzień",
+ "First" : "Pierwsza",
+ "Second" : "Druga",
+ "Third" : "Trzecia",
+ "Fourth" : "Czwarta",
+ "Fifth" : "Piąty",
+ "Last" : "Ostatnia",
+ "Second Last" : "Druga ostatnia",
+ "Third Last" : "Trzecia ostatnia",
+ "Fourth Last" : "Czwarty ostatni",
+ "Fifth Last" : "Piąty od końca",
"Contacts" : "Kontakty",
"{actor} created address book {addressbook}" : "{actor} utworzył książkę adresową {addressbook}",
"You created address book {addressbook}" : "Utworzyłeś książkę adresową {addressbook}",
@@ -106,7 +183,10 @@
"{actor} updated contact {card} in address book {addressbook}" : "{actor} zaktualizował kontakt {card} w książce adresowej {addressbook}",
"You updated contact {card} in address book {addressbook}" : "Zaktualizowałeś kontakt {card} w książce adresowej {addressbook}",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "Zmodyfikowano <strong>kontakt</strong> lub <strong>książkę adresową</strong>",
+ "Accounts" : "Konta",
+ "System address book which holds all accounts" : "Systemowa książka adresowa, która przechowuje wszystkie konta",
"File is not updatable: %1$s" : "Plik nie podlega aktualizacji: %1$s",
+ "Failed to get storage for file" : "Nie udało się uzyskać miejsca na plik",
"Could not write to final file, canceled by hook" : "Nie można zapisać do pliku końcowego, anulowane przez hook",
"Could not write file contents" : "Nie można zapisać zawartości pliku",
"_%n byte_::_%n bytes_" : ["%n bajt","%n bajty","%n bajtów","%n bajtów"],
@@ -115,45 +195,91 @@
"Could not rename part file to final file, canceled by hook" : "Nie można zmienić nazwy pliku podzielonego na plik końcowy, anulowane przez hook",
"Could not rename part file to final file" : "Nie można zmienić nazwy pliku podzielonego na plik końcowy",
"Failed to check file size: %1$s" : "Nie udało się sprawdzić rozmiaru pliku: %1$s",
- "Could not open file" : "Nie można otworzyć pliku",
+ "Could not open file: %1$s, file does seem to exist" : "Nie można otworzyć: %1$s, wygląda na to, że plik istnieje",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Nie można otworzyć: %1$s, wygląda na to, że plik nie istnieje",
"Encryption not ready: %1$s" : "Szyfrowanie nie jest gotowe: %1$s",
"Failed to open file: %1$s" : "Nie udało się otworzyć pliku: %1$s",
"Failed to unlink: %1$s" : "Nie udało się odłączyć: %1$s",
- "Invalid chunk name" : "Nieprawidłowa nazwa fragmentu",
- "Could not rename part file assembled from chunks" : "Nie można zmienić nazwy pliku podzielonego złożonego z kawałków",
"Failed to write file contents: %1$s" : "Nie udało się zapisać zawartości pliku: %1$s",
"File not found: %1$s" : "Nie znaleziono pliku: %1$s",
+ "Invalid target path" : "Nieprawidłowa ścieżka docelowa",
"System is in maintenance mode." : "Serwer jest w trybie konserwacji.",
"Upgrade needed" : "Wymagana aktualizacja",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "%s musisz używać protokołu HTTPS aby móc korzystać z CalDAV i CardDAV w systemach iOS/macOS.",
"Configures a CalDAV account" : "Konfiguruje konto CalDAV",
"Configures a CardDAV account" : "Konfiguruje konto CardDAV",
"Events" : "Wydarzenia",
- "Tasks" : "Zadania",
"Untitled task" : "Zadanie bez tytułu",
"Completed on %s" : "Ukończono %s",
"Due on %s by %s" : "Na dzień %s w %s",
"Due on %s" : "Na dzień %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Witamy w Kalendarzu Nextcloud!\n\nTo przykładowe wydarzenie – poznaj elastyczność planowania z Kalendarzem Nextcloud, edytując je dowolnie!\n\nZ Kalendarzem Nextcloud możesz:\n– Tworzyć, edytować i zarządzać wydarzeniami z łatwością.\n– Tworzyć wiele kalendarzy i udostępniać je współpracownikom, znajomym lub rodzinie.\n– Sprawdzać dostępność i pokazywać innym, kiedy jesteś zajęty.\n– Łatwo integrować się z aplikacjami i urządzeniami przez CalDAV.\n– Dostosować wszystko do siebie: ustawiać wydarzenia cykliczne, powiadomienia i inne opcje.",
+ "Example event - open me!" : "Przykładowe wydarzenie – kliknij, aby otworzyć!",
+ "System Address Book" : "Systemowa książka adresowa",
+ "The system address book contains contact information for all users in your instance." : "Systemowa książka adresowa zawiera informacje kontaktowe wszystkich użytkowników w Twojej instancji",
+ "Enable System Address Book" : "Włącz systemową książkę adresową",
+ "DAV system address book" : "Książka adresowa systemu DAV",
+ "No outstanding DAV system address book sync." : "Brak zaległej synchronizacji książki adresowej systemu DAV.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Synchronizacja książki adresowej systemu DAV nie została jeszcze uruchomiona, ponieważ Twoja instancja ma ponad 1000 użytkowników lub wystąpił błąd. Uruchom go ręcznie, wywołując \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "Adres WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Nie można sprawdzić, czy serwer WWW jest prawidłowo skonfigurowany, aby umożliwić synchronizację plików przez WebDAV. Sprawdź ręcznie.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Serwer WWW nie jest jeszcze na tyle poprawnie skonfigurowany, aby umożliwić synchronizację plików, ponieważ interfejs WebDAV może być uszkodzony.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Twój serwer internetowy jest prawidłowo skonfigurowany, aby umożliwić synchronizację plików przez WebDAV.",
"Migrated calendar (%1$s)" : "Przeniesiony kalendarz (%1$s)",
"Calendars including events, details and attendees" : "Kalendarze zawierające wydarzenia, szczegóły i uczestników",
"Contacts and groups" : "Kontakty i grupy",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "Adres WebDAV",
- "Availability" : "Dostępność",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Jeśli skonfigurujesz godziny pracy, inni użytkownicy będą widzieć, kiedy jesteś poza biurem, gdy będą rezerwować spotkanie.",
+ "Absence saved" : "Nieobecność zapisana",
+ "Failed to save your absence settings" : "Niedane zapisanie ustawień nieobecności",
+ "Absence cleared" : "Nieobecność została usunięta",
+ "Failed to clear your absence settings" : "Nieudane kasowanie ustawień nieobecności",
+ "First day" : "Pierwszy dzień",
+ "Last day (inclusive)" : "Ostatni dzień (inclusive)",
+ "Out of office replacement (optional)" : "Zamiennik poza biurem (opcjonalnie)",
+ "Name of the replacement" : "Nazwa zamiennika",
+ "No results." : "Brak wyników.",
+ "Start typing." : "Zacznij pisać.",
+ "Short absence status" : "Powiadomienie o krótkiej nieobecności ",
+ "Long absence Message" : "Powiadomienie o długiej nieobecności ",
+ "Save" : "Zapisz",
+ "Disable absence" : "Wyłącz nieobecność",
+ "Failed to load availability" : "Nie udało się wczytać dostępności",
+ "Saved availability" : "Zapisana dostępność",
+ "Failed to save availability" : "Nie udało się zapisać dostępności",
"Time zone:" : "Strefa czasowa:",
"to" : "od",
"Delete slot" : "Usuń przedział czasu",
"No working hours set" : "Nie ustawiono godzin pracy",
"Add slot" : "Dodaj przedział czasu",
- "Monday" : "Poniedziałek",
- "Tuesday" : "Wtorek",
- "Wednesday" : "Środa",
- "Thursday" : "Czwartek",
- "Friday" : "Piątek",
- "Saturday" : "Sobota",
- "Sunday" : "Niedziela",
- "Save" : "Zapisz",
+ "Weekdays" : "Dni powszednie",
+ "Pick a start time for {dayName}" : "Wybierz dzień rozpoczęcia {dayName}",
+ "Pick a end time for {dayName}" : "Wybierz dzień zakończenia {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Automatycznie ustaw status użytkownika na \"Nie przeszkadzać\" poza dostępnością, aby wyciszyć wszystkie powiadomienia.",
+ "Cancel" : "Anuluj",
+ "Import" : "Importuj",
+ "Error while saving settings" : "Błąd podczas zapisywania ustawień",
+ "Contact reset successfully" : "Kontakt został pomyślnie zresetowany",
+ "Error while resetting contact" : "Błąd podczas resetowania kontaktu",
+ "Contact imported successfully" : "Kontakt został pomyślnie zaimportowany",
+ "Error while importing contact" : "Błąd podczas importowania kontaktu",
+ "Import contact" : "Importuj kontakt",
+ "Reset to default" : "Przywróć domyślne",
+ "Import contacts" : "Importuj kontakty",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Importowanie nowego pliku .vcf usunie domyślny kontakt i zastąpi go nowym. Czy chcesz kontynuować?",
+ "Failed to save example event creation setting" : "Nie udało się zapisać ustawień tworzenia przykładowego wydarzenia",
+ "Failed to upload the example event" : "Nie udało się przesłać przykładowego wydarzenia",
+ "Custom example event was saved successfully" : "Niestandardowe przykładowe wydarzenie zostało pomyślnie zapisane",
+ "Failed to delete the custom example event" : "Nie udało się usunąć niestandardowego przykładowego wydarzenia",
+ "Custom example event was deleted successfully" : "Niestandardowe przykładowe wydarzenie zostało pomyślnie usunięte",
+ "Import calendar event" : "Importuj wydarzenie z kalendarza",
+ "Uploading a new event will overwrite the existing one." : "Przesłanie nowego wydarzenia zastąpi istniejące",
+ "Upload event" : "Prześlij wydarzenie",
+ "Availability" : "Dostępność",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Jeśli skonfigurujesz godziny pracy, inne osoby będą widzieć, kiedy jesteś poza biurem, rezerwując spotkanie.",
+ "Absence" : "Nieobecność",
+ "Configure your next absence period." : "Ustaw czas swojej nieobecności ",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Zainstaluj także {calendarappstoreopen}aplikację Kalendarz{linkclose} lub {calendardocopen}połącz swój komputer i telefon komórkowy, aby zsynchronizować ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Upewnij się, że poprawnie skonfigurowałeś {emailopen}serwer poczty{linkclose}.",
"Calendar server" : "Serwer kalendarza",
"Send invitations to attendees" : "Wyślij zaproszenia do uczestników",
"Automatically generate a birthday calendar" : "Automatycznie generuj kalendarz urodzin",
@@ -164,15 +290,12 @@
"Send reminder notifications to calendar sharees as well" : "Wysyłaj przypomnienia również do udostępnionych kalendarzy",
"Reminders are always sent to organizers and attendees." : "Przypomnienia są zawsze wysyłane do organizatorów i uczestników.",
"Enable notifications for events via push" : "Włącz powiadomienia o zdarzeniach poprzez Push",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Zainstaluj także {calendarappstoreopen}aplikację Kalendarz{linkclose} lub {calendardocopen}połącz swój komputer i telefon komórkowy, aby zsynchronizować ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Upewnij się, że poprawnie skonfigurowałeś {emailopen}serwer poczty{linkclose}.",
+ "Example content" : "Przykładowa zawartość",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Przykładowa zawartość służy do prezentacji funkcji Nextcloud. Domyślna zawartość jest dostarczana z Nextcloud i może zostać zastąpiona zawartością niestandardową",
"There was an error updating your attendance status." : "Wystąpił błąd zmiany stanu uczestnictwa",
"Please contact the organizer directly." : "Skontaktuj się bezpośrednio z orgnizatorem.",
"Are you accepting the invitation?" : "Czy akceptujesz zaproszenie?",
"Tentative" : "Niepewne",
- "Number of guests" : "Liczba gości",
- "Comment" : "Komentarz",
- "Your attendance was updated successfully." : "Twoja obecność została pomyślnie zaktualizowana.",
- "Calendar and tasks" : "Kalendarz i zadania"
+ "Your attendance was updated successfully." : "Twoja obecność została pomyślnie zaktualizowana."
},"pluralForm" :"nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);"
} \ No newline at end of file
diff --git a/apps/dav/l10n/pt_BR.js b/apps/dav/l10n/pt_BR.js
index 6997704b5a2..52a410b4b6f 100644
--- a/apps/dav/l10n/pt_BR.js
+++ b/apps/dav/l10n/pt_BR.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Calendário",
- "Todos" : "Tarefas",
+ "Tasks" : "Tarefas",
"Personal" : "Pessoal",
"{actor} created calendar {calendar}" : "{actor} criou o calendário {calendar}",
"You created calendar {calendar}" : "Você criou o calendário {calendar}",
@@ -25,41 +25,46 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} compartilhou o calendário {calendar} com o grupo {group}",
"You unshared calendar {calendar} from group {group}" : "Você descompartilhou o calendário {calendar} do grupo {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} descompartilhou o calendário {calendar} do grupo {group}",
+ "Untitled event" : "Evento sem título",
"{actor} created event {event} in calendar {calendar}" : "{actor} criou o evento {event} no calendário {calendar}",
"You created event {event} in calendar {calendar}" : "Você criou o evento {event} no calendário {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} excluiu o evento {event} do calendário {calendar}",
"You deleted event {event} from calendar {calendar}" : "Você excluiu o evento {event} do calendário {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} atualizou o evento {event} no calendário {calendar}",
"You updated event {event} in calendar {calendar}" : "Você atualizou o evento {event} no calendário {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} moveu o evento {event} do calendário {sourceCalendar} para o calendário {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Você moveu o evento {event} do calendário {sourceCalendar} para o calendário {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} restaurou o evento {event} do calendário {calendar}",
"You restored event {event} of calendar {calendar}" : "Você restaurou o evento {event} do calendário {calendar}",
"Busy" : "Ocupado",
- "{actor} created todo {todo} in list {calendar}" : "{actor} criou a tarefa {todo} na lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Você criou a tarefa {todo} na lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} excluiu a tarefa {todo} da lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Você excluiu a tarefa {todo} da lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} atualizou a tarefa {todo} na lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Você atualizou a tarefa {todo} na lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} tarefa resolvida {todo} na lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Você terminou a tarefa {todo} na lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabriu tarefa {todo} na lista {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Você reabriu a terefa {todo} na lista {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} criou a tarefa {todo} na lista {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Você criou a tarefa {todo} na lista {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} excluiu a tarefa {todo} da lista {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Você excluiu a tarefa {todo} da lista {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} atualizou a tarefa {todo} na lista {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Você atualizou a tarefa {todo} na lista {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} resolveu a tarefa {todo} na lista {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Você resolveu a tarefa {todo} na lista {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} reabriu a tarefa {todo} na lista {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Você reabriu a tarefa {todo} na lista {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} moveu a tarefa {todo} da lista {sourceCalendar} para a lista {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Você moveu a tarefa {todo} da lista {sourceCalendar} para a lista {targetCalendar}",
"Calendar, contacts and tasks" : "Calendário, contatos e tarefas",
"A <strong>calendar</strong> was modified" : "Um <strong>calendário</strong> foi modificado",
- "A calendar <strong>event</strong> was modified" : "Um <strong>evento</strong> do calendário foi modificado",
- "A calendar <strong>todo</strong> was modified" : "Uma <strong>tarefa</strong> do calendário foi modificada",
+ "A calendar <strong>event</strong> was modified" : "Um <strong>evento</strong> de calendário foi modificado",
+ "A calendar <strong>to-do</strong> was modified" : "Uma <strong>tarefa</strong> de calendário foi modificada",
"Contact birthdays" : "Aniversário dos contatos",
"Death of %s" : "Morte de %s",
+ "Untitled calendar" : "Calendário sem título",
"Calendar:" : "Calendário:",
"Date:" : "Data:",
"Where:" : "Onde:",
"Description:" : "Descrição:",
- "Untitled event" : "Evento sem título",
- "_%n year_::_%n years_" : ["%n ano","%n anos"],
- "_%n month_::_%n months_" : ["%n mês","%n meses"],
- "_%n day_::_%n days_" : ["%n dia","%n dias"],
- "_%n hour_::_%n hours_" : ["%n hora","%n horas"],
- "_%n minute_::_%n minutes_" : ["%n minuto","%n minutos"],
+ "_%n year_::_%n years_" : ["%n ano","%n anos","%n anos"],
+ "_%n month_::_%n months_" : ["%n mês","%n meses","%n meses"],
+ "_%n day_::_%n days_" : ["%n dia","%n dias","%n dias"],
+ "_%n hour_::_%n hours_" : ["%n hora","%n horas","%n horas"],
+ "_%n minute_::_%n minutes_" : ["%n minuto","%n minutos","%n minutos"],
"%s (in %s)" : "%s (em %s)",
"%s (%s ago)" : "%s (%s atrás)",
"Calendar: %s" : "Calendário: %s",
@@ -67,95 +72,251 @@ OC.L10N.register(
"Description: %s" : "Descrição: %s",
"Where: %s" : "Onde: %s",
"%1$s via %2$s" : "%1$s via %2$s",
+ "In the past on %1$s for the entire day" : "No passado em %1$s para o dia inteiro",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Em um minuto em %1$s para o dia inteiro","Em %n minutos em %1$s para o dia inteiro","Em %n minutos em %1$s para o dia inteiro"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Em uma hora em %1$s para o dia inteiro","Em %n horas em %1$s para o dia inteiro","Em %n horas em %1$s para o dia inteiro"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Em um dias em %1$s para o dia inteiro","Em %n dias em %1$s para o dia inteiro","Em %n dias em %1$s para o dia inteiro"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Em uma semana em %1$s para o dia inteiro","Em %n semanas em %1$s para o dia inteiro","Em %n semanas em %1$s para o dia inteiro"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Em um mês em %1$s para o dia inteiro","Em %n meses em %1$s para o dia inteiro","Em %n meses em %1$s para o dia inteiro"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Em um ano em %1$s para o dia inteiro","Em %n anos em %1$s para o dia inteiro","Em %n anos em %1$s para o dia inteiro"],
+ "In the past on %1$s between %2$s - %3$s" : "No passado em %1$s entre %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Em um minuto em %1$s entre %2$s - %3$s","Em %n minutos em %1$s entre %2$s - %3$s","Em %n minutos em %1$s entre %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Em uma hora em %1$s entre %2$s - %3$s","Em %n horas em %1$s entre %2$s - %3$s","Em %n horas em %1$s entre %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Em um dia em %1$s entre %2$s - %3$s","Em %n dias em %1$s entre %2$s - %3$s","Em %n dias em %1$s entre %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Em uma semana em %1$s entre %2$s - %3$s","Em %n semanas em %1$s entre %2$s - %3$s","Em %n semanas em %1$s entre %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Em um mês em %1$s entre %2$s - %3$s","Em %n meses em %1$s entre %2$s - %3$s","Em %n meses em %1$s entre %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Em um ano em %1$s entre %2$s - %3$s","Em %n anos em %1$s entre %2$s - %3$s","Em %n anos em %1$s entre %2$s - %3$s"],
+ "Could not generate when statement" : "Não foi possível gerar a indicação sobre quando",
+ "Every Day for the entire day" : "Todo Dia durante o dia inteiro",
+ "Every Day for the entire day until %1$s" : "Todo Dia para o dia inteiro até %1$s",
+ "Every Day between %1$s - %2$s" : "Todo Dia entre %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Todo Dia entre %1$s - %2$s até %3$s",
+ "Every %1$d Days for the entire day" : "A Cada %1$d Dias para o dia inteiro",
+ "Every %1$d Days for the entire day until %2$s" : "A Cada %1$d Dias para o dia inteiro até %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "A Cada %1$d Dias entre %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "A Cada %1$d Dias entre %2$s - %3$s até %4$s",
+ "Could not generate event recurrence statement" : "Não foi possível gerar a indicação de recorrência do evento",
+ "Every Week on %1$s for the entire day" : "Toda Semana, %1$s para o dia inteiro",
+ "Every Week on %1$s for the entire day until %2$s" : "Toda Semana, %1$s para o dia inteiro até %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Toda Semana, %1$s entre %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Toda Semana, %1$s entre %2$s - %3$s até %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "A Cada %1$d Semanas, %2$s para o dia inteiro",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "A Cada %1$d Semanas, %2$s para o dia inteiro até %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "A Cada %1$d Semanas, %2$s entre %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "A Cada %1$d Semanas, %2$s entre %3$s - %4$s até %5$s",
+ "Every Month on the %1$s for the entire day" : "Todo Mês, dia: %1$s, para o dia inteiro",
+ "Every Month on the %1$s for the entire day until %2$s" : "Todo Mês, dia: %1$s, para o dia inteiro até %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Todo Mês, dia: %1$s, entre %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Todo Mês, dia: %1$s, entre %2$s - %3$s até %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "A Cada %1$d Meses, dia: %2$s, para o dia inteiro",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "A Cada %1$d Meses, dia: %2$s, para o dia inteiro até %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "A Cada %1$d Meses, dia: %2$s, entre %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "A Cada %1$d Meses, dia: %2$s, entre %3$s - %4$s até %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Todo Ano em %1$s, dia: %2$s, para o dia inteiro",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Todo Ano em %1$s, dia: %2$s, para o dia inteiro até %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Todo Ano em %1$s, dia: %2$s, entre %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Todo Ano em %1$s, dia: %2$s, entre %3$s - %4$s até %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "A Cada %1$d Anos em %2$s, dia: %3$s, para o dia inteiro",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "A Cada %1$d Anos em %2$s, dia: %3$s, para o dia inteiro até %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "A Cada %1$d Anos em %2$s, dia: %3$s, entre %4$s - %5$s ",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "A Cada %1$d Anos em %2$s, dia: %3$s, entre %4$s - %5$s até %6$s",
+ "On specific dates for the entire day until %1$s" : "Em datas específicas para o dia inteiro até %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "Em datas específicas entre %1$s - %2$s até %3$s",
+ "In the past on %1$s" : "No passado em %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Em um minuto em %1$s","Em %n minutos em %1$s","Em %n minutos em %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Em uma hora em %1$s","Em %n horas em %1$s","Em %n horas em %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Em um dia em %1$s","Em %n dias em %1$s","Em %n dias em %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Em uma semana em %1$s","Em %n semanas em %1$s","Em %n semanas em %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Em um mês em %1$s","Em %n meses em %1$s","Em %n meses em %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Em um ano em %1$s","Em %n anos em %1$s","Em %n anos em %1$s"],
+ "In the past on %1$s then on %2$s" : "No passado em %1$s e depois em %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Em um minuto em %1$s e depois em %2$s","Em %n minutos em %1$s e depois em %2$s","Em %n minutos em %1$s e depois em %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Em um hora em %1$s e depois em %2$s","Em %n horas em %1$s e depois em %2$s","Em %n horas em %1$s e depois em %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Em um dia em %1$s e depois em %2$s","Em %n dias em %1$s e depois em %2$s","Em %n dias em %1$s e depois em %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Em uma semana em %1$s e depois em %2$s","Em %n semanas em %1$s e depois em %2$s","Em %n semanas em %1$s e depois em %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Em um mês em %1$s e depois em %2$s","Em %n meses em %1$s e depois em %2$s","Em %n meses em %1$s e depois em %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Em um ano em %1$s e depois em %2$s","Em %n anos em %1$s e depois em %2$s","Em %n anos em %1$s e depois em %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "No passado em %1$s e depois em %2$s e %3$s",
+ "_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_" : ["Em um minuto e %1$s e depois em %2$s e %3$s","Em %n minutos e %1$s e depois em %2$s e %3$s","Em %n minutos e %1$s e depois em %2$s e %3$s"],
+ "_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_" : ["Em uma hora e %1$s e depois em %2$s e %3$s","Em %n horas e %1$s e depois em %2$s e %3$s","Em %n horas e %1$s e depois em %2$s e %3$s"],
+ "_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_" : ["Em um dia e %1$s e depois em %2$s e %3$s","Em %n dias e %1$s e depois em %2$s e %3$s","Em %n dias e %1$s e depois em %2$s e %3$s"],
+ "_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_" : ["Em uma semana e %1$s e depois em %2$s e %3$s","Em %n semanas e %1$s e depois em %2$s e %3$s","Em %n semanas e %1$s e depois em %2$s e %3$s"],
+ "_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_" : ["Em um mês e %1$s e depois em %2$s e %3$s","Em %n meses e %1$s e depois em %2$s e %3$s","Em %n meses e %1$s e depois em %2$s e %3$s"],
+ "_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_" : ["Em um ano e %1$s e depois em %2$s e %3$s","Em %n anos e %1$s e depois em %2$s e %3$s","Em %n anos e %1$s e depois em %2$s e %3$s"],
+ "Could not generate next recurrence statement" : "Não foi possível gerar a indicação da próxima recorrência",
"Cancelled: %1$s" : "Cancelado: %1$s",
- "Invitation canceled" : "Convite cancelado",
- "Re: %1$s" : "Remetente: %1$s",
- "Invitation updated" : "Convite atualizado",
+ "\"%1$s\" has been canceled" : "\"%1$s\" foi cancelado",
+ "Re: %1$s" : "Re: %1$s",
+ "%1$s has accepted your invitation" : "%1$s aceitou seu convite",
+ "%1$s has tentatively accepted your invitation" : "%1$s aceitou provisoriamente seu convite",
+ "%1$s has declined your invitation" : "%1$s recusou seu convite",
+ "%1$s has responded to your invitation" : "%1$s respondeu ao seu convite",
+ "Invitation updated: %1$s" : "Convite atualizado: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s atualizou o evento \"%2$s\"",
"Invitation: %1$s" : "Convite: %1$s",
- "Invitation" : "Convite",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s gostaria de convidar você para \"%2$s\"",
+ "Organizer:" : "Organizador:",
+ "Attendees:" : "Participantes:",
"Title:" : "Título:",
- "Time:" : "Horário:",
+ "When:" : "Quando:",
"Location:" : "Localização:",
"Link:" : "Link:",
- "Organizer:" : "Organizador:",
- "Attendees:" : "Participantes:",
+ "Occurring:" : "Ocorrendo:",
"Accept" : "Aceitar",
"Decline" : "Rejeitar",
"More options …" : "Mais opções...",
"More options at %s" : "Mais opções em %s",
+ "Monday" : "Segunda-feira",
+ "Tuesday" : "Terça-feira",
+ "Wednesday" : "Quarta-feira",
+ "Thursday" : "Quinta-feira",
+ "Friday" : "Sexta-feira",
+ "Saturday" : "Sábado ",
+ "Sunday" : "Domingo",
+ "January" : "Janeiro",
+ "February" : "Fevereiro",
+ "March" : "Março",
+ "April" : "Abril",
+ "May" : "Maio",
+ "June" : "Junho",
+ "July" : "Julho",
+ "August" : "Agosto",
+ "September" : "Setembro",
+ "October" : "Outubro",
+ "November" : "Novembro",
+ "December" : "Dezembro",
+ "First" : "1.º/ª",
+ "Second" : "2.º/ª",
+ "Third" : "3.º/ª",
+ "Fourth" : "4.º/ª",
+ "Fifth" : "5.º/ª",
+ "Last" : "Último/a",
+ "Second Last" : "Penúltimo/a",
+ "Third Last" : "3.º/ª Último/a",
+ "Fourth Last" : "4.º/ª Último/a",
+ "Fifth Last" : "5.º/ª Último/a",
"Contacts" : "Contatos",
- "{actor} created address book {addressbook}" : "{actor} criou o livro de endereço {addressbook}",
+ "{actor} created address book {addressbook}" : "{actor} criou o catálogo de endereços {addressbook}",
"You created address book {addressbook}" : "Você criou o catálogo de endereços {addressbook}",
- "{actor} deleted address book {addressbook}" : "{actor} apagou o livro de endereços {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} excluiu o catálogo de endereços {addressbook}",
"You deleted address book {addressbook}" : "Você excluiu o catálogo de endereços {addressbook}",
- "{actor} updated address book {addressbook}" : "{actor} atualizou o livro de endereço {addressbook}",
- "You updated address book {addressbook}" : "Você atualizou o livro de endereço {addressbook}",
- "{actor} shared address book {addressbook} with you" : "{actor} compartilhou o livro de endereço {addressbook} com você",
+ "{actor} updated address book {addressbook}" : "{actor} atualizou o catálogo de endereços {addressbook}",
+ "You updated address book {addressbook}" : "Você atualizou o catálogo de endereços {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} compartilhou o catálogo de endereços {addressbook} com você",
"You shared address book {addressbook} with {user}" : "Você compartilhou o catálogo de endereços {addressbook} com {user}",
- "{actor} shared address book {addressbook} with {user}" : "{actor} compartilhou o livro de endereço {addressbook} com {user}",
- "{actor} unshared address book {addressbook} from you" : "{actor} retirou o compartilhamento do livro de endereço {addressbook} com você",
- "You unshared address book {addressbook} from {user}" : "Você retirou o compartilhamento do livro de endereço {addressbook} com {user}",
- "{actor} unshared address book {addressbook} from {user}" : "{actor} retirou o compartilhamento do livro de endereço {addressbook} com {user}",
- "{actor} unshared address book {addressbook} from themselves" : "{actor} retirou o compartilhamento do livro de endereço {addressbook} com ele mesmo",
- "You shared address book {addressbook} with group {group}" : "Você compartilhou o livro de endereço {addressbook} com o grupo {group}",
- "{actor} shared address book {addressbook} with group {group}" : "{actor} compartilhou o livro de endereço {addressbook} com o grupo {group}",
- "You unshared address book {addressbook} from group {group}" : "Você retirou o compartilhamento do livro de enredeços {addressbook} com o grupo {group}",
- "{actor} unshared address book {addressbook} from group {group}" : "{actor} retirou o compartilhamento do livro de endereço {addressbook} com o grupo {group}",
- "{actor} created contact {card} in address book {addressbook}" : "{actor} criou o contato {card} no livro de enrereço {addressbook}",
- "You created contact {card} in address book {addressbook}" : "Você criou o contato {card} no livro de endereço {addressbook}",
- "{actor} deleted contact {card} from address book {addressbook}" : "{actor} excluiu o contato {card} do livro de endereço {addressbook}",
- "You deleted contact {card} from address book {addressbook}" : "Você apagou o contato {card} do livro de endereços {addressbook}",
- "{actor} updated contact {card} in address book {addressbook}" : "{actor} updated contact {card} no livro de endereço {addressbook}",
- "You updated contact {card} in address book {addressbook}" : "Você atualizou o contato {card} no livro de endereços {addressbook}",
- "A <strong>contact</strong> or <strong>address book</strong> was modified" : "O <strong>contato</strong> ou <strong>livro de endereço</strong> foi modificado",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} compartilhou o catálogo de endereços {addressbook} com {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} retirou o compartilhamento do catálogo de endereços {addressbook} de você",
+ "You unshared address book {addressbook} from {user}" : "Você retirou o compartilhamento do catálogo de endereços {addressbook} de {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} retirou o compartilhamento do catálogo de endereços {addressbook} de {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} retirou o compartilhamento do catálogo de endereços {addressbook} de si mesmo",
+ "You shared address book {addressbook} with group {group}" : "Você compartilhou o catálogo de endereços {addressbook} com o grupo {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} compartilhou o catálogo de endereços {addressbook} com o grupo {group}",
+ "You unshared address book {addressbook} from group {group}" : "Você retirou o compartilhamento do catálogo de endereços {addressbook} do grupo {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} retirou o compartilhamento do catálogo de endereços {addressbook} do grupo {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} criou o contato {card} no catálogo de endereços {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Você criou o contato {card} no catálogo de endereços {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} excluiu o contato {card} do catálogo de endereços {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Você exluiu o contato {card} do catálogo de endereços {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} atualizou o contato {card} no catálogo de endereços {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Você atualizou o contato {card} no catálogo de endereços {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Um <strong>contato</strong> ou <strong>catálogo de endereços</strong> foi modificado",
+ "Accounts" : "Contas",
+ "System address book which holds all accounts" : "Catálogo de endereços do sistema que contém todas as contas",
"File is not updatable: %1$s" : "O arquivo não é atualizável: %1$s",
- "Could not write to final file, canceled by hook" : "Não foi possível gravar no arquivo final, cancelado pelo gancho",
+ "Failed to get storage for file" : "Falha ao obter armazenamento para arquivo",
+ "Could not write to final file, canceled by hook" : "Não foi possível gravar no arquivo final, cancelado pelo hook",
"Could not write file contents" : "Não foi possível gravar o conteúdo do arquivo",
- "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes","%n bytes"],
"Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Erro ao copiar o arquivo para o local de destino (copiado: %1$s, tamanho de arquivo esperado: %2$s)",
- "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." : "Tamanho de arquivo esperado de %1$s mas lido (do cliente Nextcloud) e gravado (no armazenamento Nextcloud) %2$s. Pode ser um problema de rede no lado de envio ou um problema de gravação no armazenamento no lado do servidor.",
- "Could not rename part file to final file, canceled by hook" : "Não foi possível renomear o arquivo de parte para o arquivo final, cancelado pelo gancho",
- "Could not rename part file to final file" : "Não foi possível renomear o arquivo de parte para o arquivo final",
+ "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." : "Tamanho de arquivo esperado de %1$s, mas lido (do cliente Nextcloud) e gravado (no armazenamento Nextcloud) %2$s. Pode ser um problema de rede no lado de envio ou um problema de gravação no armazenamento no lado do servidor.",
+ "Could not rename part file to final file, canceled by hook" : "Não foi possível renomear o arquivo de peça para o arquivo final, cancelado pelo hook",
+ "Could not rename part file to final file" : "Não foi possível renomear o arquivo de peça para o arquivo final",
"Failed to check file size: %1$s" : "Falha ao verificar o tamanho do arquivo: %1$s",
- "Could not open file" : "Não pode abrir o arquivo",
+ "Could not open file: %1$s, file does seem to exist" : "Não foi possível abrir o arquivo: %1$s, o arquivo parece existir",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Não foi possível abrir o arquivo: %1$s, o arquivo parece não existir",
"Encryption not ready: %1$s" : "A criptografia não está pronta: %1$s",
"Failed to open file: %1$s" : "Falha ao abrir arquivo: %1$s",
"Failed to unlink: %1$s" : "Falha ao desvincular: %1$s",
- "Invalid chunk name" : "Nome do bloco inválido",
- "Could not rename part file assembled from chunks" : "Não foi possível renomear parte do arquivo montado a partir de pedaços",
"Failed to write file contents: %1$s" : "Falha ao gravar o conteúdo do arquivo:%1$s",
- "File not found: %1$s" : "Arquivo não encontrado:%1$s",
+ "File not found: %1$s" : "Arquivo não encontrado: %1$s",
+ "Invalid target path" : "Caminho de destino inválido",
"System is in maintenance mode." : "O sistema está em modo de manutenção",
"Upgrade needed" : "Upgrade necessário",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Seu %s precisa estar configurado para usar HTTPS a fim de usar o CalDAV e o CardDAV com o iOS/macOS.",
"Configures a CalDAV account" : "Configure uma conta CalDAV",
"Configures a CardDAV account" : "Configure uma conta CardDAV",
"Events" : "Eventos",
- "Tasks" : "Tarefas",
"Untitled task" : "Tarefa sem título",
"Completed on %s" : "Concluída em %s",
"Due on %s by %s" : "Vence em %s até %s",
"Due on %s" : "Vence em %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Bem-vindo ao Nextcloud Calendário!\n\nEste é um exemplo de evento - explore a flexibilidade do planejamento com o Nextcloud Calendário fazendo as edições que desejar!\n\nCom o Nextcloud Calendário, você pode:\n- Criar, editar e gerenciar eventos sem esforço.\n- Criar vários calendários e compartilhá-los com colegas de equipe, amigos ou familiares.\n- Verificar a disponibilidade e exibir seus horários de trabalho para outras pessoas.\n- Integrá-lo perfeitamente com aplicativos e dispositivos via CalDAV.\n- Personalizar sua experiência: agende eventos recorrentes, ajuste as notificações e outras configurações.",
+ "Example event - open me!" : "Exemplo de evento - abra-me!",
+ "System Address Book" : "Catálogo de Endereços do Sistema",
+ "The system address book contains contact information for all users in your instance." : "O catálogo de endereços do sistema contém informações de contato de todos os usuários da sua instância.",
+ "Enable System Address Book" : "Ativar Catálogo de Endereços do Sistema",
+ "DAV system address book" : "Catálogo de endereços DAV do sistema",
+ "No outstanding DAV system address book sync." : "Não há sincronização pendente do catálogo de endereços DAV do sistema.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "A sincronização do catálogo de endereços DAV do sistema ainda não foi executada porque sua instância tem mais de 1000 usuários ou porque ocorreu um erro. Execute-a manualmente chamando \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "Endpoint WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Não foi possível verificar se o seu servidor web está configurado corretamente para permitir a sincronização de arquivos via WebDAV. Verifique manualmente.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Seu servidor web ainda não está configurado corretamente para permitir a sincronização de arquivos, porque a interface do WebDAV parece estar quebrada.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Seu servidor web está configurado corretamente para permitir a sincronização de arquivos via WebDAV.",
"Migrated calendar (%1$s)" : "Calendário migrado (%1$s)",
"Calendars including events, details and attendees" : "Calendários, incluindo eventos, detalhes e participantes",
"Contacts and groups" : "Contatos e grupos",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "Endpoint WebDAV",
- "Availability" : "Disponibilidade",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Se você configurar seu horário de trabalho, outros usuários verão quando você estiver fora do escritório quando marcarem uma reunião. ",
+ "Absence saved" : "Ausência salva",
+ "Failed to save your absence settings" : "Falha ao salvar suas configurações de ausência",
+ "Absence cleared" : "Limpou a ausência",
+ "Failed to clear your absence settings" : "Falha ao limpar suas configurações de ausência",
+ "First day" : "Primeiro dia",
+ "Last day (inclusive)" : "Último dia (inclusive)",
+ "Out of office replacement (optional)" : "Substituto durante sua ausência (opcional)",
+ "Name of the replacement" : "Nome do substituto",
+ "No results." : "Nenhum resultado.",
+ "Start typing." : "Comece a digitar.",
+ "Short absence status" : "Status curto sobre a ausência",
+ "Long absence Message" : "Mensagem longa sobre a ausência",
+ "Save" : "Salvar",
+ "Disable absence" : "Desativar ausência",
+ "Failed to load availability" : "Falha ao carregar a disponibilidade",
+ "Saved availability" : "Disponibilidade salva",
+ "Failed to save availability" : "Falha ao salvar a disponibilidade",
"Time zone:" : "Fuso horário:",
"to" : "para",
"Delete slot" : "Excluir slot",
"No working hours set" : "Sem horário de trabalho definido",
"Add slot" : "Adicionar slot ",
- "Monday" : "Segunda-feira",
- "Tuesday" : "Terça-feira",
- "Wednesday" : "Quarta-feira",
- "Thursday" : "Quinta-feira",
- "Friday" : "Sexta-feira",
- "Saturday" : "Sábado ",
- "Sunday" : "Domingo",
- "Save" : "Salvar",
+ "Weekdays" : "Dias da semana",
+ "Pick a start time for {dayName}" : "Selecione um horário de início para {dayName}",
+ "Pick a end time for {dayName}" : "Selecione um horário de fim para {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Defina automaticamente o status do usuário como \"Não perturbe\" fora de disponibilidade para silenciar todas as notificações.",
+ "Cancel" : "Cancelar",
+ "Import" : "Importar",
+ "Error while saving settings" : "Erro ao salvar as configurações",
+ "Contact reset successfully" : "O contato foi reiniciado com sucesso",
+ "Error while resetting contact" : "Erro ao reiniciar o contato",
+ "Contact imported successfully" : "O contato foi importado com sucesso",
+ "Error while importing contact" : "Erro ao importar o contato",
+ "Import contact" : "Importar contato",
+ "Reset to default" : "Redefinir para o padrão",
+ "Import contacts" : "Importar contatos",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "A importação de um novo arquivo .vcf excluirá o contato padrão existente e o substituirá pelo novo. Deseja continuar?",
+ "Failed to save example event creation setting" : "Falha ao salvar a configuração de criação de evento de exemplo",
+ "Failed to upload the example event" : "Falha ao fazer upload do evento de exemplo",
+ "Custom example event was saved successfully" : "O evento de exemplo personalizado foi salvo com êxito",
+ "Failed to delete the custom example event" : "Falha ao excluir o evento de exemplo personalizado",
+ "Custom example event was deleted successfully" : "O evento de exemplo personalizado foi excluído com êxito",
+ "Import calendar event" : "Importar evento de calendário",
+ "Uploading a new event will overwrite the existing one." : "O upload de um novo evento substituirá o existente.",
+ "Upload event" : "Fazer upload do evento",
+ "Availability" : "Disponibilidade",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Se você configurar seu horário de trabalho, outras pessoas verão quando você estiver ausente quando agendarem uma reunião.",
+ "Absence" : "Ausência",
+ "Configure your next absence period." : "Configure seu próximo período de ausência",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instale também o {calendarappstoreopen}aplicativo de calendário{linkclose} ou {calendardocopen}conecte sua área de trabalho e celular à sincronização ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Certifique-se de configurar corretamente {emailopen}o servidor de e-mail{linkclose}.",
"Calendar server" : "Servidor de calendário",
"Send invitations to attendees" : "Enviar convites aos participantes",
"Automatically generate a birthday calendar" : "Gerar um calendário de aniversários automaticamente",
@@ -163,18 +324,15 @@ OC.L10N.register(
"Hence they will not be available immediately after enabling but will show up after some time." : "Portanto, eles não estarão disponíveis imediatamente após a ativação, mas depois de algum tempo.",
"Send notifications for events" : "Enviar notificações para eventos",
"Notifications are sent via background jobs, so these must occur often enough." : "As notificações são enviadas via trabalhos em segundo plano, portanto, elas devem ocorrer com frequência suficiente.",
- "Send reminder notifications to calendar sharees as well" : "Envie notificações de lembrete para compartilhamentos de calendário também",
- "Reminders are always sent to organizers and attendees." : "Os lembretes são sempre enviados aos organizadores e participantes.",
+ "Send reminder notifications to calendar sharees as well" : "Enviar notificações de lembrete também para os compartilhadores de calendário",
+ "Reminders are always sent to organizers and attendees." : "Lembretes são sempre enviados aos organizadores e participantes.",
"Enable notifications for events via push" : "Ativar notificações para eventos via push",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instale também o {calendarappstoreopen}aplicativo de calendário{linkclose} ou {calendardocopen}conecte sua área de trabalho e celular à sincronização ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Certifique-se de configurar corretamente {emailopen}o servidor de e-mail{linkclose}.",
+ "Example content" : "Exemplo de conteúdo",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "O conteúdo de exemplo serve para mostrar os recursos do Nextcloud. O conteúdo padrão é fornecido com o Nextcloud e pode ser substituído por conteúdo personalizado.",
"There was an error updating your attendance status." : "Houve um erro ao atualizar seu status de participação.",
"Please contact the organizer directly." : "Por favor, contate o organizador diretamente.",
"Are you accepting the invitation?" : "Você está aceitando o convite?",
"Tentative" : "Tentativa",
- "Number of guests" : "Número de convidados",
- "Comment" : "Comentário",
- "Your attendance was updated successfully." : "Sua presença foi atualizada com sucesso.",
- "Calendar and tasks" : "Calendário e tarefas"
+ "Your attendance was updated successfully." : "Sua presença foi atualizada com sucesso."
},
-"nplurals=2; plural=(n > 1);");
+"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;");
diff --git a/apps/dav/l10n/pt_BR.json b/apps/dav/l10n/pt_BR.json
index 3f02d503ab0..40b01efc812 100644
--- a/apps/dav/l10n/pt_BR.json
+++ b/apps/dav/l10n/pt_BR.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Calendário",
- "Todos" : "Tarefas",
+ "Tasks" : "Tarefas",
"Personal" : "Pessoal",
"{actor} created calendar {calendar}" : "{actor} criou o calendário {calendar}",
"You created calendar {calendar}" : "Você criou o calendário {calendar}",
@@ -23,41 +23,46 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} compartilhou o calendário {calendar} com o grupo {group}",
"You unshared calendar {calendar} from group {group}" : "Você descompartilhou o calendário {calendar} do grupo {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} descompartilhou o calendário {calendar} do grupo {group}",
+ "Untitled event" : "Evento sem título",
"{actor} created event {event} in calendar {calendar}" : "{actor} criou o evento {event} no calendário {calendar}",
"You created event {event} in calendar {calendar}" : "Você criou o evento {event} no calendário {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} excluiu o evento {event} do calendário {calendar}",
"You deleted event {event} from calendar {calendar}" : "Você excluiu o evento {event} do calendário {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} atualizou o evento {event} no calendário {calendar}",
"You updated event {event} in calendar {calendar}" : "Você atualizou o evento {event} no calendário {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} moveu o evento {event} do calendário {sourceCalendar} para o calendário {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Você moveu o evento {event} do calendário {sourceCalendar} para o calendário {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} restaurou o evento {event} do calendário {calendar}",
"You restored event {event} of calendar {calendar}" : "Você restaurou o evento {event} do calendário {calendar}",
"Busy" : "Ocupado",
- "{actor} created todo {todo} in list {calendar}" : "{actor} criou a tarefa {todo} na lista {calendar}",
- "You created todo {todo} in list {calendar}" : "Você criou a tarefa {todo} na lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} excluiu a tarefa {todo} da lista {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Você excluiu a tarefa {todo} da lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} atualizou a tarefa {todo} na lista {calendar}",
- "You updated todo {todo} in list {calendar}" : "Você atualizou a tarefa {todo} na lista {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} tarefa resolvida {todo} na lista {calendar}",
- "You solved todo {todo} in list {calendar}" : "Você terminou a tarefa {todo} na lista {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} reabriu tarefa {todo} na lista {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Você reabriu a terefa {todo} na lista {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} criou a tarefa {todo} na lista {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Você criou a tarefa {todo} na lista {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} excluiu a tarefa {todo} da lista {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Você excluiu a tarefa {todo} da lista {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} atualizou a tarefa {todo} na lista {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Você atualizou a tarefa {todo} na lista {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} resolveu a tarefa {todo} na lista {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Você resolveu a tarefa {todo} na lista {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} reabriu a tarefa {todo} na lista {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Você reabriu a tarefa {todo} na lista {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} moveu a tarefa {todo} da lista {sourceCalendar} para a lista {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Você moveu a tarefa {todo} da lista {sourceCalendar} para a lista {targetCalendar}",
"Calendar, contacts and tasks" : "Calendário, contatos e tarefas",
"A <strong>calendar</strong> was modified" : "Um <strong>calendário</strong> foi modificado",
- "A calendar <strong>event</strong> was modified" : "Um <strong>evento</strong> do calendário foi modificado",
- "A calendar <strong>todo</strong> was modified" : "Uma <strong>tarefa</strong> do calendário foi modificada",
+ "A calendar <strong>event</strong> was modified" : "Um <strong>evento</strong> de calendário foi modificado",
+ "A calendar <strong>to-do</strong> was modified" : "Uma <strong>tarefa</strong> de calendário foi modificada",
"Contact birthdays" : "Aniversário dos contatos",
"Death of %s" : "Morte de %s",
+ "Untitled calendar" : "Calendário sem título",
"Calendar:" : "Calendário:",
"Date:" : "Data:",
"Where:" : "Onde:",
"Description:" : "Descrição:",
- "Untitled event" : "Evento sem título",
- "_%n year_::_%n years_" : ["%n ano","%n anos"],
- "_%n month_::_%n months_" : ["%n mês","%n meses"],
- "_%n day_::_%n days_" : ["%n dia","%n dias"],
- "_%n hour_::_%n hours_" : ["%n hora","%n horas"],
- "_%n minute_::_%n minutes_" : ["%n minuto","%n minutos"],
+ "_%n year_::_%n years_" : ["%n ano","%n anos","%n anos"],
+ "_%n month_::_%n months_" : ["%n mês","%n meses","%n meses"],
+ "_%n day_::_%n days_" : ["%n dia","%n dias","%n dias"],
+ "_%n hour_::_%n hours_" : ["%n hora","%n horas","%n horas"],
+ "_%n minute_::_%n minutes_" : ["%n minuto","%n minutos","%n minutos"],
"%s (in %s)" : "%s (em %s)",
"%s (%s ago)" : "%s (%s atrás)",
"Calendar: %s" : "Calendário: %s",
@@ -65,95 +70,251 @@
"Description: %s" : "Descrição: %s",
"Where: %s" : "Onde: %s",
"%1$s via %2$s" : "%1$s via %2$s",
+ "In the past on %1$s for the entire day" : "No passado em %1$s para o dia inteiro",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Em um minuto em %1$s para o dia inteiro","Em %n minutos em %1$s para o dia inteiro","Em %n minutos em %1$s para o dia inteiro"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Em uma hora em %1$s para o dia inteiro","Em %n horas em %1$s para o dia inteiro","Em %n horas em %1$s para o dia inteiro"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Em um dias em %1$s para o dia inteiro","Em %n dias em %1$s para o dia inteiro","Em %n dias em %1$s para o dia inteiro"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Em uma semana em %1$s para o dia inteiro","Em %n semanas em %1$s para o dia inteiro","Em %n semanas em %1$s para o dia inteiro"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Em um mês em %1$s para o dia inteiro","Em %n meses em %1$s para o dia inteiro","Em %n meses em %1$s para o dia inteiro"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Em um ano em %1$s para o dia inteiro","Em %n anos em %1$s para o dia inteiro","Em %n anos em %1$s para o dia inteiro"],
+ "In the past on %1$s between %2$s - %3$s" : "No passado em %1$s entre %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Em um minuto em %1$s entre %2$s - %3$s","Em %n minutos em %1$s entre %2$s - %3$s","Em %n minutos em %1$s entre %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Em uma hora em %1$s entre %2$s - %3$s","Em %n horas em %1$s entre %2$s - %3$s","Em %n horas em %1$s entre %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Em um dia em %1$s entre %2$s - %3$s","Em %n dias em %1$s entre %2$s - %3$s","Em %n dias em %1$s entre %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Em uma semana em %1$s entre %2$s - %3$s","Em %n semanas em %1$s entre %2$s - %3$s","Em %n semanas em %1$s entre %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Em um mês em %1$s entre %2$s - %3$s","Em %n meses em %1$s entre %2$s - %3$s","Em %n meses em %1$s entre %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Em um ano em %1$s entre %2$s - %3$s","Em %n anos em %1$s entre %2$s - %3$s","Em %n anos em %1$s entre %2$s - %3$s"],
+ "Could not generate when statement" : "Não foi possível gerar a indicação sobre quando",
+ "Every Day for the entire day" : "Todo Dia durante o dia inteiro",
+ "Every Day for the entire day until %1$s" : "Todo Dia para o dia inteiro até %1$s",
+ "Every Day between %1$s - %2$s" : "Todo Dia entre %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Todo Dia entre %1$s - %2$s até %3$s",
+ "Every %1$d Days for the entire day" : "A Cada %1$d Dias para o dia inteiro",
+ "Every %1$d Days for the entire day until %2$s" : "A Cada %1$d Dias para o dia inteiro até %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "A Cada %1$d Dias entre %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "A Cada %1$d Dias entre %2$s - %3$s até %4$s",
+ "Could not generate event recurrence statement" : "Não foi possível gerar a indicação de recorrência do evento",
+ "Every Week on %1$s for the entire day" : "Toda Semana, %1$s para o dia inteiro",
+ "Every Week on %1$s for the entire day until %2$s" : "Toda Semana, %1$s para o dia inteiro até %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Toda Semana, %1$s entre %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Toda Semana, %1$s entre %2$s - %3$s até %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "A Cada %1$d Semanas, %2$s para o dia inteiro",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "A Cada %1$d Semanas, %2$s para o dia inteiro até %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "A Cada %1$d Semanas, %2$s entre %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "A Cada %1$d Semanas, %2$s entre %3$s - %4$s até %5$s",
+ "Every Month on the %1$s for the entire day" : "Todo Mês, dia: %1$s, para o dia inteiro",
+ "Every Month on the %1$s for the entire day until %2$s" : "Todo Mês, dia: %1$s, para o dia inteiro até %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Todo Mês, dia: %1$s, entre %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Todo Mês, dia: %1$s, entre %2$s - %3$s até %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "A Cada %1$d Meses, dia: %2$s, para o dia inteiro",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "A Cada %1$d Meses, dia: %2$s, para o dia inteiro até %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "A Cada %1$d Meses, dia: %2$s, entre %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "A Cada %1$d Meses, dia: %2$s, entre %3$s - %4$s até %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Todo Ano em %1$s, dia: %2$s, para o dia inteiro",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Todo Ano em %1$s, dia: %2$s, para o dia inteiro até %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Todo Ano em %1$s, dia: %2$s, entre %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Todo Ano em %1$s, dia: %2$s, entre %3$s - %4$s até %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "A Cada %1$d Anos em %2$s, dia: %3$s, para o dia inteiro",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "A Cada %1$d Anos em %2$s, dia: %3$s, para o dia inteiro até %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "A Cada %1$d Anos em %2$s, dia: %3$s, entre %4$s - %5$s ",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "A Cada %1$d Anos em %2$s, dia: %3$s, entre %4$s - %5$s até %6$s",
+ "On specific dates for the entire day until %1$s" : "Em datas específicas para o dia inteiro até %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "Em datas específicas entre %1$s - %2$s até %3$s",
+ "In the past on %1$s" : "No passado em %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Em um minuto em %1$s","Em %n minutos em %1$s","Em %n minutos em %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Em uma hora em %1$s","Em %n horas em %1$s","Em %n horas em %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Em um dia em %1$s","Em %n dias em %1$s","Em %n dias em %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Em uma semana em %1$s","Em %n semanas em %1$s","Em %n semanas em %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Em um mês em %1$s","Em %n meses em %1$s","Em %n meses em %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Em um ano em %1$s","Em %n anos em %1$s","Em %n anos em %1$s"],
+ "In the past on %1$s then on %2$s" : "No passado em %1$s e depois em %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Em um minuto em %1$s e depois em %2$s","Em %n minutos em %1$s e depois em %2$s","Em %n minutos em %1$s e depois em %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Em um hora em %1$s e depois em %2$s","Em %n horas em %1$s e depois em %2$s","Em %n horas em %1$s e depois em %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Em um dia em %1$s e depois em %2$s","Em %n dias em %1$s e depois em %2$s","Em %n dias em %1$s e depois em %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Em uma semana em %1$s e depois em %2$s","Em %n semanas em %1$s e depois em %2$s","Em %n semanas em %1$s e depois em %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Em um mês em %1$s e depois em %2$s","Em %n meses em %1$s e depois em %2$s","Em %n meses em %1$s e depois em %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Em um ano em %1$s e depois em %2$s","Em %n anos em %1$s e depois em %2$s","Em %n anos em %1$s e depois em %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "No passado em %1$s e depois em %2$s e %3$s",
+ "_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_" : ["Em um minuto e %1$s e depois em %2$s e %3$s","Em %n minutos e %1$s e depois em %2$s e %3$s","Em %n minutos e %1$s e depois em %2$s e %3$s"],
+ "_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_" : ["Em uma hora e %1$s e depois em %2$s e %3$s","Em %n horas e %1$s e depois em %2$s e %3$s","Em %n horas e %1$s e depois em %2$s e %3$s"],
+ "_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_" : ["Em um dia e %1$s e depois em %2$s e %3$s","Em %n dias e %1$s e depois em %2$s e %3$s","Em %n dias e %1$s e depois em %2$s e %3$s"],
+ "_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_" : ["Em uma semana e %1$s e depois em %2$s e %3$s","Em %n semanas e %1$s e depois em %2$s e %3$s","Em %n semanas e %1$s e depois em %2$s e %3$s"],
+ "_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_" : ["Em um mês e %1$s e depois em %2$s e %3$s","Em %n meses e %1$s e depois em %2$s e %3$s","Em %n meses e %1$s e depois em %2$s e %3$s"],
+ "_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_" : ["Em um ano e %1$s e depois em %2$s e %3$s","Em %n anos e %1$s e depois em %2$s e %3$s","Em %n anos e %1$s e depois em %2$s e %3$s"],
+ "Could not generate next recurrence statement" : "Não foi possível gerar a indicação da próxima recorrência",
"Cancelled: %1$s" : "Cancelado: %1$s",
- "Invitation canceled" : "Convite cancelado",
- "Re: %1$s" : "Remetente: %1$s",
- "Invitation updated" : "Convite atualizado",
+ "\"%1$s\" has been canceled" : "\"%1$s\" foi cancelado",
+ "Re: %1$s" : "Re: %1$s",
+ "%1$s has accepted your invitation" : "%1$s aceitou seu convite",
+ "%1$s has tentatively accepted your invitation" : "%1$s aceitou provisoriamente seu convite",
+ "%1$s has declined your invitation" : "%1$s recusou seu convite",
+ "%1$s has responded to your invitation" : "%1$s respondeu ao seu convite",
+ "Invitation updated: %1$s" : "Convite atualizado: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s atualizou o evento \"%2$s\"",
"Invitation: %1$s" : "Convite: %1$s",
- "Invitation" : "Convite",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s gostaria de convidar você para \"%2$s\"",
+ "Organizer:" : "Organizador:",
+ "Attendees:" : "Participantes:",
"Title:" : "Título:",
- "Time:" : "Horário:",
+ "When:" : "Quando:",
"Location:" : "Localização:",
"Link:" : "Link:",
- "Organizer:" : "Organizador:",
- "Attendees:" : "Participantes:",
+ "Occurring:" : "Ocorrendo:",
"Accept" : "Aceitar",
"Decline" : "Rejeitar",
"More options …" : "Mais opções...",
"More options at %s" : "Mais opções em %s",
+ "Monday" : "Segunda-feira",
+ "Tuesday" : "Terça-feira",
+ "Wednesday" : "Quarta-feira",
+ "Thursday" : "Quinta-feira",
+ "Friday" : "Sexta-feira",
+ "Saturday" : "Sábado ",
+ "Sunday" : "Domingo",
+ "January" : "Janeiro",
+ "February" : "Fevereiro",
+ "March" : "Março",
+ "April" : "Abril",
+ "May" : "Maio",
+ "June" : "Junho",
+ "July" : "Julho",
+ "August" : "Agosto",
+ "September" : "Setembro",
+ "October" : "Outubro",
+ "November" : "Novembro",
+ "December" : "Dezembro",
+ "First" : "1.º/ª",
+ "Second" : "2.º/ª",
+ "Third" : "3.º/ª",
+ "Fourth" : "4.º/ª",
+ "Fifth" : "5.º/ª",
+ "Last" : "Último/a",
+ "Second Last" : "Penúltimo/a",
+ "Third Last" : "3.º/ª Último/a",
+ "Fourth Last" : "4.º/ª Último/a",
+ "Fifth Last" : "5.º/ª Último/a",
"Contacts" : "Contatos",
- "{actor} created address book {addressbook}" : "{actor} criou o livro de endereço {addressbook}",
+ "{actor} created address book {addressbook}" : "{actor} criou o catálogo de endereços {addressbook}",
"You created address book {addressbook}" : "Você criou o catálogo de endereços {addressbook}",
- "{actor} deleted address book {addressbook}" : "{actor} apagou o livro de endereços {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} excluiu o catálogo de endereços {addressbook}",
"You deleted address book {addressbook}" : "Você excluiu o catálogo de endereços {addressbook}",
- "{actor} updated address book {addressbook}" : "{actor} atualizou o livro de endereço {addressbook}",
- "You updated address book {addressbook}" : "Você atualizou o livro de endereço {addressbook}",
- "{actor} shared address book {addressbook} with you" : "{actor} compartilhou o livro de endereço {addressbook} com você",
+ "{actor} updated address book {addressbook}" : "{actor} atualizou o catálogo de endereços {addressbook}",
+ "You updated address book {addressbook}" : "Você atualizou o catálogo de endereços {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} compartilhou o catálogo de endereços {addressbook} com você",
"You shared address book {addressbook} with {user}" : "Você compartilhou o catálogo de endereços {addressbook} com {user}",
- "{actor} shared address book {addressbook} with {user}" : "{actor} compartilhou o livro de endereço {addressbook} com {user}",
- "{actor} unshared address book {addressbook} from you" : "{actor} retirou o compartilhamento do livro de endereço {addressbook} com você",
- "You unshared address book {addressbook} from {user}" : "Você retirou o compartilhamento do livro de endereço {addressbook} com {user}",
- "{actor} unshared address book {addressbook} from {user}" : "{actor} retirou o compartilhamento do livro de endereço {addressbook} com {user}",
- "{actor} unshared address book {addressbook} from themselves" : "{actor} retirou o compartilhamento do livro de endereço {addressbook} com ele mesmo",
- "You shared address book {addressbook} with group {group}" : "Você compartilhou o livro de endereço {addressbook} com o grupo {group}",
- "{actor} shared address book {addressbook} with group {group}" : "{actor} compartilhou o livro de endereço {addressbook} com o grupo {group}",
- "You unshared address book {addressbook} from group {group}" : "Você retirou o compartilhamento do livro de enredeços {addressbook} com o grupo {group}",
- "{actor} unshared address book {addressbook} from group {group}" : "{actor} retirou o compartilhamento do livro de endereço {addressbook} com o grupo {group}",
- "{actor} created contact {card} in address book {addressbook}" : "{actor} criou o contato {card} no livro de enrereço {addressbook}",
- "You created contact {card} in address book {addressbook}" : "Você criou o contato {card} no livro de endereço {addressbook}",
- "{actor} deleted contact {card} from address book {addressbook}" : "{actor} excluiu o contato {card} do livro de endereço {addressbook}",
- "You deleted contact {card} from address book {addressbook}" : "Você apagou o contato {card} do livro de endereços {addressbook}",
- "{actor} updated contact {card} in address book {addressbook}" : "{actor} updated contact {card} no livro de endereço {addressbook}",
- "You updated contact {card} in address book {addressbook}" : "Você atualizou o contato {card} no livro de endereços {addressbook}",
- "A <strong>contact</strong> or <strong>address book</strong> was modified" : "O <strong>contato</strong> ou <strong>livro de endereço</strong> foi modificado",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} compartilhou o catálogo de endereços {addressbook} com {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} retirou o compartilhamento do catálogo de endereços {addressbook} de você",
+ "You unshared address book {addressbook} from {user}" : "Você retirou o compartilhamento do catálogo de endereços {addressbook} de {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} retirou o compartilhamento do catálogo de endereços {addressbook} de {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} retirou o compartilhamento do catálogo de endereços {addressbook} de si mesmo",
+ "You shared address book {addressbook} with group {group}" : "Você compartilhou o catálogo de endereços {addressbook} com o grupo {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} compartilhou o catálogo de endereços {addressbook} com o grupo {group}",
+ "You unshared address book {addressbook} from group {group}" : "Você retirou o compartilhamento do catálogo de endereços {addressbook} do grupo {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} retirou o compartilhamento do catálogo de endereços {addressbook} do grupo {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} criou o contato {card} no catálogo de endereços {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Você criou o contato {card} no catálogo de endereços {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} excluiu o contato {card} do catálogo de endereços {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Você exluiu o contato {card} do catálogo de endereços {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} atualizou o contato {card} no catálogo de endereços {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Você atualizou o contato {card} no catálogo de endereços {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Um <strong>contato</strong> ou <strong>catálogo de endereços</strong> foi modificado",
+ "Accounts" : "Contas",
+ "System address book which holds all accounts" : "Catálogo de endereços do sistema que contém todas as contas",
"File is not updatable: %1$s" : "O arquivo não é atualizável: %1$s",
- "Could not write to final file, canceled by hook" : "Não foi possível gravar no arquivo final, cancelado pelo gancho",
+ "Failed to get storage for file" : "Falha ao obter armazenamento para arquivo",
+ "Could not write to final file, canceled by hook" : "Não foi possível gravar no arquivo final, cancelado pelo hook",
"Could not write file contents" : "Não foi possível gravar o conteúdo do arquivo",
- "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes","%n bytes"],
"Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Erro ao copiar o arquivo para o local de destino (copiado: %1$s, tamanho de arquivo esperado: %2$s)",
- "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." : "Tamanho de arquivo esperado de %1$s mas lido (do cliente Nextcloud) e gravado (no armazenamento Nextcloud) %2$s. Pode ser um problema de rede no lado de envio ou um problema de gravação no armazenamento no lado do servidor.",
- "Could not rename part file to final file, canceled by hook" : "Não foi possível renomear o arquivo de parte para o arquivo final, cancelado pelo gancho",
- "Could not rename part file to final file" : "Não foi possível renomear o arquivo de parte para o arquivo final",
+ "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." : "Tamanho de arquivo esperado de %1$s, mas lido (do cliente Nextcloud) e gravado (no armazenamento Nextcloud) %2$s. Pode ser um problema de rede no lado de envio ou um problema de gravação no armazenamento no lado do servidor.",
+ "Could not rename part file to final file, canceled by hook" : "Não foi possível renomear o arquivo de peça para o arquivo final, cancelado pelo hook",
+ "Could not rename part file to final file" : "Não foi possível renomear o arquivo de peça para o arquivo final",
"Failed to check file size: %1$s" : "Falha ao verificar o tamanho do arquivo: %1$s",
- "Could not open file" : "Não pode abrir o arquivo",
+ "Could not open file: %1$s, file does seem to exist" : "Não foi possível abrir o arquivo: %1$s, o arquivo parece existir",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Não foi possível abrir o arquivo: %1$s, o arquivo parece não existir",
"Encryption not ready: %1$s" : "A criptografia não está pronta: %1$s",
"Failed to open file: %1$s" : "Falha ao abrir arquivo: %1$s",
"Failed to unlink: %1$s" : "Falha ao desvincular: %1$s",
- "Invalid chunk name" : "Nome do bloco inválido",
- "Could not rename part file assembled from chunks" : "Não foi possível renomear parte do arquivo montado a partir de pedaços",
"Failed to write file contents: %1$s" : "Falha ao gravar o conteúdo do arquivo:%1$s",
- "File not found: %1$s" : "Arquivo não encontrado:%1$s",
+ "File not found: %1$s" : "Arquivo não encontrado: %1$s",
+ "Invalid target path" : "Caminho de destino inválido",
"System is in maintenance mode." : "O sistema está em modo de manutenção",
"Upgrade needed" : "Upgrade necessário",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Seu %s precisa estar configurado para usar HTTPS a fim de usar o CalDAV e o CardDAV com o iOS/macOS.",
"Configures a CalDAV account" : "Configure uma conta CalDAV",
"Configures a CardDAV account" : "Configure uma conta CardDAV",
"Events" : "Eventos",
- "Tasks" : "Tarefas",
"Untitled task" : "Tarefa sem título",
"Completed on %s" : "Concluída em %s",
"Due on %s by %s" : "Vence em %s até %s",
"Due on %s" : "Vence em %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Bem-vindo ao Nextcloud Calendário!\n\nEste é um exemplo de evento - explore a flexibilidade do planejamento com o Nextcloud Calendário fazendo as edições que desejar!\n\nCom o Nextcloud Calendário, você pode:\n- Criar, editar e gerenciar eventos sem esforço.\n- Criar vários calendários e compartilhá-los com colegas de equipe, amigos ou familiares.\n- Verificar a disponibilidade e exibir seus horários de trabalho para outras pessoas.\n- Integrá-lo perfeitamente com aplicativos e dispositivos via CalDAV.\n- Personalizar sua experiência: agende eventos recorrentes, ajuste as notificações e outras configurações.",
+ "Example event - open me!" : "Exemplo de evento - abra-me!",
+ "System Address Book" : "Catálogo de Endereços do Sistema",
+ "The system address book contains contact information for all users in your instance." : "O catálogo de endereços do sistema contém informações de contato de todos os usuários da sua instância.",
+ "Enable System Address Book" : "Ativar Catálogo de Endereços do Sistema",
+ "DAV system address book" : "Catálogo de endereços DAV do sistema",
+ "No outstanding DAV system address book sync." : "Não há sincronização pendente do catálogo de endereços DAV do sistema.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "A sincronização do catálogo de endereços DAV do sistema ainda não foi executada porque sua instância tem mais de 1000 usuários ou porque ocorreu um erro. Execute-a manualmente chamando \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "Endpoint WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Não foi possível verificar se o seu servidor web está configurado corretamente para permitir a sincronização de arquivos via WebDAV. Verifique manualmente.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Seu servidor web ainda não está configurado corretamente para permitir a sincronização de arquivos, porque a interface do WebDAV parece estar quebrada.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Seu servidor web está configurado corretamente para permitir a sincronização de arquivos via WebDAV.",
"Migrated calendar (%1$s)" : "Calendário migrado (%1$s)",
"Calendars including events, details and attendees" : "Calendários, incluindo eventos, detalhes e participantes",
"Contacts and groups" : "Contatos e grupos",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "Endpoint WebDAV",
- "Availability" : "Disponibilidade",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Se você configurar seu horário de trabalho, outros usuários verão quando você estiver fora do escritório quando marcarem uma reunião. ",
+ "Absence saved" : "Ausência salva",
+ "Failed to save your absence settings" : "Falha ao salvar suas configurações de ausência",
+ "Absence cleared" : "Limpou a ausência",
+ "Failed to clear your absence settings" : "Falha ao limpar suas configurações de ausência",
+ "First day" : "Primeiro dia",
+ "Last day (inclusive)" : "Último dia (inclusive)",
+ "Out of office replacement (optional)" : "Substituto durante sua ausência (opcional)",
+ "Name of the replacement" : "Nome do substituto",
+ "No results." : "Nenhum resultado.",
+ "Start typing." : "Comece a digitar.",
+ "Short absence status" : "Status curto sobre a ausência",
+ "Long absence Message" : "Mensagem longa sobre a ausência",
+ "Save" : "Salvar",
+ "Disable absence" : "Desativar ausência",
+ "Failed to load availability" : "Falha ao carregar a disponibilidade",
+ "Saved availability" : "Disponibilidade salva",
+ "Failed to save availability" : "Falha ao salvar a disponibilidade",
"Time zone:" : "Fuso horário:",
"to" : "para",
"Delete slot" : "Excluir slot",
"No working hours set" : "Sem horário de trabalho definido",
"Add slot" : "Adicionar slot ",
- "Monday" : "Segunda-feira",
- "Tuesday" : "Terça-feira",
- "Wednesday" : "Quarta-feira",
- "Thursday" : "Quinta-feira",
- "Friday" : "Sexta-feira",
- "Saturday" : "Sábado ",
- "Sunday" : "Domingo",
- "Save" : "Salvar",
+ "Weekdays" : "Dias da semana",
+ "Pick a start time for {dayName}" : "Selecione um horário de início para {dayName}",
+ "Pick a end time for {dayName}" : "Selecione um horário de fim para {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Defina automaticamente o status do usuário como \"Não perturbe\" fora de disponibilidade para silenciar todas as notificações.",
+ "Cancel" : "Cancelar",
+ "Import" : "Importar",
+ "Error while saving settings" : "Erro ao salvar as configurações",
+ "Contact reset successfully" : "O contato foi reiniciado com sucesso",
+ "Error while resetting contact" : "Erro ao reiniciar o contato",
+ "Contact imported successfully" : "O contato foi importado com sucesso",
+ "Error while importing contact" : "Erro ao importar o contato",
+ "Import contact" : "Importar contato",
+ "Reset to default" : "Redefinir para o padrão",
+ "Import contacts" : "Importar contatos",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "A importação de um novo arquivo .vcf excluirá o contato padrão existente e o substituirá pelo novo. Deseja continuar?",
+ "Failed to save example event creation setting" : "Falha ao salvar a configuração de criação de evento de exemplo",
+ "Failed to upload the example event" : "Falha ao fazer upload do evento de exemplo",
+ "Custom example event was saved successfully" : "O evento de exemplo personalizado foi salvo com êxito",
+ "Failed to delete the custom example event" : "Falha ao excluir o evento de exemplo personalizado",
+ "Custom example event was deleted successfully" : "O evento de exemplo personalizado foi excluído com êxito",
+ "Import calendar event" : "Importar evento de calendário",
+ "Uploading a new event will overwrite the existing one." : "O upload de um novo evento substituirá o existente.",
+ "Upload event" : "Fazer upload do evento",
+ "Availability" : "Disponibilidade",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Se você configurar seu horário de trabalho, outras pessoas verão quando você estiver ausente quando agendarem uma reunião.",
+ "Absence" : "Ausência",
+ "Configure your next absence period." : "Configure seu próximo período de ausência",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instale também o {calendarappstoreopen}aplicativo de calendário{linkclose} ou {calendardocopen}conecte sua área de trabalho e celular à sincronização ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Certifique-se de configurar corretamente {emailopen}o servidor de e-mail{linkclose}.",
"Calendar server" : "Servidor de calendário",
"Send invitations to attendees" : "Enviar convites aos participantes",
"Automatically generate a birthday calendar" : "Gerar um calendário de aniversários automaticamente",
@@ -161,18 +322,15 @@
"Hence they will not be available immediately after enabling but will show up after some time." : "Portanto, eles não estarão disponíveis imediatamente após a ativação, mas depois de algum tempo.",
"Send notifications for events" : "Enviar notificações para eventos",
"Notifications are sent via background jobs, so these must occur often enough." : "As notificações são enviadas via trabalhos em segundo plano, portanto, elas devem ocorrer com frequência suficiente.",
- "Send reminder notifications to calendar sharees as well" : "Envie notificações de lembrete para compartilhamentos de calendário também",
- "Reminders are always sent to organizers and attendees." : "Os lembretes são sempre enviados aos organizadores e participantes.",
+ "Send reminder notifications to calendar sharees as well" : "Enviar notificações de lembrete também para os compartilhadores de calendário",
+ "Reminders are always sent to organizers and attendees." : "Lembretes são sempre enviados aos organizadores e participantes.",
"Enable notifications for events via push" : "Ativar notificações para eventos via push",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instale também o {calendarappstoreopen}aplicativo de calendário{linkclose} ou {calendardocopen}conecte sua área de trabalho e celular à sincronização ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Certifique-se de configurar corretamente {emailopen}o servidor de e-mail{linkclose}.",
+ "Example content" : "Exemplo de conteúdo",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "O conteúdo de exemplo serve para mostrar os recursos do Nextcloud. O conteúdo padrão é fornecido com o Nextcloud e pode ser substituído por conteúdo personalizado.",
"There was an error updating your attendance status." : "Houve um erro ao atualizar seu status de participação.",
"Please contact the organizer directly." : "Por favor, contate o organizador diretamente.",
"Are you accepting the invitation?" : "Você está aceitando o convite?",
"Tentative" : "Tentativa",
- "Number of guests" : "Número de convidados",
- "Comment" : "Comentário",
- "Your attendance was updated successfully." : "Sua presença foi atualizada com sucesso.",
- "Calendar and tasks" : "Calendário e tarefas"
-},"pluralForm" :"nplurals=2; plural=(n > 1);"
+ "Your attendance was updated successfully." : "Sua presença foi atualizada com sucesso."
+},"pluralForm" :"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"
} \ No newline at end of file
diff --git a/apps/dav/l10n/pt_PT.js b/apps/dav/l10n/pt_PT.js
new file mode 100644
index 00000000000..17246a2a574
--- /dev/null
+++ b/apps/dav/l10n/pt_PT.js
@@ -0,0 +1,336 @@
+OC.L10N.register(
+ "dav",
+ {
+ "Calendar" : "Calendário",
+ "Tasks" : "Tarefas",
+ "Personal" : "Pessoal",
+ "{actor} created calendar {calendar}" : "{actor} criou o calendário {calendar}",
+ "You created calendar {calendar}" : "Criou o calendario {calendar}",
+ "{actor} deleted calendar {calendar}" : "{actor} apagou o calendário {calendar}",
+ "You deleted calendar {calendar}" : "Apagou o calendário {calendar}",
+ "{actor} updated calendar {calendar}" : "{actor} atualizou o calendário {calendar}",
+ "You updated calendar {calendar}" : "Atualizou o calendário {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} restaurou o calendário {calendar}",
+ "You restored calendar {calendar}" : "restaurou o calendário {calendar}",
+ "You shared calendar {calendar} as public link" : "Partilhou o calendário {calendar} como uma hiperligação pública",
+ "You removed public link for calendar {calendar}" : "Removeu uma hiperligação pública para o calendário {calendar}",
+ "{actor} shared calendar {calendar} with you" : "{actor} partilhou o calendário {calendar} consigo",
+ "You shared calendar {calendar} with {user}" : "Partilhou o calendário {calendar} com {user}",
+ "{actor} shared calendar {calendar} with {user}" : "{actor} partilhou o calendário {calendar} com {user}",
+ "{actor} unshared calendar {calendar} from you" : "{actor} retirou-lhe a partilha do calendário {calendar}",
+ "You unshared calendar {calendar} from {user}" : "Retirou a partilha do calendário {calendar} com {user}",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor} retirou a partilha do calendário {calendar} com {user}",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} deixou de partilhar o calendário {calendar} de próprio",
+ "You shared calendar {calendar} with group {group}" : "partilhou o calendário {calendar} com o grupo {group}",
+ "{actor} shared calendar {calendar} with group {group}" : "{actor} partilhou o calendário {calendar} com o grupo {group}",
+ "You unshared calendar {calendar} from group {group}" : "deixou de partilhar o calendário {calendar} do grupo {group}",
+ "{actor} unshared calendar {calendar} from group {group}" : "{actor} deixou de partilhar o calendário {calendar} do grupo {group}",
+ "Untitled event" : "Evento sem título",
+ "{actor} created event {event} in calendar {calendar}" : "{actor} criou o evento {event} in o calendário {calendar}",
+ "You created event {event} in calendar {calendar}" : "criou o evento {event} in o calendário {calendar}",
+ "{actor} deleted event {event} from calendar {calendar}" : "{actor} eliminou o evento {event} de o calendário {calendar}",
+ "You deleted event {event} from calendar {calendar}" : "eliminou o evento {event} de o calendário {calendar}",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} atualizou o evento {event} in o calendário {calendar}",
+ "You updated event {event} in calendar {calendar}" : "atualizou o evento {event} in o calendário {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} moveu o evento {event} de o calendário {sourceCalendar} para o calendário {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "moveu o evento {event} de o calendário {sourceCalendar} para o calendário {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} restaurou o evento {event} of o calendário {calendar}",
+ "You restored event {event} of calendar {calendar}" : "restaurou o evento {event} of o calendário {calendar}",
+ "Busy" : "Ocupado",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} criou to-do {todo} in list {calendar}",
+ "You created to-do {todo} in list {calendar}" : "criou to-do {todo} in list {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} eliminou to-do {todo} de list {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "eliminou to-do {todo} de list {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} atualizou to-do {todo} in list {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "atualizou to-do {todo} in list {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} concluiu a tarefa {todo} na lista {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "solved to-do {todo} in list {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} reabriu a tarefa {todo} na lista {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "reopened to-do {todo} in list {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} moveu to-do {todo} de list {sourceCalendar} para list {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "moveu to-do {todo} de list {sourceCalendar} para list {targetCalendar}",
+ "Calendar, contacts and tasks" : "Calendário, contactos e tarefas",
+ "A <strong>calendar</strong> was modified" : "O <strong>calendário</strong> foi modificado.",
+ "A calendar <strong>event</strong> was modified" : "Um <strong>event</strong> do calendário foi modificado",
+ "A calendar <strong>to-do</strong> was modified" : "Foi modificada uma <strong>tarefa</strong> do calendário",
+ "Contact birthdays" : "Aniversário dos contactos",
+ "Death of %s" : "Falecimento de %s",
+ "Untitled calendar" : "Calendário sem titulo",
+ "Calendar:" : "Calendário:",
+ "Date:" : "Data:",
+ "Where:" : "Local:",
+ "Description:" : "Descrição:",
+ "_%n year_::_%n years_" : ["%n ano","%n anos","%n anos"],
+ "_%n month_::_%n months_" : ["%n mês","%n meses","%n meses"],
+ "_%n day_::_%n days_" : ["%n dia","%n dias","%n dias"],
+ "_%n hour_::_%n hours_" : ["%n hora","%n horas","%n horas"],
+ "_%n minute_::_%n minutes_" : ["%n minuto","%n minutos","%n minutos"],
+ "%s (in %s)" : "%s (em %s)",
+ "%s (%s ago)" : "%s (%s atrás)",
+ "Calendar: %s" : "Calendário: %s",
+ "Date: %s" : "Data: %s",
+ "Description: %s" : "Descrição: %s",
+ "Where: %s" : "Local: %s",
+ "%1$s via %2$s" : "%1$s via %2$s",
+ "In the past on %1$s for the entire day" : "No passado em %1$s durante todo o dia",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Daqui a um minuto em %1$s para todo o dia","Daqui a %n minutos em %1$s para todo o dia","Daqui a %n minutos em %1$s para todo o dia"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Daqui a um hora em %1$s para todo o dia","Daqui a %n horas em %1$s para todo o dia","Daqui a %n horas em %1$s para todo o dia"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Daqui a um dia em %1$s para todo o dia","Daqui a %n dias em %1$s para todo o dia","Daqui a %n dias em %1$s para todo o dia"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Daqui a um semana em %1$s para todo o dia","Daqui a %n semanas em %1$s para todo o dia","Daqui a %n semanas em %1$s para todo o dia"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Daqui a um mês em %1$s para todo o dia","Daqui a %n meses em %1$s para todo o dia","Daqui a %n meses em %1$s para todo o dia"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Daqui a um ano em %1$s para todo o dia","Daqui a %n anos em %1$s para todo o dia","Daqui a %n anos em %1$s para todo o dia"],
+ "In the past on %1$s between %2$s - %3$s" : "No passado em %1$s entre %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Daqui a um minuto em %1$s entre %2$s - %3$s","Daqui a %n minutos em %1$s entre %2$s - %3$s","Daqui a %n minutos em %1$s entre %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Daqui a um hora em %1$s entre %2$s - %3$s","Daqui a %n horas em %1$s entre %2$s - %3$s","Daqui a %n horas em %1$s entre %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Daqui a um dia em %1$s entre %2$s - %3$s","Daqui a %n dias em %1$s entre %2$s - %3$s","Daqui a %n dias em %1$s entre %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Daqui a um semana em %1$s entre %2$s - %3$s","Daqui a %n semanas em %1$s entre %2$s - %3$s","Daqui a %n semanas em %1$s entre %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Daqui a um mês em %1$s entre %2$s - %3$s","Daqui a %n meses em %1$s entre %2$s - %3$s","Daqui a %n meses em %1$s entre %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Daqui a um ano em %1$s entre %2$s - %3$s","Daqui a %n anos em %1$s entre %2$s - %3$s","Daqui a %n anos em %1$s entre %2$s - %3$s"],
+ "Could not generate when statement" : "Não foi possível gerar a expressão \"quando\"",
+ "Every Day for the entire day" : "Todos os dias, dia inteiro",
+ "Every Day for the entire day until %1$s" : "Todos os dias, dia inteiro até %1$s",
+ "Every Day between %1$s - %2$s" : "Todos os dias entre %1$s e %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Todos os dias entre %1$s e %2$s até %3$s",
+ "Every %1$d Days for the entire day" : "A cada %1$d dias, dia inteiro",
+ "Every %1$d Days for the entire day until %2$s" : "A cada %1$d dias, dia inteiro até %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "A cada %1$d dias entre %2$s e %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "A cada %1$d dias entre %2$s e %3$s até %4$s",
+ "Could not generate event recurrence statement" : "Não foi possível gerar a expressão de recorrência do evento",
+ "Every Week on %1$s for the entire day" : "Todas as semanas, em %1$s, dia inteiro",
+ "Every Week on %1$s for the entire day until %2$s" : "Todas as semanas, em %1$s, dia inteiro até %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Todas as semanas, em %1$s entre %2$s e %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Todas as semanas, em %1$s entre %2$s e %3$s até %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "A cada %1$d semanas, em %2$s, dia inteiro",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "A cada %1$d semanas, em %2$s, dia inteiro até %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "A cada %1$d semanas, em %2$s entre %3$s e %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "A cada %1$d semanas, em %2$s entre %3$s e %4$s até %5$s",
+ "Every Month on the %1$s for the entire day" : "Todos os meses, no dia %1$s, dia inteiro",
+ "Every Month on the %1$s for the entire day until %2$s" : "Todos os meses, no dia %1$s, dia inteiro até %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Todos os meses, no dia %1$s entre %2$s e %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Todos os meses, no dia %1$s entre %2$s e %3$s até %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "A cada %1$d meses, no dia %2$s, dia inteiro",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "A cada %1$d meses, no dia %2$s, dia inteiro até %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "A cada %1$d meses, no dia %2$s entre %3$s e %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "A cada %1$d meses, no dia %2$s entre %3$s e %4$s até %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Todos os anos em %1$s, no dia %2$s, dia inteiro",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Todos os anos em %1$s, no dia %2$s, dia inteiro até %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Todos os anos em %1$s, no dia %2$s entre %3$s e %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Todos os anos em %1$s, no dia %2$s entre %3$s e %4$s até %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "A cada %1$d anos em %2$s, no dia %3$s, dia inteiro",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "A cada %1$d anos em %2$s, no dia %3$s, dia inteiro até %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "A cada %1$d anos em %2$s, no dia %3$s entre %4$s e %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "A cada %1$d anos em %2$s, no dia %3$s entre %4$s e %5$s até %6$s",
+ "On specific dates for the entire day until %1$s" : "Em datas específicas, dia inteiro até %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "Em datas específicas entre %1$s e %2$s até %3$s",
+ "In the past on %1$s" : "No passado em %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Daqui a um minuto em %1$s","Daqui a %n minutos em %1$s","Daqui a %n minutos em %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Daqui a um hora em %1$s","Daqui a %n horas em %1$s","Daqui a %n horas em %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Daqui a um dia em %1$s","Daqui a %n dias em %1$s","Daqui a %n dias em %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Daqui a um semana em %1$s","Daqui a %n semanas em %1$s","Daqui a %n semanas em %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Daqui a um mês em %1$s","Daqui a %n meses em %1$s","Daqui a %n meses em %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Daqui a um ano em %1$s","Daqui a %n anos em %1$s","Daqui a %n anos em %1$s"],
+ "In the past on %1$s then on %2$s" : "No passado em %1$s e depois %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Daqui a um minuto em %1$s e depois em %2$s","Daqui a %n minutos em %1$s e depois em %2$s","Daqui a %n minutos em %1$s e depois em %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Daqui a um hora em %1$s e depois em %2$s","Daqui a %n horas em %1$s e depois em %2$s","Daqui a %n horas em %1$s e depois em %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Daqui a um dia em %1$s e depois em %2$s","Daqui a %n dias em %1$s e depois em %2$s","Daqui a %n dias em %1$s e depois em %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Daqui a um semana em %1$s e depois em %2$s","Daqui a %n semanas em %1$s e depois em %2$s","Daqui a %n semanas em %1$s e depois em %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Daqui a um mês em %1$s e depois em %2$s","Daqui a %n meses em %1$s e depois em %2$s","Daqui a %n meses em %1$s e depois em %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Daqui a um ano em %1$s e depois em %2$s","Daqui a %n anos em %1$s e depois em %2$s","Daqui a %n anos em %1$s e depois em %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "No passado em %1$s e depois em %2$s e %3$s",
+ "_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_" : ["Daqui a um minuto em %1$s e depois em %2$s e %3$s","Daqui a %n minutos em %1$s e depois em %2$s e %3$s","Daqui a %n minutos em %1$s e depois em %2$s e %3$s"],
+ "_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_" : ["Daqui a um hora em %1$s e depois em %2$s e %3$s","Daqui a %n horas em %1$s e depois em %2$s e %3$s","Daqui a %n horas em %1$s e depois em %2$s e %3$s"],
+ "_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_" : ["Daqui a um dia em %1$s e depois em %2$s e %3$s","Daqui a %n dias em %1$s e depois em %2$s e %3$s","Daqui a %n dias em %1$s e depois em %2$s e %3$s"],
+ "_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_" : ["Daqui a um semana em %1$s e depois em %2$s e %3$s","Daqui a %n semanas em %1$s e depois em %2$s e %3$s","Daqui a %n semanas em %1$s e depois em %2$s e %3$s"],
+ "_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_" : ["Daqui a um mês em %1$s e depois em %2$s e %3$s","Daqui a %n meses em %1$s e depois em %2$s e %3$s","Daqui a %n meses em %1$s e depois em %2$s e %3$s"],
+ "_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_" : ["Daqui a um ano em %1$s e depois em %2$s e %3$s","Daqui a %n anos em %1$s e depois em %2$s e %3$s","Daqui a %n anos em %1$s e depois em %2$s e %3$s"],
+ "Could not generate next recurrence statement" : "Não foi possível gerar a próxima expressão de recorrência",
+ "Cancelled: %1$s" : "Cancelado: %1$s",
+ "\"%1$s\" has been canceled" : "\"%1$s\" foi cancelado",
+ "Re: %1$s" : "Re: %1$s",
+ "%1$s has accepted your invitation" : "%1$s aceitou o seu convite",
+ "%1$s has tentatively accepted your invitation" : "%1$s aceitou provisoriamente o seu convite",
+ "%1$s has declined your invitation" : "%1$s recusou o seu convite",
+ "%1$s has responded to your invitation" : "%1$s respondeu ao seu convite",
+ "Invitation updated: %1$s" : "Convite atualizado: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s atualizou o evento \"%2$s\"",
+ "Invitation: %1$s" : "Convite: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s gostaria de convidá‑lo para \"%2$s\"",
+ "Organizer:" : "Organizador:",
+ "Attendees:" : "Participantes:",
+ "Title:" : "Titulo: ",
+ "When:" : "Quando:",
+ "Location:" : "Localização:",
+ "Link:" : "Ligação:",
+ "Occurring:" : "Ocorrências:",
+ "Accept" : "Aceitar",
+ "Decline" : "Recusar",
+ "More options …" : "Mais opções…",
+ "More options at %s" : "Mais opções em %s",
+ "Monday" : "Segunda",
+ "Tuesday" : "Terça",
+ "Wednesday" : "Quarta",
+ "Thursday" : "Quinta",
+ "Friday" : "Sexta",
+ "Saturday" : "Sábado",
+ "Sunday" : "Domingo",
+ "January" : "Janeiro",
+ "February" : "Fevereiro",
+ "March" : "Março",
+ "April" : "Abril",
+ "May" : "Maio",
+ "June" : "Junho",
+ "July" : "Julho",
+ "August" : "Agosto",
+ "September" : "Setembro",
+ "October" : "Outubro",
+ "November" : "Novembro",
+ "December" : "Dezembro",
+ "First" : "Primeiro",
+ "Second" : "Segundo",
+ "Third" : "Terceiro",
+ "Fourth" : "Quarto",
+ "Fifth" : "Quinto",
+ "Last" : "Último",
+ "Second Last" : "Penúltimo",
+ "Third Last" : "Antepenúltimo",
+ "Fourth Last" : "Quarto do fim",
+ "Fifth Last" : "Quinto do fim",
+ "Contacts" : "Contactos",
+ "{actor} created address book {addressbook}" : "{actor} criou address book {addressbook}",
+ "You created address book {addressbook}" : "criou address book {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} eliminou address book {addressbook}",
+ "You deleted address book {addressbook}" : "eliminou address book {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} atualizou address book {addressbook}",
+ "You updated address book {addressbook}" : "atualizou address book {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} partilhou address book {addressbook} com you",
+ "You shared address book {addressbook} with {user}" : "partilhou address book {addressbook} com {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} partilhou address book {addressbook} com {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} deixou de partilhar address book {addressbook} de you",
+ "You unshared address book {addressbook} from {user}" : "deixou de partilhar address book {addressbook} de {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} deixou de partilhar address book {addressbook} de {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} deixou de partilhar address book {addressbook} de próprio",
+ "You shared address book {addressbook} with group {group}" : "partilhou address book {addressbook} com o grupo {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} partilhou address book {addressbook} com o grupo {group}",
+ "You unshared address book {addressbook} from group {group}" : "deixou de partilhar address book {addressbook} do grupo {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} deixou de partilhar address book {addressbook} do grupo {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} criou contact {card} in address book {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "criou contact {card} in address book {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} eliminou contact {card} de address book {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "eliminou contact {card} de address book {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} atualizou contact {card} in address book {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "atualizou contact {card} in address book {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Foi modificado um <strong>contacto</strong> ou <strong>livro de endereços</strong>",
+ "Accounts" : "Contas",
+ "System address book which holds all accounts" : "Livro de endereços do sistema que contém todas as contas",
+ "File is not updatable: %1$s" : "O ficheiro não pode ser atualizado: %1$s",
+ "Failed to get storage for file" : "Falha ao obter armazenamento para o ficheiro",
+ "Could not write to final file, canceled by hook" : "Não foi possível escrever no ficheiro final, cancelado por hook",
+ "Could not write file contents" : "Não foi possível escrever o conteúdo do ficheiro",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes","%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Erro ao copiar ficheiro para o destino (copiado: %1$s, tamanho esperado: %2$s)",
+ "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." : "Tamanho esperado de %1$s, mas leitura diferente (do cliente Nextcloud) e escreveu (para armazenamento Nextcloud) %2$s. Possível problema do lado do cliente ou ao escrever no armazenamento do servidor.",
+ "Could not rename part file to final file, canceled by hook" : "Não foi possível renomear o ficheiro parcial para ficheiro final, cancelado por hook",
+ "Could not rename part file to final file" : "Não foi possível renomear o ficheiro parcial para ficheiro final",
+ "Failed to check file size: %1$s" : "Falha ao verificar o tamanho do ficheiro: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "Não foi possível abrir o ficheiro: %1$s, o ficheiro parece existir",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Não foi possível abrir o ficheiro: %1$s, o ficheiro não parece existir",
+ "Encryption not ready: %1$s" : "Encriptação não pronta: %1$s",
+ "Failed to open file: %1$s" : "Falha ao abrir o ficheiro: %1$s",
+ "Failed to unlink: %1$s" : "Falha ao eliminar: %1$s",
+ "Failed to write file contents: %1$s" : "Falha ao escrever o conteúdo do ficheiro: %1$s",
+ "File not found: %1$s" : "Ficheiro não encontrado: %1$s",
+ "Invalid target path" : "Caminho de destino inválido",
+ "System is in maintenance mode." : "O sistema está em modo de manutenção.",
+ "Upgrade needed" : "É necessária atualização",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "O seu %s precisa de estar configurado para usar HTTPS para que possa utilizar CalDAV e CardDAV com iOS/macOS.",
+ "Configures a CalDAV account" : "Configura uma conta CalDAV",
+ "Configures a CardDAV account" : "Configura uma conta CardDAV",
+ "Events" : "Eventos",
+ "Untitled task" : "Tarefa sem título",
+ "Completed on %s" : "Concluída em %s",
+ "Due on %s by %s" : "Vence em %s às %s",
+ "Due on %s" : "Vence em %s",
+ "System Address Book" : "Livro de endereços do sistema",
+ "The system address book contains contact information for all users in your instance." : "O livro de endereços do sistema contém informação de contacto de todos os utilizadores da sua instância.",
+ "Enable System Address Book" : "Ativar livro de endereços do sistema",
+ "DAV system address book" : "Livro de endereços DAV do sistema",
+ "No outstanding DAV system address book sync." : "Sem sincronizações pendentes do livro de endereços DAV do sistema.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "A sincronização do livro de endereços DAV do sistema ainda não foi executada porque a sua instância não tem utilizadores; execute‑a manualmente com \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "Endpoint WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Não foi possível verificar se o seu servidor web está corretamente configurado para permitir a sincronização de ficheiros via WebDAV. Verifique manualmente.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "O seu servidor da Web não está configurado corretamente para permitir a sincronização de ficheiros, porque a interface WebDAV parece estar com problemas.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "O seu servidor web está corretamente configurado para permitir a sincronização de ficheiros via WebDAV.",
+ "Migrated calendar (%1$s)" : "Calendário migrado (%1$s)",
+ "Calendars including events, details and attendees" : "Calendários incluindo eventos, detalhes e participantes",
+ "Contacts and groups" : "Contactos e grupos",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "Ausência guardada",
+ "Failed to save your absence settings" : "Falha ao guardar as suas definições de ausência",
+ "Absence cleared" : "Ausência removida",
+ "Failed to clear your absence settings" : "Falha ao remover as suas definições de ausência",
+ "First day" : "Primeiro dia",
+ "Last day (inclusive)" : "Último dia (inclusive)",
+ "Out of office replacement (optional)" : "Substituto durante ausência (opcional)",
+ "Name of the replacement" : "Nome do substituto",
+ "No results." : "Sem resultados.",
+ "Start typing." : "Comece a escrever.",
+ "Short absence status" : "Estado de ausência curto",
+ "Long absence Message" : "Mensagem de ausência longa",
+ "Save" : "Guardar",
+ "Disable absence" : "Desativar ausência",
+ "Failed to load availability" : "Falha ao carregar disponibilidade",
+ "Saved availability" : "Disponibilidade guardada",
+ "Failed to save availability" : "Falha ao guardar disponibilidade",
+ "Time zone:" : "Fuso horário:",
+ "to" : "Para",
+ "Delete slot" : "Eliminar intervalo",
+ "No working hours set" : "Sem horário de trabalho definido",
+ "Add slot" : "Adicionar intervalo",
+ "Weekdays" : "Dias da semana",
+ "Pick a start time for {dayName}" : "Escolha uma hora de início para {dayName}",
+ "Pick a end time for {dayName}" : "Escolha uma hora de fim para {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Definir automaticamente o estado para \"Não incomodar\" fora do horário de disponibilidade para silenciar todas as notificações.",
+ "Cancel" : "Cancelar",
+ "Import" : "Importar",
+ "Error while saving settings" : "Erro ao guardar definições",
+ "Contact reset successfully" : "Contacto reposto com sucesso",
+ "Error while resetting contact" : "Erro ao repor contacto",
+ "Contact imported successfully" : "Contacto importado com sucesso",
+ "Error while importing contact" : "Erro ao importar contacto",
+ "Import contact" : "Importar contacto",
+ "Reset to default" : "Repor original",
+ "Import contacts" : "Importar contactos",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "A importação de um novo ficheiro .vcf eliminará o contacto predefinido existente e substituí‑lo‑á pelo novo. Deseja continuar?",
+ "Failed to save example event creation setting" : "Falha ao guardar a definição de criação de evento de exemplo",
+ "Failed to upload the example event" : "Falha ao carregar o evento de exemplo",
+ "Custom example event was saved successfully" : "Evento de exemplo personalizado guardado com sucesso",
+ "Failed to delete the custom example event" : "Falha ao eliminar o evento de exemplo personalizado",
+ "Custom example event was deleted successfully" : "Evento de exemplo personalizado eliminado com sucesso",
+ "Import calendar event" : "Importar evento de calendário",
+ "Uploading a new event will overwrite the existing one." : "O carregamento de um novo evento substituirá o existente.",
+ "Upload event" : "Carregar evento",
+ "Availability" : "Disponibilidade",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Se configurar o seu horário de trabalho, outras pessoas saberão quando está fora do escritório ao marcar uma reunião.",
+ "Absence" : "Ausência",
+ "Configure your next absence period." : "Configure o seu próximo período de ausência.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instale também a {calendarappstoreopen}aplicação Calendário {linkclose}, ou {calendardocopen} ligue o seu computador e telemóvel para sincronizar ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Certifique‑se de configurar corretamente {emailopen}o servidor de email{linkclose}.",
+ "Calendar server" : "Servidor de calendário",
+ "Send invitations to attendees" : "Enviar convites aos participantes",
+ "Automatically generate a birthday calendar" : "Gerar automaticamente um calendário de aniversários",
+ "Birthday calendars will be generated by a background job." : "Os calendários de aniversário serão gerados por uma tarefa em segundo plano.",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "Por isso, não estarão disponíveis imediatamente após a ativação, mas aparecerão após algum tempo.",
+ "Send notifications for events" : "Enviar notificações de eventos",
+ "Notifications are sent via background jobs, so these must occur often enough." : "As notificações são enviadas através de tarefas em segundo plano; estas devem ser executadas com frequência suficiente.",
+ "Send reminder notifications to calendar sharees as well" : "Enviar também notificações de lembrete aos partilhados do calendário",
+ "Reminders are always sent to organizers and attendees." : "Os lembretes são sempre enviados aos organizadores e participantes.",
+ "Enable notifications for events via push" : "Ativar notificações push para eventos",
+ "Example content" : "Conteúdo de exemplo",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "O conteúdo de exemplo serve para demonstrar as funcionalidades do Nextcloud. É fornecido com o Nextcloud e pode ser substituído por conteúdo personalizado.",
+ "There was an error updating your attendance status." : "Ocorreu um erro ao atualizar o seu estado de participação.",
+ "Please contact the organizer directly." : "Contacte diretamente o organizador.",
+ "Are you accepting the invitation?" : "Aceita o convite?",
+ "Tentative" : "Tentativa",
+ "Your attendance was updated successfully." : "O seu estado de participação foi atualizado com sucesso."
+},
+"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;");
diff --git a/apps/dav/l10n/pt_PT.json b/apps/dav/l10n/pt_PT.json
new file mode 100644
index 00000000000..7120a7a8e96
--- /dev/null
+++ b/apps/dav/l10n/pt_PT.json
@@ -0,0 +1,334 @@
+{ "translations": {
+ "Calendar" : "Calendário",
+ "Tasks" : "Tarefas",
+ "Personal" : "Pessoal",
+ "{actor} created calendar {calendar}" : "{actor} criou o calendário {calendar}",
+ "You created calendar {calendar}" : "Criou o calendario {calendar}",
+ "{actor} deleted calendar {calendar}" : "{actor} apagou o calendário {calendar}",
+ "You deleted calendar {calendar}" : "Apagou o calendário {calendar}",
+ "{actor} updated calendar {calendar}" : "{actor} atualizou o calendário {calendar}",
+ "You updated calendar {calendar}" : "Atualizou o calendário {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} restaurou o calendário {calendar}",
+ "You restored calendar {calendar}" : "restaurou o calendário {calendar}",
+ "You shared calendar {calendar} as public link" : "Partilhou o calendário {calendar} como uma hiperligação pública",
+ "You removed public link for calendar {calendar}" : "Removeu uma hiperligação pública para o calendário {calendar}",
+ "{actor} shared calendar {calendar} with you" : "{actor} partilhou o calendário {calendar} consigo",
+ "You shared calendar {calendar} with {user}" : "Partilhou o calendário {calendar} com {user}",
+ "{actor} shared calendar {calendar} with {user}" : "{actor} partilhou o calendário {calendar} com {user}",
+ "{actor} unshared calendar {calendar} from you" : "{actor} retirou-lhe a partilha do calendário {calendar}",
+ "You unshared calendar {calendar} from {user}" : "Retirou a partilha do calendário {calendar} com {user}",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor} retirou a partilha do calendário {calendar} com {user}",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} deixou de partilhar o calendário {calendar} de próprio",
+ "You shared calendar {calendar} with group {group}" : "partilhou o calendário {calendar} com o grupo {group}",
+ "{actor} shared calendar {calendar} with group {group}" : "{actor} partilhou o calendário {calendar} com o grupo {group}",
+ "You unshared calendar {calendar} from group {group}" : "deixou de partilhar o calendário {calendar} do grupo {group}",
+ "{actor} unshared calendar {calendar} from group {group}" : "{actor} deixou de partilhar o calendário {calendar} do grupo {group}",
+ "Untitled event" : "Evento sem título",
+ "{actor} created event {event} in calendar {calendar}" : "{actor} criou o evento {event} in o calendário {calendar}",
+ "You created event {event} in calendar {calendar}" : "criou o evento {event} in o calendário {calendar}",
+ "{actor} deleted event {event} from calendar {calendar}" : "{actor} eliminou o evento {event} de o calendário {calendar}",
+ "You deleted event {event} from calendar {calendar}" : "eliminou o evento {event} de o calendário {calendar}",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} atualizou o evento {event} in o calendário {calendar}",
+ "You updated event {event} in calendar {calendar}" : "atualizou o evento {event} in o calendário {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} moveu o evento {event} de o calendário {sourceCalendar} para o calendário {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "moveu o evento {event} de o calendário {sourceCalendar} para o calendário {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} restaurou o evento {event} of o calendário {calendar}",
+ "You restored event {event} of calendar {calendar}" : "restaurou o evento {event} of o calendário {calendar}",
+ "Busy" : "Ocupado",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} criou to-do {todo} in list {calendar}",
+ "You created to-do {todo} in list {calendar}" : "criou to-do {todo} in list {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} eliminou to-do {todo} de list {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "eliminou to-do {todo} de list {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} atualizou to-do {todo} in list {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "atualizou to-do {todo} in list {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} concluiu a tarefa {todo} na lista {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "solved to-do {todo} in list {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} reabriu a tarefa {todo} na lista {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "reopened to-do {todo} in list {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} moveu to-do {todo} de list {sourceCalendar} para list {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "moveu to-do {todo} de list {sourceCalendar} para list {targetCalendar}",
+ "Calendar, contacts and tasks" : "Calendário, contactos e tarefas",
+ "A <strong>calendar</strong> was modified" : "O <strong>calendário</strong> foi modificado.",
+ "A calendar <strong>event</strong> was modified" : "Um <strong>event</strong> do calendário foi modificado",
+ "A calendar <strong>to-do</strong> was modified" : "Foi modificada uma <strong>tarefa</strong> do calendário",
+ "Contact birthdays" : "Aniversário dos contactos",
+ "Death of %s" : "Falecimento de %s",
+ "Untitled calendar" : "Calendário sem titulo",
+ "Calendar:" : "Calendário:",
+ "Date:" : "Data:",
+ "Where:" : "Local:",
+ "Description:" : "Descrição:",
+ "_%n year_::_%n years_" : ["%n ano","%n anos","%n anos"],
+ "_%n month_::_%n months_" : ["%n mês","%n meses","%n meses"],
+ "_%n day_::_%n days_" : ["%n dia","%n dias","%n dias"],
+ "_%n hour_::_%n hours_" : ["%n hora","%n horas","%n horas"],
+ "_%n minute_::_%n minutes_" : ["%n minuto","%n minutos","%n minutos"],
+ "%s (in %s)" : "%s (em %s)",
+ "%s (%s ago)" : "%s (%s atrás)",
+ "Calendar: %s" : "Calendário: %s",
+ "Date: %s" : "Data: %s",
+ "Description: %s" : "Descrição: %s",
+ "Where: %s" : "Local: %s",
+ "%1$s via %2$s" : "%1$s via %2$s",
+ "In the past on %1$s for the entire day" : "No passado em %1$s durante todo o dia",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Daqui a um minuto em %1$s para todo o dia","Daqui a %n minutos em %1$s para todo o dia","Daqui a %n minutos em %1$s para todo o dia"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Daqui a um hora em %1$s para todo o dia","Daqui a %n horas em %1$s para todo o dia","Daqui a %n horas em %1$s para todo o dia"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Daqui a um dia em %1$s para todo o dia","Daqui a %n dias em %1$s para todo o dia","Daqui a %n dias em %1$s para todo o dia"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Daqui a um semana em %1$s para todo o dia","Daqui a %n semanas em %1$s para todo o dia","Daqui a %n semanas em %1$s para todo o dia"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Daqui a um mês em %1$s para todo o dia","Daqui a %n meses em %1$s para todo o dia","Daqui a %n meses em %1$s para todo o dia"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Daqui a um ano em %1$s para todo o dia","Daqui a %n anos em %1$s para todo o dia","Daqui a %n anos em %1$s para todo o dia"],
+ "In the past on %1$s between %2$s - %3$s" : "No passado em %1$s entre %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Daqui a um minuto em %1$s entre %2$s - %3$s","Daqui a %n minutos em %1$s entre %2$s - %3$s","Daqui a %n minutos em %1$s entre %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Daqui a um hora em %1$s entre %2$s - %3$s","Daqui a %n horas em %1$s entre %2$s - %3$s","Daqui a %n horas em %1$s entre %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Daqui a um dia em %1$s entre %2$s - %3$s","Daqui a %n dias em %1$s entre %2$s - %3$s","Daqui a %n dias em %1$s entre %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Daqui a um semana em %1$s entre %2$s - %3$s","Daqui a %n semanas em %1$s entre %2$s - %3$s","Daqui a %n semanas em %1$s entre %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Daqui a um mês em %1$s entre %2$s - %3$s","Daqui a %n meses em %1$s entre %2$s - %3$s","Daqui a %n meses em %1$s entre %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Daqui a um ano em %1$s entre %2$s - %3$s","Daqui a %n anos em %1$s entre %2$s - %3$s","Daqui a %n anos em %1$s entre %2$s - %3$s"],
+ "Could not generate when statement" : "Não foi possível gerar a expressão \"quando\"",
+ "Every Day for the entire day" : "Todos os dias, dia inteiro",
+ "Every Day for the entire day until %1$s" : "Todos os dias, dia inteiro até %1$s",
+ "Every Day between %1$s - %2$s" : "Todos os dias entre %1$s e %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Todos os dias entre %1$s e %2$s até %3$s",
+ "Every %1$d Days for the entire day" : "A cada %1$d dias, dia inteiro",
+ "Every %1$d Days for the entire day until %2$s" : "A cada %1$d dias, dia inteiro até %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "A cada %1$d dias entre %2$s e %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "A cada %1$d dias entre %2$s e %3$s até %4$s",
+ "Could not generate event recurrence statement" : "Não foi possível gerar a expressão de recorrência do evento",
+ "Every Week on %1$s for the entire day" : "Todas as semanas, em %1$s, dia inteiro",
+ "Every Week on %1$s for the entire day until %2$s" : "Todas as semanas, em %1$s, dia inteiro até %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Todas as semanas, em %1$s entre %2$s e %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Todas as semanas, em %1$s entre %2$s e %3$s até %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "A cada %1$d semanas, em %2$s, dia inteiro",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "A cada %1$d semanas, em %2$s, dia inteiro até %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "A cada %1$d semanas, em %2$s entre %3$s e %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "A cada %1$d semanas, em %2$s entre %3$s e %4$s até %5$s",
+ "Every Month on the %1$s for the entire day" : "Todos os meses, no dia %1$s, dia inteiro",
+ "Every Month on the %1$s for the entire day until %2$s" : "Todos os meses, no dia %1$s, dia inteiro até %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Todos os meses, no dia %1$s entre %2$s e %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Todos os meses, no dia %1$s entre %2$s e %3$s até %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "A cada %1$d meses, no dia %2$s, dia inteiro",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "A cada %1$d meses, no dia %2$s, dia inteiro até %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "A cada %1$d meses, no dia %2$s entre %3$s e %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "A cada %1$d meses, no dia %2$s entre %3$s e %4$s até %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Todos os anos em %1$s, no dia %2$s, dia inteiro",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Todos os anos em %1$s, no dia %2$s, dia inteiro até %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Todos os anos em %1$s, no dia %2$s entre %3$s e %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Todos os anos em %1$s, no dia %2$s entre %3$s e %4$s até %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "A cada %1$d anos em %2$s, no dia %3$s, dia inteiro",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "A cada %1$d anos em %2$s, no dia %3$s, dia inteiro até %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "A cada %1$d anos em %2$s, no dia %3$s entre %4$s e %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "A cada %1$d anos em %2$s, no dia %3$s entre %4$s e %5$s até %6$s",
+ "On specific dates for the entire day until %1$s" : "Em datas específicas, dia inteiro até %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "Em datas específicas entre %1$s e %2$s até %3$s",
+ "In the past on %1$s" : "No passado em %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Daqui a um minuto em %1$s","Daqui a %n minutos em %1$s","Daqui a %n minutos em %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Daqui a um hora em %1$s","Daqui a %n horas em %1$s","Daqui a %n horas em %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Daqui a um dia em %1$s","Daqui a %n dias em %1$s","Daqui a %n dias em %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Daqui a um semana em %1$s","Daqui a %n semanas em %1$s","Daqui a %n semanas em %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Daqui a um mês em %1$s","Daqui a %n meses em %1$s","Daqui a %n meses em %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Daqui a um ano em %1$s","Daqui a %n anos em %1$s","Daqui a %n anos em %1$s"],
+ "In the past on %1$s then on %2$s" : "No passado em %1$s e depois %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Daqui a um minuto em %1$s e depois em %2$s","Daqui a %n minutos em %1$s e depois em %2$s","Daqui a %n minutos em %1$s e depois em %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Daqui a um hora em %1$s e depois em %2$s","Daqui a %n horas em %1$s e depois em %2$s","Daqui a %n horas em %1$s e depois em %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Daqui a um dia em %1$s e depois em %2$s","Daqui a %n dias em %1$s e depois em %2$s","Daqui a %n dias em %1$s e depois em %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Daqui a um semana em %1$s e depois em %2$s","Daqui a %n semanas em %1$s e depois em %2$s","Daqui a %n semanas em %1$s e depois em %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Daqui a um mês em %1$s e depois em %2$s","Daqui a %n meses em %1$s e depois em %2$s","Daqui a %n meses em %1$s e depois em %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Daqui a um ano em %1$s e depois em %2$s","Daqui a %n anos em %1$s e depois em %2$s","Daqui a %n anos em %1$s e depois em %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "No passado em %1$s e depois em %2$s e %3$s",
+ "_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_" : ["Daqui a um minuto em %1$s e depois em %2$s e %3$s","Daqui a %n minutos em %1$s e depois em %2$s e %3$s","Daqui a %n minutos em %1$s e depois em %2$s e %3$s"],
+ "_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_" : ["Daqui a um hora em %1$s e depois em %2$s e %3$s","Daqui a %n horas em %1$s e depois em %2$s e %3$s","Daqui a %n horas em %1$s e depois em %2$s e %3$s"],
+ "_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_" : ["Daqui a um dia em %1$s e depois em %2$s e %3$s","Daqui a %n dias em %1$s e depois em %2$s e %3$s","Daqui a %n dias em %1$s e depois em %2$s e %3$s"],
+ "_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_" : ["Daqui a um semana em %1$s e depois em %2$s e %3$s","Daqui a %n semanas em %1$s e depois em %2$s e %3$s","Daqui a %n semanas em %1$s e depois em %2$s e %3$s"],
+ "_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_" : ["Daqui a um mês em %1$s e depois em %2$s e %3$s","Daqui a %n meses em %1$s e depois em %2$s e %3$s","Daqui a %n meses em %1$s e depois em %2$s e %3$s"],
+ "_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_" : ["Daqui a um ano em %1$s e depois em %2$s e %3$s","Daqui a %n anos em %1$s e depois em %2$s e %3$s","Daqui a %n anos em %1$s e depois em %2$s e %3$s"],
+ "Could not generate next recurrence statement" : "Não foi possível gerar a próxima expressão de recorrência",
+ "Cancelled: %1$s" : "Cancelado: %1$s",
+ "\"%1$s\" has been canceled" : "\"%1$s\" foi cancelado",
+ "Re: %1$s" : "Re: %1$s",
+ "%1$s has accepted your invitation" : "%1$s aceitou o seu convite",
+ "%1$s has tentatively accepted your invitation" : "%1$s aceitou provisoriamente o seu convite",
+ "%1$s has declined your invitation" : "%1$s recusou o seu convite",
+ "%1$s has responded to your invitation" : "%1$s respondeu ao seu convite",
+ "Invitation updated: %1$s" : "Convite atualizado: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s atualizou o evento \"%2$s\"",
+ "Invitation: %1$s" : "Convite: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s gostaria de convidá‑lo para \"%2$s\"",
+ "Organizer:" : "Organizador:",
+ "Attendees:" : "Participantes:",
+ "Title:" : "Titulo: ",
+ "When:" : "Quando:",
+ "Location:" : "Localização:",
+ "Link:" : "Ligação:",
+ "Occurring:" : "Ocorrências:",
+ "Accept" : "Aceitar",
+ "Decline" : "Recusar",
+ "More options …" : "Mais opções…",
+ "More options at %s" : "Mais opções em %s",
+ "Monday" : "Segunda",
+ "Tuesday" : "Terça",
+ "Wednesday" : "Quarta",
+ "Thursday" : "Quinta",
+ "Friday" : "Sexta",
+ "Saturday" : "Sábado",
+ "Sunday" : "Domingo",
+ "January" : "Janeiro",
+ "February" : "Fevereiro",
+ "March" : "Março",
+ "April" : "Abril",
+ "May" : "Maio",
+ "June" : "Junho",
+ "July" : "Julho",
+ "August" : "Agosto",
+ "September" : "Setembro",
+ "October" : "Outubro",
+ "November" : "Novembro",
+ "December" : "Dezembro",
+ "First" : "Primeiro",
+ "Second" : "Segundo",
+ "Third" : "Terceiro",
+ "Fourth" : "Quarto",
+ "Fifth" : "Quinto",
+ "Last" : "Último",
+ "Second Last" : "Penúltimo",
+ "Third Last" : "Antepenúltimo",
+ "Fourth Last" : "Quarto do fim",
+ "Fifth Last" : "Quinto do fim",
+ "Contacts" : "Contactos",
+ "{actor} created address book {addressbook}" : "{actor} criou address book {addressbook}",
+ "You created address book {addressbook}" : "criou address book {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} eliminou address book {addressbook}",
+ "You deleted address book {addressbook}" : "eliminou address book {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} atualizou address book {addressbook}",
+ "You updated address book {addressbook}" : "atualizou address book {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} partilhou address book {addressbook} com you",
+ "You shared address book {addressbook} with {user}" : "partilhou address book {addressbook} com {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} partilhou address book {addressbook} com {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} deixou de partilhar address book {addressbook} de you",
+ "You unshared address book {addressbook} from {user}" : "deixou de partilhar address book {addressbook} de {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} deixou de partilhar address book {addressbook} de {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} deixou de partilhar address book {addressbook} de próprio",
+ "You shared address book {addressbook} with group {group}" : "partilhou address book {addressbook} com o grupo {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} partilhou address book {addressbook} com o grupo {group}",
+ "You unshared address book {addressbook} from group {group}" : "deixou de partilhar address book {addressbook} do grupo {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} deixou de partilhar address book {addressbook} do grupo {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} criou contact {card} in address book {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "criou contact {card} in address book {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} eliminou contact {card} de address book {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "eliminou contact {card} de address book {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} atualizou contact {card} in address book {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "atualizou contact {card} in address book {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Foi modificado um <strong>contacto</strong> ou <strong>livro de endereços</strong>",
+ "Accounts" : "Contas",
+ "System address book which holds all accounts" : "Livro de endereços do sistema que contém todas as contas",
+ "File is not updatable: %1$s" : "O ficheiro não pode ser atualizado: %1$s",
+ "Failed to get storage for file" : "Falha ao obter armazenamento para o ficheiro",
+ "Could not write to final file, canceled by hook" : "Não foi possível escrever no ficheiro final, cancelado por hook",
+ "Could not write file contents" : "Não foi possível escrever o conteúdo do ficheiro",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes","%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Erro ao copiar ficheiro para o destino (copiado: %1$s, tamanho esperado: %2$s)",
+ "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." : "Tamanho esperado de %1$s, mas leitura diferente (do cliente Nextcloud) e escreveu (para armazenamento Nextcloud) %2$s. Possível problema do lado do cliente ou ao escrever no armazenamento do servidor.",
+ "Could not rename part file to final file, canceled by hook" : "Não foi possível renomear o ficheiro parcial para ficheiro final, cancelado por hook",
+ "Could not rename part file to final file" : "Não foi possível renomear o ficheiro parcial para ficheiro final",
+ "Failed to check file size: %1$s" : "Falha ao verificar o tamanho do ficheiro: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "Não foi possível abrir o ficheiro: %1$s, o ficheiro parece existir",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Não foi possível abrir o ficheiro: %1$s, o ficheiro não parece existir",
+ "Encryption not ready: %1$s" : "Encriptação não pronta: %1$s",
+ "Failed to open file: %1$s" : "Falha ao abrir o ficheiro: %1$s",
+ "Failed to unlink: %1$s" : "Falha ao eliminar: %1$s",
+ "Failed to write file contents: %1$s" : "Falha ao escrever o conteúdo do ficheiro: %1$s",
+ "File not found: %1$s" : "Ficheiro não encontrado: %1$s",
+ "Invalid target path" : "Caminho de destino inválido",
+ "System is in maintenance mode." : "O sistema está em modo de manutenção.",
+ "Upgrade needed" : "É necessária atualização",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "O seu %s precisa de estar configurado para usar HTTPS para que possa utilizar CalDAV e CardDAV com iOS/macOS.",
+ "Configures a CalDAV account" : "Configura uma conta CalDAV",
+ "Configures a CardDAV account" : "Configura uma conta CardDAV",
+ "Events" : "Eventos",
+ "Untitled task" : "Tarefa sem título",
+ "Completed on %s" : "Concluída em %s",
+ "Due on %s by %s" : "Vence em %s às %s",
+ "Due on %s" : "Vence em %s",
+ "System Address Book" : "Livro de endereços do sistema",
+ "The system address book contains contact information for all users in your instance." : "O livro de endereços do sistema contém informação de contacto de todos os utilizadores da sua instância.",
+ "Enable System Address Book" : "Ativar livro de endereços do sistema",
+ "DAV system address book" : "Livro de endereços DAV do sistema",
+ "No outstanding DAV system address book sync." : "Sem sincronizações pendentes do livro de endereços DAV do sistema.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "A sincronização do livro de endereços DAV do sistema ainda não foi executada porque a sua instância não tem utilizadores; execute‑a manualmente com \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "Endpoint WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Não foi possível verificar se o seu servidor web está corretamente configurado para permitir a sincronização de ficheiros via WebDAV. Verifique manualmente.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "O seu servidor da Web não está configurado corretamente para permitir a sincronização de ficheiros, porque a interface WebDAV parece estar com problemas.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "O seu servidor web está corretamente configurado para permitir a sincronização de ficheiros via WebDAV.",
+ "Migrated calendar (%1$s)" : "Calendário migrado (%1$s)",
+ "Calendars including events, details and attendees" : "Calendários incluindo eventos, detalhes e participantes",
+ "Contacts and groups" : "Contactos e grupos",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "Ausência guardada",
+ "Failed to save your absence settings" : "Falha ao guardar as suas definições de ausência",
+ "Absence cleared" : "Ausência removida",
+ "Failed to clear your absence settings" : "Falha ao remover as suas definições de ausência",
+ "First day" : "Primeiro dia",
+ "Last day (inclusive)" : "Último dia (inclusive)",
+ "Out of office replacement (optional)" : "Substituto durante ausência (opcional)",
+ "Name of the replacement" : "Nome do substituto",
+ "No results." : "Sem resultados.",
+ "Start typing." : "Comece a escrever.",
+ "Short absence status" : "Estado de ausência curto",
+ "Long absence Message" : "Mensagem de ausência longa",
+ "Save" : "Guardar",
+ "Disable absence" : "Desativar ausência",
+ "Failed to load availability" : "Falha ao carregar disponibilidade",
+ "Saved availability" : "Disponibilidade guardada",
+ "Failed to save availability" : "Falha ao guardar disponibilidade",
+ "Time zone:" : "Fuso horário:",
+ "to" : "Para",
+ "Delete slot" : "Eliminar intervalo",
+ "No working hours set" : "Sem horário de trabalho definido",
+ "Add slot" : "Adicionar intervalo",
+ "Weekdays" : "Dias da semana",
+ "Pick a start time for {dayName}" : "Escolha uma hora de início para {dayName}",
+ "Pick a end time for {dayName}" : "Escolha uma hora de fim para {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Definir automaticamente o estado para \"Não incomodar\" fora do horário de disponibilidade para silenciar todas as notificações.",
+ "Cancel" : "Cancelar",
+ "Import" : "Importar",
+ "Error while saving settings" : "Erro ao guardar definições",
+ "Contact reset successfully" : "Contacto reposto com sucesso",
+ "Error while resetting contact" : "Erro ao repor contacto",
+ "Contact imported successfully" : "Contacto importado com sucesso",
+ "Error while importing contact" : "Erro ao importar contacto",
+ "Import contact" : "Importar contacto",
+ "Reset to default" : "Repor original",
+ "Import contacts" : "Importar contactos",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "A importação de um novo ficheiro .vcf eliminará o contacto predefinido existente e substituí‑lo‑á pelo novo. Deseja continuar?",
+ "Failed to save example event creation setting" : "Falha ao guardar a definição de criação de evento de exemplo",
+ "Failed to upload the example event" : "Falha ao carregar o evento de exemplo",
+ "Custom example event was saved successfully" : "Evento de exemplo personalizado guardado com sucesso",
+ "Failed to delete the custom example event" : "Falha ao eliminar o evento de exemplo personalizado",
+ "Custom example event was deleted successfully" : "Evento de exemplo personalizado eliminado com sucesso",
+ "Import calendar event" : "Importar evento de calendário",
+ "Uploading a new event will overwrite the existing one." : "O carregamento de um novo evento substituirá o existente.",
+ "Upload event" : "Carregar evento",
+ "Availability" : "Disponibilidade",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Se configurar o seu horário de trabalho, outras pessoas saberão quando está fora do escritório ao marcar uma reunião.",
+ "Absence" : "Ausência",
+ "Configure your next absence period." : "Configure o seu próximo período de ausência.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Instale também a {calendarappstoreopen}aplicação Calendário {linkclose}, ou {calendardocopen} ligue o seu computador e telemóvel para sincronizar ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Certifique‑se de configurar corretamente {emailopen}o servidor de email{linkclose}.",
+ "Calendar server" : "Servidor de calendário",
+ "Send invitations to attendees" : "Enviar convites aos participantes",
+ "Automatically generate a birthday calendar" : "Gerar automaticamente um calendário de aniversários",
+ "Birthday calendars will be generated by a background job." : "Os calendários de aniversário serão gerados por uma tarefa em segundo plano.",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "Por isso, não estarão disponíveis imediatamente após a ativação, mas aparecerão após algum tempo.",
+ "Send notifications for events" : "Enviar notificações de eventos",
+ "Notifications are sent via background jobs, so these must occur often enough." : "As notificações são enviadas através de tarefas em segundo plano; estas devem ser executadas com frequência suficiente.",
+ "Send reminder notifications to calendar sharees as well" : "Enviar também notificações de lembrete aos partilhados do calendário",
+ "Reminders are always sent to organizers and attendees." : "Os lembretes são sempre enviados aos organizadores e participantes.",
+ "Enable notifications for events via push" : "Ativar notificações push para eventos",
+ "Example content" : "Conteúdo de exemplo",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "O conteúdo de exemplo serve para demonstrar as funcionalidades do Nextcloud. É fornecido com o Nextcloud e pode ser substituído por conteúdo personalizado.",
+ "There was an error updating your attendance status." : "Ocorreu um erro ao atualizar o seu estado de participação.",
+ "Please contact the organizer directly." : "Contacte diretamente o organizador.",
+ "Are you accepting the invitation?" : "Aceita o convite?",
+ "Tentative" : "Tentativa",
+ "Your attendance was updated successfully." : "O seu estado de participação foi atualizado com sucesso."
+},"pluralForm" :"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"
+} \ No newline at end of file
diff --git a/apps/dav/l10n/ro.js b/apps/dav/l10n/ro.js
deleted file mode 100644
index d603496099d..00000000000
--- a/apps/dav/l10n/ro.js
+++ /dev/null
@@ -1,60 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Calendar",
- "Todos" : "De făcut",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} a creat calendarul {calendar}",
- "You created calendar {calendar}" : "Ai creat calendarul {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} a șters calendarul {calendar}",
- "You deleted calendar {calendar}" : "Ai șters calendarul {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} a actualiza calendarul {calendar}",
- "You updated calendar {calendar}" : "Ai actualizat calendarul {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} a partajat calendarul {calendar} cu tine",
- "You shared calendar {calendar} with {user}" : "Ai partajat calendarul {calendar} cu {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} a partajat calendarul {calendar} cu {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} a eliminat partajarea calendarului {calendar} cu tine",
- "You unshared calendar {calendar} from {user}" : "Ai eliminat partajarea calendarului {calendar} cu {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} a eliminat partajarea calendarului {calendar} cu {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} a eliminat partajarea calendarului {calendar} catre sine",
- "You shared calendar {calendar} with group {group}" : "Ai partajat calendarul {calendar} cu grupul {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} a partajat calendarul {calendar} cu grupul {group}",
- "You unshared calendar {calendar} from group {group}" : "Ai eliminat partajarea calendarului {calendar} către grupul {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} a elimina partajarea calendarului {calendar} către grupul {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} a creat evenimentul {event} în calendarul {calendar}",
- "You created event {event} in calendar {calendar}" : "Ai creat evenimentul {event} în calendarul {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} a șters evenimentul {event} din calendarul {calendar}",
- "You deleted event {event} from calendar {calendar}" : "AI șters evenimentul {event} din calendarul {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} a actualizat evenimentul {event} din calendarul {calendar}",
- "You updated event {event} in calendar {calendar}" : "Ai actualizat evenimentul {event} din calendarul {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} a creat lista {todo} în calendarul {calendar}",
- "You created todo {todo} in list {calendar}" : "Ai creat lista {todo} în lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} a șters lista {todo} din calendarul {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Ai șters lista {todo} din calendarul {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} a actualizat lista {todo} din calendarul {calendar}",
- "You updated todo {todo} in list {calendar}" : "Ai actualizat lista {todo} din calendarul {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} a completat lista {todo} din calendarul {calendar}",
- "You solved todo {todo} in list {calendar}" : "Ai completat lista {todo} din calendarul {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} a redeschis lista {todo} din calendarul {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Ai redeschis lista {todo} din calendarul {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendar</strong> a fost modificat",
- "A calendar <strong>event</strong> was modified" : "Un <strong>eveniment</strong> din calendar a fost modificat",
- "A calendar <strong>todo</strong> was modified" : "O <strong>listă</strong> din calendar a fost modificată",
- "Contact birthdays" : "Zile de naștere ale persoanelor de contact",
- "Where:" : "Unde:",
- "Description:" : "Descriere:",
- "_%n year_::_%n years_" : ["%nan","%nani","%nani"],
- "%1$s via %2$s" : "%1$sprin %2$s",
- "Hello %s," : "Salut %s,",
- "Accept" : "Accept",
- "Decline" : "Refuză",
- "Contacts" : "Persoane de contact",
- "Tasks" : "Sarcini",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativă",
- "Save" : "Salvează",
- "Technical details" : "Detalii tehnice",
- "Remote Address: %s" : "Adresă la distanță: %s",
- "Request ID: %s" : "ID-ul cererii: %s"
-},
-"nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));");
diff --git a/apps/dav/l10n/ro.json b/apps/dav/l10n/ro.json
deleted file mode 100644
index fb2c0a76987..00000000000
--- a/apps/dav/l10n/ro.json
+++ /dev/null
@@ -1,58 +0,0 @@
-{ "translations": {
- "Calendar" : "Calendar",
- "Todos" : "De făcut",
- "Personal" : "Personal",
- "{actor} created calendar {calendar}" : "{actor} a creat calendarul {calendar}",
- "You created calendar {calendar}" : "Ai creat calendarul {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} a șters calendarul {calendar}",
- "You deleted calendar {calendar}" : "Ai șters calendarul {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} a actualiza calendarul {calendar}",
- "You updated calendar {calendar}" : "Ai actualizat calendarul {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} a partajat calendarul {calendar} cu tine",
- "You shared calendar {calendar} with {user}" : "Ai partajat calendarul {calendar} cu {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} a partajat calendarul {calendar} cu {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} a eliminat partajarea calendarului {calendar} cu tine",
- "You unshared calendar {calendar} from {user}" : "Ai eliminat partajarea calendarului {calendar} cu {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} a eliminat partajarea calendarului {calendar} cu {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} a eliminat partajarea calendarului {calendar} catre sine",
- "You shared calendar {calendar} with group {group}" : "Ai partajat calendarul {calendar} cu grupul {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} a partajat calendarul {calendar} cu grupul {group}",
- "You unshared calendar {calendar} from group {group}" : "Ai eliminat partajarea calendarului {calendar} către grupul {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} a elimina partajarea calendarului {calendar} către grupul {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} a creat evenimentul {event} în calendarul {calendar}",
- "You created event {event} in calendar {calendar}" : "Ai creat evenimentul {event} în calendarul {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} a șters evenimentul {event} din calendarul {calendar}",
- "You deleted event {event} from calendar {calendar}" : "AI șters evenimentul {event} din calendarul {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} a actualizat evenimentul {event} din calendarul {calendar}",
- "You updated event {event} in calendar {calendar}" : "Ai actualizat evenimentul {event} din calendarul {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} a creat lista {todo} în calendarul {calendar}",
- "You created todo {todo} in list {calendar}" : "Ai creat lista {todo} în lista {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} a șters lista {todo} din calendarul {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Ai șters lista {todo} din calendarul {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} a actualizat lista {todo} din calendarul {calendar}",
- "You updated todo {todo} in list {calendar}" : "Ai actualizat lista {todo} din calendarul {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} a completat lista {todo} din calendarul {calendar}",
- "You solved todo {todo} in list {calendar}" : "Ai completat lista {todo} din calendarul {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} a redeschis lista {todo} din calendarul {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Ai redeschis lista {todo} din calendarul {calendar}",
- "A <strong>calendar</strong> was modified" : "Un <strong>calendar</strong> a fost modificat",
- "A calendar <strong>event</strong> was modified" : "Un <strong>eveniment</strong> din calendar a fost modificat",
- "A calendar <strong>todo</strong> was modified" : "O <strong>listă</strong> din calendar a fost modificată",
- "Contact birthdays" : "Zile de naștere ale persoanelor de contact",
- "Where:" : "Unde:",
- "Description:" : "Descriere:",
- "_%n year_::_%n years_" : ["%nan","%nani","%nani"],
- "%1$s via %2$s" : "%1$sprin %2$s",
- "Hello %s," : "Salut %s,",
- "Accept" : "Accept",
- "Decline" : "Refuză",
- "Contacts" : "Persoane de contact",
- "Tasks" : "Sarcini",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativă",
- "Save" : "Salvează",
- "Technical details" : "Detalii tehnice",
- "Remote Address: %s" : "Adresă la distanță: %s",
- "Request ID: %s" : "ID-ul cererii: %s"
-},"pluralForm" :"nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/ru.js b/apps/dav/l10n/ru.js
index b74ec8d4919..1a3a284d6ac 100644
--- a/apps/dav/l10n/ru.js
+++ b/apps/dav/l10n/ru.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Календарь",
- "Todos" : "Задачи",
+ "Tasks" : "Задачи",
"Personal" : "Личное",
"{actor} created calendar {calendar}" : "{actor} создал(а) календарь «{calendar}»",
"You created calendar {calendar}" : "Вы создали календарь «{calendar}»",
@@ -25,36 +25,41 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} предоставил(а) группе {group} общий доступ к календарю «{calendar}»",
"You unshared calendar {calendar} from group {group}" : "Вы закрыли группе {group} общий доступ к календарю «{calendar}»",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} закрыл(а) группе {group} общий доступ к календарю «{calendar}»",
+ "Untitled event" : "Событие без названия",
"{actor} created event {event} in calendar {calendar}" : "{actor} создал(а) событие «{event}» в календаре «{calendar}»",
"You created event {event} in calendar {calendar}" : "Вы создали событие «{event}» в календаре «{calendar}»",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} удалил(а) событие «{event}» из календаря «{calendar}»",
"You deleted event {event} from calendar {calendar}" : "Вы удалили событие «{event}» из календаря «{calendar}»",
"{actor} updated event {event} in calendar {calendar}" : "{actor} обновил(а) событие «{event}» в календаре «{calendar}»",
"You updated event {event} in calendar {calendar}" : "Вы обновили событие «{event}» в календаре «{calendar}»",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} переместил(а) событие «{event}» из календаря «{sourceCalendar}» в календарь «{targetCalendar}»",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Вы переместили событие «{event}» из календаря «{sourceCalendar}» в календарь «{targetCalendar}»",
"{actor} restored event {event} of calendar {calendar}" : "{actor} восстановил(а) событие {event} в календаре {calendar}",
"You restored event {event} of calendar {calendar}" : "Вы восстановили событие {event} в календаре {calendar}",
"Busy" : "Занято",
- "{actor} created todo {todo} in list {calendar}" : "{actor} создал(а) задачу «{todo}» в списке «{calendar}»",
- "You created todo {todo} in list {calendar}" : "Вы создали задачу «{todo}» в списке «{calendar}»",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} удалил(а) задачу «{todo}» из списка «{calendar}»",
- "You deleted todo {todo} from list {calendar}" : "Вы удалили задачу «{todo}» из списка «{calendar}»",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} обновил(а) задачу «{todo}» из списка «{calendar}»",
- "You updated todo {todo} in list {calendar}" : "Вы обновили задачу «{todo}» из списка «{calendar}»",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} завершил(а) задачу «{todo}» из списка «{calendar}»",
- "You solved todo {todo} in list {calendar}" : "Вы завершили задачу «{todo}» из списка «{calendar}»",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} повторно(а) открыл задачу «{todo}» из списка «{calendar}»",
- "You reopened todo {todo} in list {calendar}" : "Вы повторно открыли задачу «{todo}» из списка «{calendar}»",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} создал(а) задачу «{todo}» в списке «{calendar}»",
+ "You created to-do {todo} in list {calendar}" : "Вы создали задачу «{todo}» в списке «{calendar}»",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} удалил(а) задачу «{todo}» из списка «{calendar}»",
+ "You deleted to-do {todo} from list {calendar}" : "Вы удалили задачу «{todo}» из списка «{calendar}»",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} обновил(а) задачу «{todo}» из списка «{calendar}»",
+ "You updated to-do {todo} in list {calendar}" : "Вы обновили задачу «{todo}» из списка «{calendar}»",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} завершил(а) задачу «{todo}» из списка «{calendar}»",
+ "You solved to-do {todo} in list {calendar}" : "Вы завершили задачу «{todo}» из списка «{calendar}»",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} повторно(а) открыл задачу «{todo}» из списка «{calendar}»",
+ "You reopened to-do {todo} in list {calendar}" : "Вы повторно открыли задачу «{todo}» из списка «{calendar}»",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} переместил(а) задачу «{todo}» из календаря «{sourceCalendar}» в календарь «{targetCalendar}»",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Вы переместили задачу «{todo}» из календаря «{sourceCalendar}» в календарь «{targetCalendar}»",
"Calendar, contacts and tasks" : "Календарь, контакты и задачи",
"A <strong>calendar</strong> was modified" : "Изменения <strong>календаря</strong> ",
"A calendar <strong>event</strong> was modified" : "В календаре изменено <strong>событие</strong>",
- "A calendar <strong>todo</strong> was modified" : "В календаре изменена <strong>задача</strong>",
+ "A calendar <strong>to-do</strong> was modified" : "В календаре изменена <strong>задача</strong> ",
"Contact birthdays" : "Дни рождения контакта",
"Death of %s" : "Смерть %s",
+ "Untitled calendar" : "Календарь без названия",
"Calendar:" : "Календарь:",
"Date:" : "Дата:",
"Where:" : "Где:",
"Description:" : "Описание:",
- "Untitled event" : "Событие без названия",
"_%n year_::_%n years_" : ["%n год","%n года","%n лет","%n лет"],
"_%n month_::_%n months_" : ["%n месяц","%n месяца","%n месяцев","%n месяцев"],
"_%n day_::_%n days_" : ["%n день","%n дня","%n дней","%n дней"],
@@ -67,22 +72,129 @@ OC.L10N.register(
"Description: %s" : "Описание: %s",
"Where: %s" : "Где: %s",
"%1$s via %2$s" : "%1$s через %2$s",
+ "In the past on %1$s for the entire day" : "В прошлом, %1$s, в течение всего дня",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Через минуту, %1$s, в течение всего дня","Через %n минуты, %1$s, в течение всего дня","Через %n минут, %1$s, в течение всего дня","Через %n минут, %1$s, в течение всего дня"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Через час, %1$s, в течение всего дня","Через %n часа, %1$s, в течение всего дня","Через %n часов, %1$s, в течение всего дня","Через %n часов, %1$s, в течение всего дня"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Через день, %1$s, в течение всего дня","Через %n дня, %1$s, в течение всего дня","Через %n дней,%1$s, в течение всего дня","Через %n дней, %1$s, в течение всего дня"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Через неделю, %1$s, в течение всего дня","Через %n недели, %1$s, в течение всего дня","Через %n недель, %1$s, в течение всего дня","Через %n недель, %1$s, в течение всего дня"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Через месяц, %1$s, в течение всего дня","Через %n месяца, %1$s, в течение всего дня","Через %n месяцев, %1$s, в течение всего дня","Через %n месяцев, %1$s, в течение всего дня"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Через год, %1$s, в течение всего дня","Через %n года, %1$s, в течение всего дня","Через %n лет, %1$s, в течение всего дня","Через %n лет, %1$s, в течение всего дня"],
+ "In the past on %1$s between %2$s - %3$s" : "В прошлом, %1$s, с %2$s до %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Через минуту, %1$s, с %2$s до %3$s","Через %n минуты, %1$s, с %2$s до %3$s","Через %n минут, %1$s, с %2$s до %3$s","Через %n минут, %1$s, с %2$s до %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Через час, %1$s, с %2$s до %3$s","Через %n часа, %1$s, с %2$s до %3$s","Через %n часов, %1$s, с %2$s до %3$s","Через %n часов, %1$s, с %2$s до %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Через день, %1$s, с %2$s до %3$s","Через %n дня %1$s, с %2$s до %3$s","Через %n дней, %1$s, с %2$s до %3$s","Через %n дней, %1$s, с %2$s до %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Через неделю, %1$s, с %2$s до %3$s","Через %n недели, %1$s, с %2$s до %3$s","Через %n недель, %1$s, с %2$s до %3$s","Через %n недель, %1$s, с %2$s до %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Через месяц, %1$s, с %2$s до %3$s","Через %n месяца, %1$s, с %2$s до %3$s","Через %n месяцев, %1$s, с %2$s до %3$s","Через %n месяцев, %1$s, с %2$s до %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Через год, %1$s, с %2$s до %3$s","Через %n года, %1$s, с %2$s до %3$s","Через %n лет, %1$s, с %2$s до %3$s","Через %n лет, %1$s, с %2$s до %3$s"],
+ "Could not generate when statement" : "Не удалось сгенерировать выражение времени",
+ "Every Day for the entire day" : "Каждый день в течение всего дня",
+ "Every Day for the entire day until %1$s" : "Каждый день весь день до %1$s",
+ "Every Day between %1$s - %2$s" : "Каждый день с %1$s по %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Каждый день с %1$s до %2$s до %3$s",
+ "Every %1$d Days for the entire day" : "Каждые %1$d дней в течение всего дня",
+ "Every %1$d Days for the entire day until %2$s" : "Каждые %1$d дней в течение всего дня до %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Каждые %1$d дней с %2$s до %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Каждые %1$d дней с %2$s до %3$s до %4$s",
+ "Could not generate event recurrence statement" : "Не удалось сгенерировать правило повторения события",
+ "Every Week on %1$s for the entire day" : "Каждую неделю в %1$s в течение всего дня",
+ "Every Week on %1$s for the entire day until %2$s" : "Каждую неделю по %1$s в течение всего дня до %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Каждую неделю в %1$s с %2$s до %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Каждую неделю по %1$s с %2$s до %3$s до %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Каждые %1$d недель по %2$s в течение всего дня",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Каждые %1$d недель по %2$s в течение всего дня до %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Каждые %1$d недель по %2$s с %3$s до %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Каждые %1$d недель по %2$s с %3$s до %4$s до %5$s",
+ "Every Month on the %1$s for the entire day" : "Каждый месяц %1$s в течение всего дня",
+ "Every Month on the %1$s for the entire day until %2$s" : "Каждый месяц %1$s в течение всего дня до %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Каждый месяц %1$s с %2$s до %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Каждый месяц %1$s с %2$s до %3$s до %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Каждые %1$d месяцев %2$s в течение всего дня",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Каждые %1$d месяцев %2$s в течение всего дня до %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Каждые %1$d месяцев %2$s с %3$s до %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Каждые %1$d месяцев %2$s с %3$s до %4$s до %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Каждый год в %1$s %2$s в течение всего дня",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Каждый год в %1$s %2$s в течение всего дня до %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Каждый год в %1$s %2$s с %3$s до %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Каждый год в %1$s %2$s с %3$s до %4$s до %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Каждые %1$d лет в %2$s %3$s в течение всего дня",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Каждые %1$d лет в %2$s %3$s в течение всего дня до %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Каждые %1$d лет в %2$s %3$s с %4$s до %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Каждые %1$d лет в %2$s %3$s с %4$s до %5$s до %6$s",
+ "On specific dates for the entire day until %1$s" : "В определённые даты, весь день до %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "В определённые даты с %1$s по %2$s до %3$s",
+ "In the past on %1$s" : "В прошлом, %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Через минуту, %1$s","Через %n минуты, %1$s","Через %n минут, %1$s","Через %n минут, %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Через час, %1$s","Через %n часа, %1$s","Через %n часов, %1$s","Через %n часов, %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Через день, %1$s","Через %n дня, %1$s","Через %n дней, %1$s","Через %n дней, %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Через неделю, %1$s","Через %n недели,%1$s","Через %n недель, %1$s","Через %n недель, %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Через месяц, %1$s","Через %n месяца, %1$s","Через %n месяцев, %1$s","Через %n месяцев, %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Через год, %1$s","Через %n года, %1$s","Через %n лет, %1$s","Через %n лет, %1$s"],
+ "In the past on %1$s then on %2$s" : "В прошлом, %1$s, затем %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Через минуту, %1$s, затем %2$s","Через %n минуты, %1$s, затем %2$s","Через %n минут, %1$s, затем %2$s","Через %n минут, %1$s, затем %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Через час, %1$s, затем %2$s","Через %n часа, %1$s, затем %2$s","Через %n часов, %1$s, затем %2$s","Через %n часов, %1$s, затем %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Через день, %1$s, затем %2$s","Через %n дня, %1$s, затем %2$s","Через %n дней, %1$s, затем %2$s","Через %n дней, %1$s, затем %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Через неделю, %1$s, затем %2$s","Через %n недели, %1$s, затем %2$s","Через %n недель, %1$s, затем %2$s","Через %n недель, %1$s, затем %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Через месяц, %1$s, затем %2$s","Через %n месяца, %1$s, затем %2$s","Через %n месяцев, %1$s, затем %2$s","Через %n месяцев, %1$s, затем %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Через год, %1$s, затем %2$s","Через %n года, %1$s, затем %2$s","Через %n лет, %1$s, затем %2$s","Через %n лет, %1$s, затем %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "В прошлом, %1$s, затем %2$s и %3$s",
+ "_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_" : ["Через минуту, %1$s, затем %2$s и %3$s","Через %n минуты, %1$s, затем %2$s и %3$s","Через %n минут, %1$s, затем %2$s и %3$s","Через %n минут, %1$s, затем %2$s и %3$s"],
+ "_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_" : ["Через час, %1$s, затем %2$s и %3$s","Через %n часа, %1$s, затем %2$s и %3$s","Через %n часов, %1$s, затем %2$s и %3$s","Через %n часов, %1$s, затем %2$s и %3$s"],
+ "_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_" : ["Через день, %1$s, затем %2$s и %3$s","Через %n дня, %1$s, затем %2$s и %3$s","Через %n дней, %1$s, затем %2$s и %3$s","Через %n дней, %1$s, затем %2$s и %3$s"],
+ "_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_" : ["Через неделю, %1$s, затем %2$s и %3$s","Через %n недели, %1$s, затем %2$s и %3$s","Через %n недель, %1$s, затем %2$s и %3$s","Через %n недель, %1$s, затем %2$s и %3$s"],
+ "_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_" : ["Через месяц, %1$s, затем %2$s и %3$s","Через %n месяца, %1$s, затем %2$s и %3$s","Через %n месяцев, %1$s, затем %2$s и %3$s","Через %n месяцев, %1$s, затем %2$s и %3$s"],
+ "_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_" : ["Через год, %1$s, затем %2$s и %3$s","Через %n года, %1$s, затем %2$s и %3$s","Через %n лет, %1$s, затем %2$s и %3$s","Через %n лет, %1$s, затем %2$s и %3$s"],
+ "Could not generate next recurrence statement" : "Не удалось сгенерировать следующий оператор повторения",
"Cancelled: %1$s" : "Событие отменено: %1$s",
- "Invitation canceled" : "Приглашение отменено",
+ "\"%1$s\" has been canceled" : "Событие «%1$s» отменено",
"Re: %1$s" : "Re: %1$s",
- "Invitation updated" : "Приглашение обновлено",
+ "%1$s has accepted your invitation" : "%1$s принял(а) ваше приглашение",
+ "%1$s has tentatively accepted your invitation" : "%1$s предварительно принял(а) ваше приглашение",
+ "%1$s has declined your invitation" : "%1$s отклонил(а) ваше приглашение",
+ "%1$s has responded to your invitation" : "%1$s ответил(а) на ваше приглашение",
+ "Invitation updated: %1$s" : "Изменение приглашения: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s изменил(а) событие «%2$s»",
"Invitation: %1$s" : "Приглашение: %1$s",
- "Invitation" : "Приглашение",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s приглашает вас принять участие в событии «%2$s»",
+ "Organizer:" : "Организатор:",
+ "Attendees:" : "Участники:",
"Title:" : "Название:",
- "Time:" : "Время:",
+ "When:" : "Когда:",
"Location:" : "Местонахождение:",
"Link:" : "Ссылка:",
- "Organizer:" : "Организатор:",
- "Attendees:" : "Участники:",
+ "Occurring:" : "Происходит:",
"Accept" : "Принять",
"Decline" : "Отклонить",
"More options …" : "Дополнительные параметры…",
"More options at %s" : "Дополнительные параметры на %s",
+ "Monday" : "Понедельник",
+ "Tuesday" : "Вторник",
+ "Wednesday" : "Среда",
+ "Thursday" : "Четверг",
+ "Friday" : "Пятница",
+ "Saturday" : "Суббота",
+ "Sunday" : "Воскресенье",
+ "January" : "Январь",
+ "February" : "Февраль",
+ "March" : "Март",
+ "April" : "Апрель",
+ "May" : "Май",
+ "June" : "Июнь",
+ "July" : "Июль",
+ "August" : "Август",
+ "September" : "Сентябрь",
+ "October" : "Октябрь",
+ "November" : "Ноябрь",
+ "December" : "Декабрь",
+ "First" : "Первый",
+ "Second" : "Второй",
+ "Third" : "Третий",
+ "Fourth" : "Четвертый",
+ "Fifth" : "Пятый",
+ "Last" : "Последний",
+ "Second Last" : "Предпоследний",
+ "Third Last" : "Третий с конца",
+ "Fourth Last" : "Четвёртый с конца",
+ "Fifth Last" : "Пятый с конца",
"Contacts" : "Контакты",
"{actor} created address book {addressbook}" : "{actor} создал(а) адресную книгу «{addressbook}»",
"You created address book {addressbook}" : "Вы создали адресную книгу «{addressbook}»",
@@ -108,35 +220,103 @@ OC.L10N.register(
"{actor} updated contact {card} in address book {addressbook}" : "{actor} изменил(а) запись {card} в адресной книге {addressbook}",
"You updated contact {card} in address book {addressbook}" : "Вы изменили запись {card} в адресной книге {addressbook}",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "Изменение <strong>контакта</strong> или <strong>адресной книги</strong>",
+ "Accounts" : "Учётные записи",
+ "System address book which holds all accounts" : "Системная адресная книга, в которой хранятся все учетные записи",
+ "File is not updatable: %1$s" : "Файл не подлежит обновлению: %1$s",
+ "Failed to get storage for file" : "Не удалось получить хранилище для файла",
+ "Could not write to final file, canceled by hook" : "Не удалось записать результирующий файл, запись отменена вызовом обработчика",
+ "Could not write file contents" : "Не удалось записать содержимое файла",
+ "_%n byte_::_%n bytes_" : ["%n байт","%n байта","%n байт","%n байта"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Ошибка при копировании в целевое расположение, скопировано: %1$s, ожидаемый размер файла: %2$s",
+ "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." : "Ожидаемый размер файла составляет %1$s, но из клиента приложения Nextcloud было прочитано и записано в хранилище %2$s. К этому могла привести ошибка при передаче данных на стороне отправителя либо проблема в подсистеме хранения данных на стороне сервера.",
+ "Could not rename part file to final file, canceled by hook" : "Не удалось переименовать временный файл в результирующий, операция отменена вызовом обработчика",
+ "Could not rename part file to final file" : "Не удалось переименовать временный файл в результирующий",
+ "Failed to check file size: %1$s" : "Не удалось проверить размер файла: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "Не удалось открыть файл: %1$s, файл, похоже, существует",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Не удалось открыть файл: %1$s, файл, похоже, не существует",
+ "Encryption not ready: %1$s" : "Подсистема шифрования не готова: %1$s",
+ "Failed to open file: %1$s" : "Не удалось открыть файл: %1$s",
+ "Failed to unlink: %1$s" : "Не удалось разорвать связь: %1$s",
+ "Failed to write file contents: %1$s" : "Не удалось записать содержимое файла: %1$s",
+ "File not found: %1$s" : "Файл не найден: %1$s",
+ "Invalid target path" : "Неверный целевой путь",
"System is in maintenance mode." : "Сервер находится в режиме обслуживания.",
"Upgrade needed" : "Требуется обновление",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Ваш %s должен быть настроен на использование протокола HTTPS, чтобы можно было использовать CalDAV и CardDAV на iOS/macOS.",
"Configures a CalDAV account" : "Настройка аккаунта CalDAV",
"Configures a CardDAV account" : "Настройка аккаунта CardDAV",
"Events" : "События",
- "Tasks" : "Задачи",
"Untitled task" : "Задача без названия",
"Completed on %s" : "Завершено %s",
"Due on %s by %s" : "До %s %s",
"Due on %s" : "До %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Добро пожаловать в Календарь Nextcloud!\n\nЭто пример события — исследуйте возможности планирования с Календарём Nextcloud, внося любые изменения по своему усмотрению!\n\nС помощью Календаря Nextcloud вы можете:\n- Легко создавать, редактировать и управлять событиями.\n- Создавать несколько календарей и делиться ими с коллегами, друзьями или семьёй.\n- Проверять доступность и показывать другим своё занятое время.\n- Беспрепятственно подключать приложения и устройства через CalDAV.\n- Настроить календарь под себя: повторяющиеся события, уведомления и многое другое.",
+ "Example event - open me!" : "Пример события — отройте меня!",
+ "System Address Book" : "Системная адресная книга",
+ "The system address book contains contact information for all users in your instance." : "Системная адресная книга содержит контактную информацию всех пользователей в вашем инстансе.",
+ "Enable System Address Book" : "Включить системную адресную книгу",
+ "DAV system address book" : "Системная адресная книга DAV",
+ "No outstanding DAV system address book sync." : "Синхронизация системной адресной книги DAV выполнена.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Синхронизация системной адресной книги DAV ещё не запущена, поскольку в этом экземпляре более 1000 пользователей или произошла ошибка. Перезапустите синхронизацию вручную используя команду «occ dav:sync-system-addressbook».",
+ "WebDAV endpoint" : "точка подключения WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Не удалось проверить, правильно ли настроен ваш веб-сервер, чтобы разрешить синхронизацию файлов через WebDAV. Пожалуйста, проверьте вручную.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Веб-сервер ещё не настроен должным образом для синхронизации файлов: похоже, что не работоспособен интерфейс WebDAV.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Ваш веб-сервер настроен должным образом, чтобы обеспечить синхронизацию файлов через WebDAV.",
+ "Migrated calendar (%1$s)" : "Перенос календаря (%1$s)",
+ "Calendars including events, details and attendees" : "Календари, в том числе события, подробные сведения и участники",
"Contacts and groups" : "Контакты и группы",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "точка подключения WebDAV",
- "Availability" : "Доступность",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Если вы настроите свое рабочее время, другие пользователи будут видеть, когда вас нет в офисе, когда они планируют встречу.",
+ "Absence saved" : "Состояние отсутствия сохранено",
+ "Failed to save your absence settings" : "Не удалось сохранить параметры отсутствия",
+ "Absence cleared" : "Состояние отсутствия удалено",
+ "Failed to clear your absence settings" : "Не удалось удалить параметры отсутствия",
+ "First day" : "Первый день",
+ "Last day (inclusive)" : "Последний день (включительно)",
+ "Out of office replacement (optional)" : "Замена вне офиса (по желанию)",
+ "Name of the replacement" : "Название заменяющего устройства",
+ "No results." : "Неверный целевой путь.",
+ "Start typing." : "Начать печатать.",
+ "Short absence status" : "Краткий статус отсутствия",
+ "Long absence Message" : "Длинное сообщение об отсутствии",
+ "Save" : "Сохранить",
+ "Disable absence" : "Отключить отсутствие",
+ "Failed to load availability" : "Не удалось получить сведения о доступности",
+ "Saved availability" : "Сведения о доступности сохранены",
+ "Failed to save availability" : "Не удалось сохранить сведения о доступности",
"Time zone:" : "Часовой пояс:",
"to" : "по",
"Delete slot" : "Удалить интервал",
"No working hours set" : "Рабочие часы не указаны",
"Add slot" : "Добавить интервал",
- "Monday" : "Понедельник",
- "Tuesday" : "Вторник",
- "Wednesday" : "Среда",
- "Thursday" : "Четверг",
- "Friday" : "Пятница",
- "Saturday" : "Суббота",
- "Sunday" : "Воскресенье",
- "Save" : "Сохранить",
+ "Weekdays" : "Дни недели",
+ "Pick a start time for {dayName}" : "Выберите время начала в {dayName}",
+ "Pick a end time for {dayName}" : "Выберите время окончания в {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Автоматически изменять статус на «Не беспокоить» вне интервала доступности для отключения уведомлений.",
+ "Cancel" : "Отмена",
+ "Import" : "Импортировать",
+ "Error while saving settings" : "Ошибка при сохранении параметров",
+ "Contact reset successfully" : "Контакт успешно сброшен",
+ "Error while resetting contact" : "Ошибка при сбросе контакта",
+ "Contact imported successfully" : " Контакт успешно импортирован",
+ "Error while importing contact" : " Ошибка при импорте контакта",
+ "Import contact" : "Импортировать контакт",
+ "Reset to default" : "Сбросить изменения",
+ "Import contacts" : "Импортировать контакты",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Импорт нового файла .vcf удалит существующий контакт по умолчанию и заменит его новым. Продолжить?",
+ "Failed to save example event creation setting" : "Не удалось сохранить настройки создания примера события",
+ "Failed to upload the example event" : "Не удалось загрузить пример события",
+ "Custom example event was saved successfully" : "Настраиваемый пример события успешно сохранён",
+ "Failed to delete the custom example event" : "Не удалось удалить настраиваемый пример события",
+ "Custom example event was deleted successfully" : "Настраиваемый пример события успешно удалён",
+ "Import calendar event" : "Импортировать событие календаря",
+ "Uploading a new event will overwrite the existing one." : " Загрузка нового события перезапишет существующее.",
+ "Upload event" : "Загрузить событие",
+ "Availability" : "Рабочее время",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Если вы настроите свой расписание работы, другие пользователи при планировании встреч смогут видеть, когда вас нет в офисе.",
+ "Absence" : "Отсутствие",
+ "Configure your next absence period." : "Укажите интервал вашего следующего отсутствия.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Также установите {calendarappstoreopen}приложение Calendar{linkclose}, или {calendardocopen}подключите ваш ПК и мобильное устройство для синхронизации ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Проверьте правильность настройки {emailopen}почтового сервера{linkclose}.",
"Calendar server" : "Сервер календаря",
"Send invitations to attendees" : "Отправить приглашения",
"Automatically generate a birthday calendar" : "Создавать календарь дней рождения автоматически",
@@ -144,16 +324,15 @@ OC.L10N.register(
"Hence they will not be available immediately after enabling but will show up after some time." : "И поэтому они станут доступны не моментально, а через некоторое время.",
"Send notifications for events" : "Отправлять уведомления о событиях",
"Notifications are sent via background jobs, so these must occur often enough." : "Уведомления будут отправляться через фоновые задания, поэтому они должны выполняться достаточно часто.",
+ "Send reminder notifications to calendar sharees as well" : "Отправлять напоминания всем пользователям, имеющим доступ к календарю",
+ "Reminders are always sent to organizers and attendees." : "Организаторам и участникам уведомления отправляются во всех случаях.",
"Enable notifications for events via push" : "Включить уведомления о событиях с помощью push",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Также установите {calendarappstoreopen}приложение Calendar{linkclose}, или {calendardocopen}подключите ваш ПК и мобильное устройство для синхронизации ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Проверьте правильность настройки {emailopen}почтового сервера{linkclose}.",
+ "Example content" : "Пример содержимого",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Пример содержимого демонстрирует возможности Nextcloud. По умолчанию поставляется стандартное содержимое, которое можно заменить настраиваемым.",
"There was an error updating your attendance status." : "Ошибка обновления статуса участия.",
"Please contact the organizer directly." : "Обратитесь к организатору напрямую.",
"Are you accepting the invitation?" : "Принять приглашение?",
"Tentative" : "Под вопросом",
- "Number of guests" : "Количество гостей",
- "Comment" : "Комментарий",
- "Your attendance was updated successfully." : "Статус участия обновлён.",
- "Calendar and tasks" : "Календарь и задачи"
+ "Your attendance was updated successfully." : "Статус участия обновлён."
},
"nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);");
diff --git a/apps/dav/l10n/ru.json b/apps/dav/l10n/ru.json
index 578950fbfb9..d2ba65d9ca7 100644
--- a/apps/dav/l10n/ru.json
+++ b/apps/dav/l10n/ru.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Календарь",
- "Todos" : "Задачи",
+ "Tasks" : "Задачи",
"Personal" : "Личное",
"{actor} created calendar {calendar}" : "{actor} создал(а) календарь «{calendar}»",
"You created calendar {calendar}" : "Вы создали календарь «{calendar}»",
@@ -23,36 +23,41 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} предоставил(а) группе {group} общий доступ к календарю «{calendar}»",
"You unshared calendar {calendar} from group {group}" : "Вы закрыли группе {group} общий доступ к календарю «{calendar}»",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} закрыл(а) группе {group} общий доступ к календарю «{calendar}»",
+ "Untitled event" : "Событие без названия",
"{actor} created event {event} in calendar {calendar}" : "{actor} создал(а) событие «{event}» в календаре «{calendar}»",
"You created event {event} in calendar {calendar}" : "Вы создали событие «{event}» в календаре «{calendar}»",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} удалил(а) событие «{event}» из календаря «{calendar}»",
"You deleted event {event} from calendar {calendar}" : "Вы удалили событие «{event}» из календаря «{calendar}»",
"{actor} updated event {event} in calendar {calendar}" : "{actor} обновил(а) событие «{event}» в календаре «{calendar}»",
"You updated event {event} in calendar {calendar}" : "Вы обновили событие «{event}» в календаре «{calendar}»",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} переместил(а) событие «{event}» из календаря «{sourceCalendar}» в календарь «{targetCalendar}»",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Вы переместили событие «{event}» из календаря «{sourceCalendar}» в календарь «{targetCalendar}»",
"{actor} restored event {event} of calendar {calendar}" : "{actor} восстановил(а) событие {event} в календаре {calendar}",
"You restored event {event} of calendar {calendar}" : "Вы восстановили событие {event} в календаре {calendar}",
"Busy" : "Занято",
- "{actor} created todo {todo} in list {calendar}" : "{actor} создал(а) задачу «{todo}» в списке «{calendar}»",
- "You created todo {todo} in list {calendar}" : "Вы создали задачу «{todo}» в списке «{calendar}»",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} удалил(а) задачу «{todo}» из списка «{calendar}»",
- "You deleted todo {todo} from list {calendar}" : "Вы удалили задачу «{todo}» из списка «{calendar}»",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} обновил(а) задачу «{todo}» из списка «{calendar}»",
- "You updated todo {todo} in list {calendar}" : "Вы обновили задачу «{todo}» из списка «{calendar}»",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} завершил(а) задачу «{todo}» из списка «{calendar}»",
- "You solved todo {todo} in list {calendar}" : "Вы завершили задачу «{todo}» из списка «{calendar}»",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} повторно(а) открыл задачу «{todo}» из списка «{calendar}»",
- "You reopened todo {todo} in list {calendar}" : "Вы повторно открыли задачу «{todo}» из списка «{calendar}»",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} создал(а) задачу «{todo}» в списке «{calendar}»",
+ "You created to-do {todo} in list {calendar}" : "Вы создали задачу «{todo}» в списке «{calendar}»",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} удалил(а) задачу «{todo}» из списка «{calendar}»",
+ "You deleted to-do {todo} from list {calendar}" : "Вы удалили задачу «{todo}» из списка «{calendar}»",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} обновил(а) задачу «{todo}» из списка «{calendar}»",
+ "You updated to-do {todo} in list {calendar}" : "Вы обновили задачу «{todo}» из списка «{calendar}»",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} завершил(а) задачу «{todo}» из списка «{calendar}»",
+ "You solved to-do {todo} in list {calendar}" : "Вы завершили задачу «{todo}» из списка «{calendar}»",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} повторно(а) открыл задачу «{todo}» из списка «{calendar}»",
+ "You reopened to-do {todo} in list {calendar}" : "Вы повторно открыли задачу «{todo}» из списка «{calendar}»",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} переместил(а) задачу «{todo}» из календаря «{sourceCalendar}» в календарь «{targetCalendar}»",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Вы переместили задачу «{todo}» из календаря «{sourceCalendar}» в календарь «{targetCalendar}»",
"Calendar, contacts and tasks" : "Календарь, контакты и задачи",
"A <strong>calendar</strong> was modified" : "Изменения <strong>календаря</strong> ",
"A calendar <strong>event</strong> was modified" : "В календаре изменено <strong>событие</strong>",
- "A calendar <strong>todo</strong> was modified" : "В календаре изменена <strong>задача</strong>",
+ "A calendar <strong>to-do</strong> was modified" : "В календаре изменена <strong>задача</strong> ",
"Contact birthdays" : "Дни рождения контакта",
"Death of %s" : "Смерть %s",
+ "Untitled calendar" : "Календарь без названия",
"Calendar:" : "Календарь:",
"Date:" : "Дата:",
"Where:" : "Где:",
"Description:" : "Описание:",
- "Untitled event" : "Событие без названия",
"_%n year_::_%n years_" : ["%n год","%n года","%n лет","%n лет"],
"_%n month_::_%n months_" : ["%n месяц","%n месяца","%n месяцев","%n месяцев"],
"_%n day_::_%n days_" : ["%n день","%n дня","%n дней","%n дней"],
@@ -65,22 +70,129 @@
"Description: %s" : "Описание: %s",
"Where: %s" : "Где: %s",
"%1$s via %2$s" : "%1$s через %2$s",
+ "In the past on %1$s for the entire day" : "В прошлом, %1$s, в течение всего дня",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Через минуту, %1$s, в течение всего дня","Через %n минуты, %1$s, в течение всего дня","Через %n минут, %1$s, в течение всего дня","Через %n минут, %1$s, в течение всего дня"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Через час, %1$s, в течение всего дня","Через %n часа, %1$s, в течение всего дня","Через %n часов, %1$s, в течение всего дня","Через %n часов, %1$s, в течение всего дня"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Через день, %1$s, в течение всего дня","Через %n дня, %1$s, в течение всего дня","Через %n дней,%1$s, в течение всего дня","Через %n дней, %1$s, в течение всего дня"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Через неделю, %1$s, в течение всего дня","Через %n недели, %1$s, в течение всего дня","Через %n недель, %1$s, в течение всего дня","Через %n недель, %1$s, в течение всего дня"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Через месяц, %1$s, в течение всего дня","Через %n месяца, %1$s, в течение всего дня","Через %n месяцев, %1$s, в течение всего дня","Через %n месяцев, %1$s, в течение всего дня"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Через год, %1$s, в течение всего дня","Через %n года, %1$s, в течение всего дня","Через %n лет, %1$s, в течение всего дня","Через %n лет, %1$s, в течение всего дня"],
+ "In the past on %1$s between %2$s - %3$s" : "В прошлом, %1$s, с %2$s до %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Через минуту, %1$s, с %2$s до %3$s","Через %n минуты, %1$s, с %2$s до %3$s","Через %n минут, %1$s, с %2$s до %3$s","Через %n минут, %1$s, с %2$s до %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Через час, %1$s, с %2$s до %3$s","Через %n часа, %1$s, с %2$s до %3$s","Через %n часов, %1$s, с %2$s до %3$s","Через %n часов, %1$s, с %2$s до %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Через день, %1$s, с %2$s до %3$s","Через %n дня %1$s, с %2$s до %3$s","Через %n дней, %1$s, с %2$s до %3$s","Через %n дней, %1$s, с %2$s до %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Через неделю, %1$s, с %2$s до %3$s","Через %n недели, %1$s, с %2$s до %3$s","Через %n недель, %1$s, с %2$s до %3$s","Через %n недель, %1$s, с %2$s до %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Через месяц, %1$s, с %2$s до %3$s","Через %n месяца, %1$s, с %2$s до %3$s","Через %n месяцев, %1$s, с %2$s до %3$s","Через %n месяцев, %1$s, с %2$s до %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Через год, %1$s, с %2$s до %3$s","Через %n года, %1$s, с %2$s до %3$s","Через %n лет, %1$s, с %2$s до %3$s","Через %n лет, %1$s, с %2$s до %3$s"],
+ "Could not generate when statement" : "Не удалось сгенерировать выражение времени",
+ "Every Day for the entire day" : "Каждый день в течение всего дня",
+ "Every Day for the entire day until %1$s" : "Каждый день весь день до %1$s",
+ "Every Day between %1$s - %2$s" : "Каждый день с %1$s по %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Каждый день с %1$s до %2$s до %3$s",
+ "Every %1$d Days for the entire day" : "Каждые %1$d дней в течение всего дня",
+ "Every %1$d Days for the entire day until %2$s" : "Каждые %1$d дней в течение всего дня до %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Каждые %1$d дней с %2$s до %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Каждые %1$d дней с %2$s до %3$s до %4$s",
+ "Could not generate event recurrence statement" : "Не удалось сгенерировать правило повторения события",
+ "Every Week on %1$s for the entire day" : "Каждую неделю в %1$s в течение всего дня",
+ "Every Week on %1$s for the entire day until %2$s" : "Каждую неделю по %1$s в течение всего дня до %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Каждую неделю в %1$s с %2$s до %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Каждую неделю по %1$s с %2$s до %3$s до %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Каждые %1$d недель по %2$s в течение всего дня",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Каждые %1$d недель по %2$s в течение всего дня до %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Каждые %1$d недель по %2$s с %3$s до %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Каждые %1$d недель по %2$s с %3$s до %4$s до %5$s",
+ "Every Month on the %1$s for the entire day" : "Каждый месяц %1$s в течение всего дня",
+ "Every Month on the %1$s for the entire day until %2$s" : "Каждый месяц %1$s в течение всего дня до %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Каждый месяц %1$s с %2$s до %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Каждый месяц %1$s с %2$s до %3$s до %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Каждые %1$d месяцев %2$s в течение всего дня",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Каждые %1$d месяцев %2$s в течение всего дня до %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Каждые %1$d месяцев %2$s с %3$s до %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Каждые %1$d месяцев %2$s с %3$s до %4$s до %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Каждый год в %1$s %2$s в течение всего дня",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Каждый год в %1$s %2$s в течение всего дня до %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Каждый год в %1$s %2$s с %3$s до %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Каждый год в %1$s %2$s с %3$s до %4$s до %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Каждые %1$d лет в %2$s %3$s в течение всего дня",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Каждые %1$d лет в %2$s %3$s в течение всего дня до %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Каждые %1$d лет в %2$s %3$s с %4$s до %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Каждые %1$d лет в %2$s %3$s с %4$s до %5$s до %6$s",
+ "On specific dates for the entire day until %1$s" : "В определённые даты, весь день до %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "В определённые даты с %1$s по %2$s до %3$s",
+ "In the past on %1$s" : "В прошлом, %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Через минуту, %1$s","Через %n минуты, %1$s","Через %n минут, %1$s","Через %n минут, %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Через час, %1$s","Через %n часа, %1$s","Через %n часов, %1$s","Через %n часов, %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Через день, %1$s","Через %n дня, %1$s","Через %n дней, %1$s","Через %n дней, %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Через неделю, %1$s","Через %n недели,%1$s","Через %n недель, %1$s","Через %n недель, %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Через месяц, %1$s","Через %n месяца, %1$s","Через %n месяцев, %1$s","Через %n месяцев, %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Через год, %1$s","Через %n года, %1$s","Через %n лет, %1$s","Через %n лет, %1$s"],
+ "In the past on %1$s then on %2$s" : "В прошлом, %1$s, затем %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Через минуту, %1$s, затем %2$s","Через %n минуты, %1$s, затем %2$s","Через %n минут, %1$s, затем %2$s","Через %n минут, %1$s, затем %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Через час, %1$s, затем %2$s","Через %n часа, %1$s, затем %2$s","Через %n часов, %1$s, затем %2$s","Через %n часов, %1$s, затем %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Через день, %1$s, затем %2$s","Через %n дня, %1$s, затем %2$s","Через %n дней, %1$s, затем %2$s","Через %n дней, %1$s, затем %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Через неделю, %1$s, затем %2$s","Через %n недели, %1$s, затем %2$s","Через %n недель, %1$s, затем %2$s","Через %n недель, %1$s, затем %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Через месяц, %1$s, затем %2$s","Через %n месяца, %1$s, затем %2$s","Через %n месяцев, %1$s, затем %2$s","Через %n месяцев, %1$s, затем %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Через год, %1$s, затем %2$s","Через %n года, %1$s, затем %2$s","Через %n лет, %1$s, затем %2$s","Через %n лет, %1$s, затем %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "В прошлом, %1$s, затем %2$s и %3$s",
+ "_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_" : ["Через минуту, %1$s, затем %2$s и %3$s","Через %n минуты, %1$s, затем %2$s и %3$s","Через %n минут, %1$s, затем %2$s и %3$s","Через %n минут, %1$s, затем %2$s и %3$s"],
+ "_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_" : ["Через час, %1$s, затем %2$s и %3$s","Через %n часа, %1$s, затем %2$s и %3$s","Через %n часов, %1$s, затем %2$s и %3$s","Через %n часов, %1$s, затем %2$s и %3$s"],
+ "_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_" : ["Через день, %1$s, затем %2$s и %3$s","Через %n дня, %1$s, затем %2$s и %3$s","Через %n дней, %1$s, затем %2$s и %3$s","Через %n дней, %1$s, затем %2$s и %3$s"],
+ "_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_" : ["Через неделю, %1$s, затем %2$s и %3$s","Через %n недели, %1$s, затем %2$s и %3$s","Через %n недель, %1$s, затем %2$s и %3$s","Через %n недель, %1$s, затем %2$s и %3$s"],
+ "_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_" : ["Через месяц, %1$s, затем %2$s и %3$s","Через %n месяца, %1$s, затем %2$s и %3$s","Через %n месяцев, %1$s, затем %2$s и %3$s","Через %n месяцев, %1$s, затем %2$s и %3$s"],
+ "_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_" : ["Через год, %1$s, затем %2$s и %3$s","Через %n года, %1$s, затем %2$s и %3$s","Через %n лет, %1$s, затем %2$s и %3$s","Через %n лет, %1$s, затем %2$s и %3$s"],
+ "Could not generate next recurrence statement" : "Не удалось сгенерировать следующий оператор повторения",
"Cancelled: %1$s" : "Событие отменено: %1$s",
- "Invitation canceled" : "Приглашение отменено",
+ "\"%1$s\" has been canceled" : "Событие «%1$s» отменено",
"Re: %1$s" : "Re: %1$s",
- "Invitation updated" : "Приглашение обновлено",
+ "%1$s has accepted your invitation" : "%1$s принял(а) ваше приглашение",
+ "%1$s has tentatively accepted your invitation" : "%1$s предварительно принял(а) ваше приглашение",
+ "%1$s has declined your invitation" : "%1$s отклонил(а) ваше приглашение",
+ "%1$s has responded to your invitation" : "%1$s ответил(а) на ваше приглашение",
+ "Invitation updated: %1$s" : "Изменение приглашения: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s изменил(а) событие «%2$s»",
"Invitation: %1$s" : "Приглашение: %1$s",
- "Invitation" : "Приглашение",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s приглашает вас принять участие в событии «%2$s»",
+ "Organizer:" : "Организатор:",
+ "Attendees:" : "Участники:",
"Title:" : "Название:",
- "Time:" : "Время:",
+ "When:" : "Когда:",
"Location:" : "Местонахождение:",
"Link:" : "Ссылка:",
- "Organizer:" : "Организатор:",
- "Attendees:" : "Участники:",
+ "Occurring:" : "Происходит:",
"Accept" : "Принять",
"Decline" : "Отклонить",
"More options …" : "Дополнительные параметры…",
"More options at %s" : "Дополнительные параметры на %s",
+ "Monday" : "Понедельник",
+ "Tuesday" : "Вторник",
+ "Wednesday" : "Среда",
+ "Thursday" : "Четверг",
+ "Friday" : "Пятница",
+ "Saturday" : "Суббота",
+ "Sunday" : "Воскресенье",
+ "January" : "Январь",
+ "February" : "Февраль",
+ "March" : "Март",
+ "April" : "Апрель",
+ "May" : "Май",
+ "June" : "Июнь",
+ "July" : "Июль",
+ "August" : "Август",
+ "September" : "Сентябрь",
+ "October" : "Октябрь",
+ "November" : "Ноябрь",
+ "December" : "Декабрь",
+ "First" : "Первый",
+ "Second" : "Второй",
+ "Third" : "Третий",
+ "Fourth" : "Четвертый",
+ "Fifth" : "Пятый",
+ "Last" : "Последний",
+ "Second Last" : "Предпоследний",
+ "Third Last" : "Третий с конца",
+ "Fourth Last" : "Четвёртый с конца",
+ "Fifth Last" : "Пятый с конца",
"Contacts" : "Контакты",
"{actor} created address book {addressbook}" : "{actor} создал(а) адресную книгу «{addressbook}»",
"You created address book {addressbook}" : "Вы создали адресную книгу «{addressbook}»",
@@ -106,35 +218,103 @@
"{actor} updated contact {card} in address book {addressbook}" : "{actor} изменил(а) запись {card} в адресной книге {addressbook}",
"You updated contact {card} in address book {addressbook}" : "Вы изменили запись {card} в адресной книге {addressbook}",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "Изменение <strong>контакта</strong> или <strong>адресной книги</strong>",
+ "Accounts" : "Учётные записи",
+ "System address book which holds all accounts" : "Системная адресная книга, в которой хранятся все учетные записи",
+ "File is not updatable: %1$s" : "Файл не подлежит обновлению: %1$s",
+ "Failed to get storage for file" : "Не удалось получить хранилище для файла",
+ "Could not write to final file, canceled by hook" : "Не удалось записать результирующий файл, запись отменена вызовом обработчика",
+ "Could not write file contents" : "Не удалось записать содержимое файла",
+ "_%n byte_::_%n bytes_" : ["%n байт","%n байта","%n байт","%n байта"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Ошибка при копировании в целевое расположение, скопировано: %1$s, ожидаемый размер файла: %2$s",
+ "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." : "Ожидаемый размер файла составляет %1$s, но из клиента приложения Nextcloud было прочитано и записано в хранилище %2$s. К этому могла привести ошибка при передаче данных на стороне отправителя либо проблема в подсистеме хранения данных на стороне сервера.",
+ "Could not rename part file to final file, canceled by hook" : "Не удалось переименовать временный файл в результирующий, операция отменена вызовом обработчика",
+ "Could not rename part file to final file" : "Не удалось переименовать временный файл в результирующий",
+ "Failed to check file size: %1$s" : "Не удалось проверить размер файла: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "Не удалось открыть файл: %1$s, файл, похоже, существует",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Не удалось открыть файл: %1$s, файл, похоже, не существует",
+ "Encryption not ready: %1$s" : "Подсистема шифрования не готова: %1$s",
+ "Failed to open file: %1$s" : "Не удалось открыть файл: %1$s",
+ "Failed to unlink: %1$s" : "Не удалось разорвать связь: %1$s",
+ "Failed to write file contents: %1$s" : "Не удалось записать содержимое файла: %1$s",
+ "File not found: %1$s" : "Файл не найден: %1$s",
+ "Invalid target path" : "Неверный целевой путь",
"System is in maintenance mode." : "Сервер находится в режиме обслуживания.",
"Upgrade needed" : "Требуется обновление",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Ваш %s должен быть настроен на использование протокола HTTPS, чтобы можно было использовать CalDAV и CardDAV на iOS/macOS.",
"Configures a CalDAV account" : "Настройка аккаунта CalDAV",
"Configures a CardDAV account" : "Настройка аккаунта CardDAV",
"Events" : "События",
- "Tasks" : "Задачи",
"Untitled task" : "Задача без названия",
"Completed on %s" : "Завершено %s",
"Due on %s by %s" : "До %s %s",
"Due on %s" : "До %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Добро пожаловать в Календарь Nextcloud!\n\nЭто пример события — исследуйте возможности планирования с Календарём Nextcloud, внося любые изменения по своему усмотрению!\n\nС помощью Календаря Nextcloud вы можете:\n- Легко создавать, редактировать и управлять событиями.\n- Создавать несколько календарей и делиться ими с коллегами, друзьями или семьёй.\n- Проверять доступность и показывать другим своё занятое время.\n- Беспрепятственно подключать приложения и устройства через CalDAV.\n- Настроить календарь под себя: повторяющиеся события, уведомления и многое другое.",
+ "Example event - open me!" : "Пример события — отройте меня!",
+ "System Address Book" : "Системная адресная книга",
+ "The system address book contains contact information for all users in your instance." : "Системная адресная книга содержит контактную информацию всех пользователей в вашем инстансе.",
+ "Enable System Address Book" : "Включить системную адресную книгу",
+ "DAV system address book" : "Системная адресная книга DAV",
+ "No outstanding DAV system address book sync." : "Синхронизация системной адресной книги DAV выполнена.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Синхронизация системной адресной книги DAV ещё не запущена, поскольку в этом экземпляре более 1000 пользователей или произошла ошибка. Перезапустите синхронизацию вручную используя команду «occ dav:sync-system-addressbook».",
+ "WebDAV endpoint" : "точка подключения WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Не удалось проверить, правильно ли настроен ваш веб-сервер, чтобы разрешить синхронизацию файлов через WebDAV. Пожалуйста, проверьте вручную.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Веб-сервер ещё не настроен должным образом для синхронизации файлов: похоже, что не работоспособен интерфейс WebDAV.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Ваш веб-сервер настроен должным образом, чтобы обеспечить синхронизацию файлов через WebDAV.",
+ "Migrated calendar (%1$s)" : "Перенос календаря (%1$s)",
+ "Calendars including events, details and attendees" : "Календари, в том числе события, подробные сведения и участники",
"Contacts and groups" : "Контакты и группы",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "точка подключения WebDAV",
- "Availability" : "Доступность",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Если вы настроите свое рабочее время, другие пользователи будут видеть, когда вас нет в офисе, когда они планируют встречу.",
+ "Absence saved" : "Состояние отсутствия сохранено",
+ "Failed to save your absence settings" : "Не удалось сохранить параметры отсутствия",
+ "Absence cleared" : "Состояние отсутствия удалено",
+ "Failed to clear your absence settings" : "Не удалось удалить параметры отсутствия",
+ "First day" : "Первый день",
+ "Last day (inclusive)" : "Последний день (включительно)",
+ "Out of office replacement (optional)" : "Замена вне офиса (по желанию)",
+ "Name of the replacement" : "Название заменяющего устройства",
+ "No results." : "Неверный целевой путь.",
+ "Start typing." : "Начать печатать.",
+ "Short absence status" : "Краткий статус отсутствия",
+ "Long absence Message" : "Длинное сообщение об отсутствии",
+ "Save" : "Сохранить",
+ "Disable absence" : "Отключить отсутствие",
+ "Failed to load availability" : "Не удалось получить сведения о доступности",
+ "Saved availability" : "Сведения о доступности сохранены",
+ "Failed to save availability" : "Не удалось сохранить сведения о доступности",
"Time zone:" : "Часовой пояс:",
"to" : "по",
"Delete slot" : "Удалить интервал",
"No working hours set" : "Рабочие часы не указаны",
"Add slot" : "Добавить интервал",
- "Monday" : "Понедельник",
- "Tuesday" : "Вторник",
- "Wednesday" : "Среда",
- "Thursday" : "Четверг",
- "Friday" : "Пятница",
- "Saturday" : "Суббота",
- "Sunday" : "Воскресенье",
- "Save" : "Сохранить",
+ "Weekdays" : "Дни недели",
+ "Pick a start time for {dayName}" : "Выберите время начала в {dayName}",
+ "Pick a end time for {dayName}" : "Выберите время окончания в {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Автоматически изменять статус на «Не беспокоить» вне интервала доступности для отключения уведомлений.",
+ "Cancel" : "Отмена",
+ "Import" : "Импортировать",
+ "Error while saving settings" : "Ошибка при сохранении параметров",
+ "Contact reset successfully" : "Контакт успешно сброшен",
+ "Error while resetting contact" : "Ошибка при сбросе контакта",
+ "Contact imported successfully" : " Контакт успешно импортирован",
+ "Error while importing contact" : " Ошибка при импорте контакта",
+ "Import contact" : "Импортировать контакт",
+ "Reset to default" : "Сбросить изменения",
+ "Import contacts" : "Импортировать контакты",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Импорт нового файла .vcf удалит существующий контакт по умолчанию и заменит его новым. Продолжить?",
+ "Failed to save example event creation setting" : "Не удалось сохранить настройки создания примера события",
+ "Failed to upload the example event" : "Не удалось загрузить пример события",
+ "Custom example event was saved successfully" : "Настраиваемый пример события успешно сохранён",
+ "Failed to delete the custom example event" : "Не удалось удалить настраиваемый пример события",
+ "Custom example event was deleted successfully" : "Настраиваемый пример события успешно удалён",
+ "Import calendar event" : "Импортировать событие календаря",
+ "Uploading a new event will overwrite the existing one." : " Загрузка нового события перезапишет существующее.",
+ "Upload event" : "Загрузить событие",
+ "Availability" : "Рабочее время",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Если вы настроите свой расписание работы, другие пользователи при планировании встреч смогут видеть, когда вас нет в офисе.",
+ "Absence" : "Отсутствие",
+ "Configure your next absence period." : "Укажите интервал вашего следующего отсутствия.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Также установите {calendarappstoreopen}приложение Calendar{linkclose}, или {calendardocopen}подключите ваш ПК и мобильное устройство для синхронизации ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Проверьте правильность настройки {emailopen}почтового сервера{linkclose}.",
"Calendar server" : "Сервер календаря",
"Send invitations to attendees" : "Отправить приглашения",
"Automatically generate a birthday calendar" : "Создавать календарь дней рождения автоматически",
@@ -142,16 +322,15 @@
"Hence they will not be available immediately after enabling but will show up after some time." : "И поэтому они станут доступны не моментально, а через некоторое время.",
"Send notifications for events" : "Отправлять уведомления о событиях",
"Notifications are sent via background jobs, so these must occur often enough." : "Уведомления будут отправляться через фоновые задания, поэтому они должны выполняться достаточно часто.",
+ "Send reminder notifications to calendar sharees as well" : "Отправлять напоминания всем пользователям, имеющим доступ к календарю",
+ "Reminders are always sent to organizers and attendees." : "Организаторам и участникам уведомления отправляются во всех случаях.",
"Enable notifications for events via push" : "Включить уведомления о событиях с помощью push",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Также установите {calendarappstoreopen}приложение Calendar{linkclose}, или {calendardocopen}подключите ваш ПК и мобильное устройство для синхронизации ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Проверьте правильность настройки {emailopen}почтового сервера{linkclose}.",
+ "Example content" : "Пример содержимого",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Пример содержимого демонстрирует возможности Nextcloud. По умолчанию поставляется стандартное содержимое, которое можно заменить настраиваемым.",
"There was an error updating your attendance status." : "Ошибка обновления статуса участия.",
"Please contact the organizer directly." : "Обратитесь к организатору напрямую.",
"Are you accepting the invitation?" : "Принять приглашение?",
"Tentative" : "Под вопросом",
- "Number of guests" : "Количество гостей",
- "Comment" : "Комментарий",
- "Your attendance was updated successfully." : "Статус участия обновлён.",
- "Calendar and tasks" : "Календарь и задачи"
+ "Your attendance was updated successfully." : "Статус участия обновлён."
},"pluralForm" :"nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);"
} \ No newline at end of file
diff --git a/apps/dav/l10n/sc.js b/apps/dav/l10n/sc.js
deleted file mode 100644
index 83484a4b54f..00000000000
--- a/apps/dav/l10n/sc.js
+++ /dev/null
@@ -1,151 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Calendàriu",
- "Todos" : "Cosas de fàghere",
- "Personal" : "Personale",
- "{actor} created calendar {calendar}" : "{actor} at creadu su calendàriu {calendar}",
- "You created calendar {calendar}" : "As creadu su calendàriu {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} at cantzelladu su calendàriu {calendar}",
- "You deleted calendar {calendar}" : "As creadu su calendàriu {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} at agiornadu su calendàriu {calendar}",
- "You updated calendar {calendar}" : "As agiornadu su calendàriu {calendar}",
- "{actor} restored calendar {calendar}" : "{actor} at ripristinadu su calendàriu {calendar}",
- "You restored calendar {calendar}" : "As ripristinadu su calendàriu {calendar}",
- "You shared calendar {calendar} as public link" : "As cumpartzidu su calendàriu {calendar} comente ligòngiu pùblicu",
- "You removed public link for calendar {calendar}" : "Ci nd'as bogadu su ligòngiu pùblicu pro su calendàriu {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} at cumpartzidu su calendàriu {calendar} cun tegus",
- "You shared calendar {calendar} with {user}" : "As cumpartzidu su calendàriu {calendar} cun {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} at cumpartzidu su calendàriu {calendar} cun {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} at annulladu sa cumpartzidura de su calendàriu {calendar} cun tegus",
- "You unshared calendar {calendar} from {user}" : "As annulladu sa cumpartzidura de su calendàriu {calendar} cun {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} at annulladu sa cumpartzidura de su calendàriu {calendar} cun {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} at annulladu sa cumpartzidura de su calendàriu {calendar} cun issu etotu",
- "You shared calendar {calendar} with group {group}" : "As cumpartzidu su calendàriu {calendar} cun su grupu {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} at cumpartzidu su calendàriu {calendar} cun su grupu {group}",
- "You unshared calendar {calendar} from group {group}" : "As annulladu su calendàriu {calendar} cun su grupu {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} at annulladu sa cumpartzidura de su calendàriu {calendar} cun su grupu {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} at creadu s'eventu {event} in su calendàriu {calendar}",
- "You created event {event} in calendar {calendar}" : "As creadu un'eventu {event} in su calendàriu {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} at cantzelladu s'eventu {event} dae su calendàriu {calendar}",
- "You deleted event {event} from calendar {calendar}" : "As cantzelladu s'eventu {event} dae su calendàriu {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} at agiornadu s'eventu {event} in su calendàriu {calendar}",
- "You updated event {event} in calendar {calendar}" : "As agiornadu s'eventu {event} in su calendàriu {calendar}",
- "{actor} restored event {event} of calendar {calendar}" : "{actor} at ripristinadu s'eventu {event} in su calendàriu {calendar}",
- "You restored event {event} of calendar {calendar}" : "As ripristinadu s'eventu {event} in su calendàriu {calendar}",
- "Busy" : "Impinnadu",
- "{actor} created todo {todo} in list {calendar}" : "{actor} at creadu sa cosa de fàghere {todo} in s'elencu {calendar}",
- "You created todo {todo} in list {calendar}" : "As creadu una cosa de fàghere {todo} in s'elencu {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} at cantzelladu sa cosa de fàghere {todo} dae s'elencu {calendar}",
- "You deleted todo {todo} from list {calendar}" : "As eliminadu sa cosa de fàghere {todo} dae s'elencu {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} at agiornadu sa cosa de fàghere {todo} in s'elencu {calendar}",
- "You updated todo {todo} in list {calendar}" : "As agiornadu sa cosa de fàghere {todo} in s'elencu {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} at isortu sa cosa de fàghere {todo} in s'elencu {calendar}",
- "You solved todo {todo} in list {calendar}" : "As isortu sa cosa de fàghere {todo} in s'elencu {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} at torradu a abèrrere sa cosa de fàghere {todo} in s'elencu {calendar}",
- "You reopened todo {todo} in list {calendar}" : "As torradu a abèrrere sa cosa de fàghere {todo} in s'elecu {calendar}",
- "Calendar, contacts and tasks" : "Calendàriu, cuntatos e fainas",
- "A <strong>calendar</strong> was modified" : "Unu <strong>calendàriu</strong> est istadu modificadu",
- "A calendar <strong>event</strong> was modified" : "Un'<strong>eventu</strong> de su calendàriu est istadu modificadu",
- "A calendar <strong>todo</strong> was modified" : "Una <strong>cosa de fàghere</strong> de su calendàriu est istada modificada",
- "Contact birthdays" : "Data de nàschida de is cuntatos",
- "Death of %s" : "Morte de %s",
- "Calendar:" : "Calendàriu:",
- "Date:" : "Data:",
- "Where:" : "Ue:",
- "Description:" : "Descritzione:",
- "Untitled event" : "Eventu sena tìtulu ",
- "_%n year_::_%n years_" : ["%n annu","%n annos"],
- "_%n month_::_%n months_" : ["%n meses","%n meses"],
- "_%n day_::_%n days_" : ["%n die","%n days"],
- "_%n hour_::_%n hours_" : ["%n ora","%n ora"],
- "_%n minute_::_%n minutes_" : ["%n minutos","%n minutos"],
- "%s (in %s)" : "%s (in %s)",
- "%s (%s ago)" : "%s (%s a immoe)",
- "Calendar: %s" : "Calendàriu: %s",
- "Date: %s" : "Data: %s",
- "Description: %s" : "Descritzione: %s",
- "Where: %s" : "Ue: %s",
- "%1$s via %2$s" : "%1$s cun %2$s",
- "Cancelled: %1$s" : "Annulladu: %1$s",
- "Invitation canceled" : "Invitu annulladu",
- "Re: %1$s" : "Re: %1$s",
- "Invitation updated" : "Invitu agiornadu",
- "Invitation: %1$s" : "Invitu: %1$s",
- "Invitation" : "Invitu",
- "Title:" : "Tìtulos:",
- "Time:" : "Tempus:",
- "Location:" : "Positzione:",
- "Link:" : "Ligòngiu:",
- "Organizer:" : "Organizadore: ",
- "Attendees:" : "Partetzipadores:",
- "Accept" : "Atzeta",
- "Decline" : "Refuda",
- "More options …" : "Àteras optziones ...",
- "More options at %s" : "Àteras optziones a is %s",
- "Contacts" : "Cuntatos",
- "{actor} created address book {addressbook}" : "{actor} at creadu sa rubrica {addressbook}",
- "You created address book {addressbook}" : "As creadu sa rubrica {addressbook}",
- "{actor} deleted address book {addressbook}" : "{actor} at cantzelladu sa rubrica {addressbook}",
- "You deleted address book {addressbook}" : "As cantzelladu sa rubrica {addressbook}",
- "{actor} updated address book {addressbook}" : "{actor} at agiornadu sa rubrica {addressbook}",
- "You updated address book {addressbook}" : "As agiornadu sa rubrica {addressbook}",
- "{actor} shared address book {addressbook} with you" : "{actor} at cumpartzidu sa rubrica {addressbook} cun tegus",
- "You shared address book {addressbook} with {user}" : "As cumpartzidu sa rubrica {addressbook} cun {user}",
- "{actor} shared address book {addressbook} with {user}" : "{actor} at cumpartzidu sa rubrica {addressbook} cun {user}",
- "{actor} unshared address book {addressbook} from you" : "{actor} at annulladu sa cumpartzidura de sa rubrica {addressbook} cun tegus",
- "You unshared address book {addressbook} from {user}" : "As annnulladu sa cumpartzidura de sa rubrica {addressbook} cun {user}",
- "{actor} unshared address book {addressbook} from {user}" : "{actor} at annulladu sa cumpartzidura de sa rubrica {addressbook} cun{user}",
- "{actor} unshared address book {addressbook} from themselves" : "{actor} at annulladu sa cumpartzidura de sa rubrica {addressbook} cun isse etotu",
- "You shared address book {addressbook} with group {group}" : "As cumpartzidu sa rubrica {addressbook} cun su grupu {group}",
- "{actor} shared address book {addressbook} with group {group}" : "{actor} at cumpartzidu sa rubrica {addressbook} cun su grupu {group}",
- "You unshared address book {addressbook} from group {group}" : "As annulladu sa cumpartzidura de sa rubrica {addressbook} cun su grupu {group}",
- "{actor} unshared address book {addressbook} from group {group}" : "{actor} at annulladu sa cumpartzidura de sa rubrica {addressbook} cun su grupu {group}",
- "{actor} created contact {card} in address book {addressbook}" : "{actor} at creadu su cuntatu {card} in sa rubrica {addressbook}",
- "You created contact {card} in address book {addressbook}" : "As creadu su cuntatu {card} in sa rubrica {addressbook}",
- "{actor} deleted contact {card} from address book {addressbook}" : "{actor} at cantzelladu su cuntatu {card} dae sa rubrica {addressbook}",
- "You deleted contact {card} from address book {addressbook}" : "As cantzelladu su cuntatu {card} dae sa rubrica {addressbook}",
- "{actor} updated contact {card} in address book {addressbook}" : "{actor} at agiornadu su cuntatu {card} in sa rubrica {addressbook}",
- "You updated contact {card} in address book {addressbook}" : "As agiornadu su cuntatu {card} in sa rubrica {addressbook}",
- "A <strong>contact</strong> or <strong>address book</strong> was modified" : "S'at modificadu unu <strong>cuntatu</strong> o <strong>rubrica</strong> ",
- "System is in maintenance mode." : "Sistema in modalidade de mantenidura.",
- "Upgrade needed" : "Tocat de agiornare",
- "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Su %s tuo tocat de lu cunfigurare pro s'impreu de HTTPS pro pòdere impreare CalDAV e CardDAV cun iOS/macOS.",
- "Configures a CalDAV account" : "Cunfigurat unu contu CalDAV",
- "Configures a CardDAV account" : "Configurat unu contu CardDAV ",
- "Events" : "Eventos",
- "Tasks" : "Fainas",
- "Untitled task" : "Faina sena tìtulu",
- "Completed on %s" : "Cumpletada su %s",
- "Due on %s by %s" : "Iscadet su %s pro su %s",
- "Due on %s" : "iscadet su %s",
- "WebDAV" : "WebDAV",
- "WebDAV endpoint" : "puntu finale WebDAV ",
- "to" : "a",
- "Monday" : "Lunis",
- "Tuesday" : "Martis",
- "Wednesday" : "Mércuris",
- "Thursday" : "Giòbia",
- "Friday" : "Chenàbura",
- "Saturday" : "Sàbudu",
- "Sunday" : "Domìnigu",
- "Save" : "Sarva",
- "Calendar server" : "Serbidore calendàriu",
- "Send invitations to attendees" : "Imbia invitos de partetzipatziones",
- "Automatically generate a birthday calendar" : "Gènera in automàticu su calendàriu de cumpleannos",
- "Birthday calendars will be generated by a background job." : "Is calendàrios de cumpleannos ant a èssere generados pro mèdiu de un'atividade dae palas.",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Perintantu non ant a èssere a diponimentu deretu a pustis de s'abilitatzione, ma s'ant a pòdere bìdere in pagos segundos.",
- "Send notifications for events" : "Imbia is notìficas de is eventos",
- "Notifications are sent via background jobs, so these must occur often enough." : "Is notìficas sunt imbiadas tràmite atividades dae palas, pro cussu custas operatziones tocat de ddas fàghere bastante a s'ispissu.",
- "Enable notifications for events via push" : "Ativa is notìficas de is eventos tràmite push",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installa puru{calendarappstoreopen}s'aplicatzione calendàriu{linkclose}, o {calendardocopen}connete s'elaboradore de iscrivania e su telefoneddu pro ddos sincronizare↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Segura·ti de àere impostadu bene {emailopen}su serbidore de posta{linkclose}.",
- "There was an error updating your attendance status." : "Ddoe at àpidu un'errore agiornende s'istadu de sa partetzipatzione tua.",
- "Please contact the organizer directly." : "Pro praghere, cuntata deretu a s'organizadore.",
- "Are you accepting the invitation?" : "Cheres atzetare s'invitu?",
- "Tentative" : "Intentu",
- "Comment" : "Cummentu",
- "Your attendance was updated successfully." : "Sa partetzipatzione tua est istada agiornada in manera curreta.",
- "Calendar and tasks" : "Calendàrios e fainas"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/sc.json b/apps/dav/l10n/sc.json
deleted file mode 100644
index 8c85b505e86..00000000000
--- a/apps/dav/l10n/sc.json
+++ /dev/null
@@ -1,149 +0,0 @@
-{ "translations": {
- "Calendar" : "Calendàriu",
- "Todos" : "Cosas de fàghere",
- "Personal" : "Personale",
- "{actor} created calendar {calendar}" : "{actor} at creadu su calendàriu {calendar}",
- "You created calendar {calendar}" : "As creadu su calendàriu {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} at cantzelladu su calendàriu {calendar}",
- "You deleted calendar {calendar}" : "As creadu su calendàriu {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} at agiornadu su calendàriu {calendar}",
- "You updated calendar {calendar}" : "As agiornadu su calendàriu {calendar}",
- "{actor} restored calendar {calendar}" : "{actor} at ripristinadu su calendàriu {calendar}",
- "You restored calendar {calendar}" : "As ripristinadu su calendàriu {calendar}",
- "You shared calendar {calendar} as public link" : "As cumpartzidu su calendàriu {calendar} comente ligòngiu pùblicu",
- "You removed public link for calendar {calendar}" : "Ci nd'as bogadu su ligòngiu pùblicu pro su calendàriu {calendar}",
- "{actor} shared calendar {calendar} with you" : "{actor} at cumpartzidu su calendàriu {calendar} cun tegus",
- "You shared calendar {calendar} with {user}" : "As cumpartzidu su calendàriu {calendar} cun {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} at cumpartzidu su calendàriu {calendar} cun {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} at annulladu sa cumpartzidura de su calendàriu {calendar} cun tegus",
- "You unshared calendar {calendar} from {user}" : "As annulladu sa cumpartzidura de su calendàriu {calendar} cun {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} at annulladu sa cumpartzidura de su calendàriu {calendar} cun {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} at annulladu sa cumpartzidura de su calendàriu {calendar} cun issu etotu",
- "You shared calendar {calendar} with group {group}" : "As cumpartzidu su calendàriu {calendar} cun su grupu {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} at cumpartzidu su calendàriu {calendar} cun su grupu {group}",
- "You unshared calendar {calendar} from group {group}" : "As annulladu su calendàriu {calendar} cun su grupu {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} at annulladu sa cumpartzidura de su calendàriu {calendar} cun su grupu {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} at creadu s'eventu {event} in su calendàriu {calendar}",
- "You created event {event} in calendar {calendar}" : "As creadu un'eventu {event} in su calendàriu {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} at cantzelladu s'eventu {event} dae su calendàriu {calendar}",
- "You deleted event {event} from calendar {calendar}" : "As cantzelladu s'eventu {event} dae su calendàriu {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} at agiornadu s'eventu {event} in su calendàriu {calendar}",
- "You updated event {event} in calendar {calendar}" : "As agiornadu s'eventu {event} in su calendàriu {calendar}",
- "{actor} restored event {event} of calendar {calendar}" : "{actor} at ripristinadu s'eventu {event} in su calendàriu {calendar}",
- "You restored event {event} of calendar {calendar}" : "As ripristinadu s'eventu {event} in su calendàriu {calendar}",
- "Busy" : "Impinnadu",
- "{actor} created todo {todo} in list {calendar}" : "{actor} at creadu sa cosa de fàghere {todo} in s'elencu {calendar}",
- "You created todo {todo} in list {calendar}" : "As creadu una cosa de fàghere {todo} in s'elencu {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} at cantzelladu sa cosa de fàghere {todo} dae s'elencu {calendar}",
- "You deleted todo {todo} from list {calendar}" : "As eliminadu sa cosa de fàghere {todo} dae s'elencu {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} at agiornadu sa cosa de fàghere {todo} in s'elencu {calendar}",
- "You updated todo {todo} in list {calendar}" : "As agiornadu sa cosa de fàghere {todo} in s'elencu {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} at isortu sa cosa de fàghere {todo} in s'elencu {calendar}",
- "You solved todo {todo} in list {calendar}" : "As isortu sa cosa de fàghere {todo} in s'elencu {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} at torradu a abèrrere sa cosa de fàghere {todo} in s'elencu {calendar}",
- "You reopened todo {todo} in list {calendar}" : "As torradu a abèrrere sa cosa de fàghere {todo} in s'elecu {calendar}",
- "Calendar, contacts and tasks" : "Calendàriu, cuntatos e fainas",
- "A <strong>calendar</strong> was modified" : "Unu <strong>calendàriu</strong> est istadu modificadu",
- "A calendar <strong>event</strong> was modified" : "Un'<strong>eventu</strong> de su calendàriu est istadu modificadu",
- "A calendar <strong>todo</strong> was modified" : "Una <strong>cosa de fàghere</strong> de su calendàriu est istada modificada",
- "Contact birthdays" : "Data de nàschida de is cuntatos",
- "Death of %s" : "Morte de %s",
- "Calendar:" : "Calendàriu:",
- "Date:" : "Data:",
- "Where:" : "Ue:",
- "Description:" : "Descritzione:",
- "Untitled event" : "Eventu sena tìtulu ",
- "_%n year_::_%n years_" : ["%n annu","%n annos"],
- "_%n month_::_%n months_" : ["%n meses","%n meses"],
- "_%n day_::_%n days_" : ["%n die","%n days"],
- "_%n hour_::_%n hours_" : ["%n ora","%n ora"],
- "_%n minute_::_%n minutes_" : ["%n minutos","%n minutos"],
- "%s (in %s)" : "%s (in %s)",
- "%s (%s ago)" : "%s (%s a immoe)",
- "Calendar: %s" : "Calendàriu: %s",
- "Date: %s" : "Data: %s",
- "Description: %s" : "Descritzione: %s",
- "Where: %s" : "Ue: %s",
- "%1$s via %2$s" : "%1$s cun %2$s",
- "Cancelled: %1$s" : "Annulladu: %1$s",
- "Invitation canceled" : "Invitu annulladu",
- "Re: %1$s" : "Re: %1$s",
- "Invitation updated" : "Invitu agiornadu",
- "Invitation: %1$s" : "Invitu: %1$s",
- "Invitation" : "Invitu",
- "Title:" : "Tìtulos:",
- "Time:" : "Tempus:",
- "Location:" : "Positzione:",
- "Link:" : "Ligòngiu:",
- "Organizer:" : "Organizadore: ",
- "Attendees:" : "Partetzipadores:",
- "Accept" : "Atzeta",
- "Decline" : "Refuda",
- "More options …" : "Àteras optziones ...",
- "More options at %s" : "Àteras optziones a is %s",
- "Contacts" : "Cuntatos",
- "{actor} created address book {addressbook}" : "{actor} at creadu sa rubrica {addressbook}",
- "You created address book {addressbook}" : "As creadu sa rubrica {addressbook}",
- "{actor} deleted address book {addressbook}" : "{actor} at cantzelladu sa rubrica {addressbook}",
- "You deleted address book {addressbook}" : "As cantzelladu sa rubrica {addressbook}",
- "{actor} updated address book {addressbook}" : "{actor} at agiornadu sa rubrica {addressbook}",
- "You updated address book {addressbook}" : "As agiornadu sa rubrica {addressbook}",
- "{actor} shared address book {addressbook} with you" : "{actor} at cumpartzidu sa rubrica {addressbook} cun tegus",
- "You shared address book {addressbook} with {user}" : "As cumpartzidu sa rubrica {addressbook} cun {user}",
- "{actor} shared address book {addressbook} with {user}" : "{actor} at cumpartzidu sa rubrica {addressbook} cun {user}",
- "{actor} unshared address book {addressbook} from you" : "{actor} at annulladu sa cumpartzidura de sa rubrica {addressbook} cun tegus",
- "You unshared address book {addressbook} from {user}" : "As annnulladu sa cumpartzidura de sa rubrica {addressbook} cun {user}",
- "{actor} unshared address book {addressbook} from {user}" : "{actor} at annulladu sa cumpartzidura de sa rubrica {addressbook} cun{user}",
- "{actor} unshared address book {addressbook} from themselves" : "{actor} at annulladu sa cumpartzidura de sa rubrica {addressbook} cun isse etotu",
- "You shared address book {addressbook} with group {group}" : "As cumpartzidu sa rubrica {addressbook} cun su grupu {group}",
- "{actor} shared address book {addressbook} with group {group}" : "{actor} at cumpartzidu sa rubrica {addressbook} cun su grupu {group}",
- "You unshared address book {addressbook} from group {group}" : "As annulladu sa cumpartzidura de sa rubrica {addressbook} cun su grupu {group}",
- "{actor} unshared address book {addressbook} from group {group}" : "{actor} at annulladu sa cumpartzidura de sa rubrica {addressbook} cun su grupu {group}",
- "{actor} created contact {card} in address book {addressbook}" : "{actor} at creadu su cuntatu {card} in sa rubrica {addressbook}",
- "You created contact {card} in address book {addressbook}" : "As creadu su cuntatu {card} in sa rubrica {addressbook}",
- "{actor} deleted contact {card} from address book {addressbook}" : "{actor} at cantzelladu su cuntatu {card} dae sa rubrica {addressbook}",
- "You deleted contact {card} from address book {addressbook}" : "As cantzelladu su cuntatu {card} dae sa rubrica {addressbook}",
- "{actor} updated contact {card} in address book {addressbook}" : "{actor} at agiornadu su cuntatu {card} in sa rubrica {addressbook}",
- "You updated contact {card} in address book {addressbook}" : "As agiornadu su cuntatu {card} in sa rubrica {addressbook}",
- "A <strong>contact</strong> or <strong>address book</strong> was modified" : "S'at modificadu unu <strong>cuntatu</strong> o <strong>rubrica</strong> ",
- "System is in maintenance mode." : "Sistema in modalidade de mantenidura.",
- "Upgrade needed" : "Tocat de agiornare",
- "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Su %s tuo tocat de lu cunfigurare pro s'impreu de HTTPS pro pòdere impreare CalDAV e CardDAV cun iOS/macOS.",
- "Configures a CalDAV account" : "Cunfigurat unu contu CalDAV",
- "Configures a CardDAV account" : "Configurat unu contu CardDAV ",
- "Events" : "Eventos",
- "Tasks" : "Fainas",
- "Untitled task" : "Faina sena tìtulu",
- "Completed on %s" : "Cumpletada su %s",
- "Due on %s by %s" : "Iscadet su %s pro su %s",
- "Due on %s" : "iscadet su %s",
- "WebDAV" : "WebDAV",
- "WebDAV endpoint" : "puntu finale WebDAV ",
- "to" : "a",
- "Monday" : "Lunis",
- "Tuesday" : "Martis",
- "Wednesday" : "Mércuris",
- "Thursday" : "Giòbia",
- "Friday" : "Chenàbura",
- "Saturday" : "Sàbudu",
- "Sunday" : "Domìnigu",
- "Save" : "Sarva",
- "Calendar server" : "Serbidore calendàriu",
- "Send invitations to attendees" : "Imbia invitos de partetzipatziones",
- "Automatically generate a birthday calendar" : "Gènera in automàticu su calendàriu de cumpleannos",
- "Birthday calendars will be generated by a background job." : "Is calendàrios de cumpleannos ant a èssere generados pro mèdiu de un'atividade dae palas.",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Perintantu non ant a èssere a diponimentu deretu a pustis de s'abilitatzione, ma s'ant a pòdere bìdere in pagos segundos.",
- "Send notifications for events" : "Imbia is notìficas de is eventos",
- "Notifications are sent via background jobs, so these must occur often enough." : "Is notìficas sunt imbiadas tràmite atividades dae palas, pro cussu custas operatziones tocat de ddas fàghere bastante a s'ispissu.",
- "Enable notifications for events via push" : "Ativa is notìficas de is eventos tràmite push",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installa puru{calendarappstoreopen}s'aplicatzione calendàriu{linkclose}, o {calendardocopen}connete s'elaboradore de iscrivania e su telefoneddu pro ddos sincronizare↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Segura·ti de àere impostadu bene {emailopen}su serbidore de posta{linkclose}.",
- "There was an error updating your attendance status." : "Ddoe at àpidu un'errore agiornende s'istadu de sa partetzipatzione tua.",
- "Please contact the organizer directly." : "Pro praghere, cuntata deretu a s'organizadore.",
- "Are you accepting the invitation?" : "Cheres atzetare s'invitu?",
- "Tentative" : "Intentu",
- "Comment" : "Cummentu",
- "Your attendance was updated successfully." : "Sa partetzipatzione tua est istada agiornada in manera curreta.",
- "Calendar and tasks" : "Calendàrios e fainas"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/sk.js b/apps/dav/l10n/sk.js
index 1ac3dad4261..3282ec3109f 100644
--- a/apps/dav/l10n/sk.js
+++ b/apps/dav/l10n/sk.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Kalendár",
- "Todos" : "Úlohy",
+ "Tasks" : "Úlohy",
"Personal" : "Osobné",
"{actor} created calendar {calendar}" : "[actor] vytvoril kalendár [calendar]",
"You created calendar {calendar}" : "Vytvorili ste kalendár {calendar}",
@@ -25,36 +25,41 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} vyzdieľal kalendár {calendar} so skupinou {group}",
"You unshared calendar {calendar} from group {group}" : "Zrušili ste zdieľanie kalendára {calendar} so skupinou {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} zrušil zdieľanie kalendára {calendar} so skupinou {group}",
+ "Untitled event" : "Udalosť bez názvu",
"{actor} created event {event} in calendar {calendar}" : "{actor} vytvoril udalosť {event} v kalendári {calendar}",
"You created event {event} in calendar {calendar}" : "Vytvorili ste udalosť {event} v kalendári {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} zmazal udalosť {event} from z kalendára {calendar}",
"You deleted event {event} from calendar {calendar}" : "Zmazali ste udalosť {event} z kalendára {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} aktualizoval udalosť {event} v kalendári {calendar}",
"You updated event {event} in calendar {calendar}" : "Aktualizovali ste udalosť {event} v kalendári {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} presunul udalosť {event} z kalendára {sourceCalendar} do kalendára {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Presunuli ste udalosť {event} z kalendára {sourceCalendar} do kalendára {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} obnovil udalosť {event} v kalendári {calendar}",
"You restored event {event} of calendar {calendar}" : "Obnovili ste udalosť {event} v kalendári {calendar}",
"Busy" : "Zaneprázdnený",
- "{actor} created todo {todo} in list {calendar}" : "{actor} vytvoril úlohu {todo} v {calendar}",
- "You created todo {todo} in list {calendar}" : "Vytvorili ste úlohu {todo} v {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} zmazal úlohu {todo} z {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Zmazali ste úlohu {todo} z {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} upravil úlohu {todo} v {calendar}",
- "You updated todo {todo} in list {calendar}" : "Upravili ste úlohu {todo} v {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} vyriešil úlohu {todo} v {calendar}",
- "You solved todo {todo} in list {calendar}" : "Vyriešili ste úlohu {todo} v {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} znovu otvoril úlohu {todo} v {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Otvorili ste znovu úlohu {todo} v {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} vytvoril úlohu {todo} v zozname {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Vytvorili ste úlohu {todo} v zozname {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} zmazal úlohu {todo} zo zoznamu {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Zmazali ste úlohu {todo} zo zoznamu {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} upravil úlohu {todo} v zozname {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Upravili ste úlohu {todo} v zozname {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} vyriešil úlohu {todo} v zozname {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Vyriešili ste úlohu {todo} v zozname {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} znovu otvoril úlohu {todo} v zozname {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Otvorili ste znovu úlohu {todo} v zozname {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} presunul udalosť {todo} zo zoznamu {sourceCalendar} do zoznamu {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Presunuli ste udalosť {todo} zo zoznamu {sourceCalendar} do zoznamu {targetCalendar}",
"Calendar, contacts and tasks" : "Kalendár, kontakty a úlohy",
- "A <strong>calendar</strong> was modified" : "<strong>kalendár</strong> bol upravený",
+ "A <strong>calendar</strong> was modified" : "<strong>Kalendár</strong> bol upravený",
"A calendar <strong>event</strong> was modified" : "<strong>Udalosť</strong> v kalendári bola upravená",
- "A calendar <strong>todo</strong> was modified" : "<strong>Pripomienka</strong> v kalendári bola upravená",
+ "A calendar <strong>to-do</strong> was modified" : "Kalendár <strong>úloh</strong> bol upravený",
"Contact birthdays" : "Narodeniny kontaktu",
"Death of %s" : "Dátum úmrtia %s",
+ "Untitled calendar" : "Kalendár bez názvu",
"Calendar:" : "Kalendár:",
"Date:" : "Dátum:",
"Where:" : "Kde:",
"Description:" : "Popis:",
- "Untitled event" : "Udalosť bez názvu",
"_%n year_::_%n years_" : ["%n rokov","%n rokov","%n rokov","%n rokov"],
"_%n month_::_%n months_" : ["%n mesiac","%n mesiace","%n mesiacov","%n mesiacov"],
"_%n day_::_%n days_" : ["%n deň","%n dni","%n dní","%n dní"],
@@ -67,22 +72,129 @@ OC.L10N.register(
"Description: %s" : "Popis: %s",
"Where: %s" : "Kde: %s",
"%1$s via %2$s" : "%1$s cez %2$s",
+ "In the past on %1$s for the entire day" : "V minulosti %1$s na celý deň",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Za %n minútu %1$s na celý deň","Za %n minúty %1$s na celý deň","Za %n minúť %1$s na celý deň","Za %n minúť %1$s na celý deň"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Za %n hodinu %1$s na celý deň","Za %n hodiny %1$s na celý deň","Za %n hodín %1$s na celý deň","Za %n hodín %1$s na celý deň"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Za %n deň %1$s na celý deň","Za %n dni %1$s na celý deň","Za %n dní %1$s na celý deň","Za %n dní %1$s na celý deň"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Za %n týždeň %1$s na celý deň","Za %n týždne %1$s na celý deň","Za %n týždňov %1$s na celý deň","Za %n týždňov %1$s na celý deň"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Za %n mesiac %1$s na celý deň","Za %n mesiace %1$s na celý deň","Za %n mesiacov %1$s na celý deň","Za %n mesiacov %1$s na celý deň"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Za %n rok %1$s na celý deň","Za %n roky %1$s na celý deň","Za %n rokov %1$s na celý deň","Za %n rokov %1$s na celý deň"],
+ "In the past on %1$s between %2$s - %3$s" : "V minulosti %1$s medzi %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Za %n minútu %1$s %2$s medzi %2$s - %3$s","Za %n minúty %1$s %2$s medzi %2$s - %3$s","Za %n minút %1$s %2$s medzi %2$s - %3$s","Za %n minút %1$s %2$s medzi %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Za %n hodinu %1$s %2$s medzi %2$s - %3$s","Za %n hodiny %1$s %2$s medzi %2$s - %3$s","Za %n hodín %1$s %2$s medzi %2$s - %3$s","Za %n hodín %1$s %2$s medzi %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Za %n deň %1$s %2$s medzi %2$s - %3$s","Za %n dni %1$s %2$s medzi %2$s - %3$s","Za %n dní %1$s %2$s medzi %2$s - %3$s","Za %n dní %1$s %2$s medzi %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Za %n týždeň %1$s %2$s medzi %2$s - %3$s","Za %n týždne %1$s %2$s medzi %2$s - %3$s","Za %n týždňov %1$s %2$s medzi %2$s - %3$s","Za %n týždňov %1$s %2$s medzi %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Za %n mesiac %1$s %2$s medzi %2$s - %3$s","Za %n mesiace %1$s %2$s medzi %2$s - %3$s","Za %n mesiacov %1$s %2$s medzi %2$s - %3$s","Za %n mesiacov %1$s %2$s medzi %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Za %n rok %1$s %2$s medzi %2$s - %3$s","Za %n roky %1$s %2$s medzi %2$s - %3$s","Za %n rokov %1$s %2$s medzi %2$s - %3$s","Za %n rokov %1$s %2$s medzi %2$s - %3$s"],
+ "Could not generate when statement" : "Nepodarilo sa vygenerovať vyhlásenie kedy",
+ "Every Day for the entire day" : "Každý deň, na celý deň",
+ "Every Day for the entire day until %1$s" : "Každý deň, na celý deň, do %1$s",
+ "Every Day between %1$s - %2$s" : "Každý deň medzi %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Každý deň medzi %1$s - %2$s do %3$s",
+ "Every %1$d Days for the entire day" : "Každých %1$d dní, na celý deň",
+ "Every %1$d Days for the entire day until %2$s" : "Každých %1$d dní, na celý deň, do %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Každých %1$d dní, medzi %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Každých %1$d dní, medzi %2$s - %3$s do %4$s",
+ "Could not generate event recurrence statement" : "Nepodarilo sa vygenerovať vyhlásenie o opakovaní udalosti",
+ "Every Week on %1$s for the entire day" : "Každý týždeň %1$s na celý deň",
+ "Every Week on %1$s for the entire day until %2$s" : "Každý týždeň %1$s na celý deň do %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Každý týždeň %1$s medzi %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Každý týždeň %1$s medzi %2$s - %3$s do %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Každých %1$d týždňov %2$s, na celý deň",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Každých %1$d týždňov od %2$s na celý deň do %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Každých %1$d týždňov od %2$s na celý deň medzi %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Každých %1$d týždňov od %2$s na celý deň medzi %3$s - %4$s do %5$s",
+ "Every Month on the %1$s for the entire day" : "Každý mesiac od %1$s na celý deň",
+ "Every Month on the %1$s for the entire day until %2$s" : "Každý mesiac od %1$s na celý deň do %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Každý mesiac od %1$s medzi %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Každý mesiac od %1$s medzi %2$s - %3$s do %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Každých %1$d mesiacov od %2$s na celý deň",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Každých %1$d mesiacov od %2$s na celý deň do %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Každých %1$d mesiacov od %2$s medzi %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Každých %1$d mesiacov od %2$s medzi %3$s - %4$s do %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Každý rok v %1$s od %2$s na celý deň",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Každý rok v %1$s od %2$s na celý deň do %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Každý rok v %1$s od %2$s medzi %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Každý rok v %1$s od %2$s medzi %3$s - %4$s do %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Každé %1$d roky v %2$s od %3$s na celý deň",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Každé %1$d roky v %2$s od %3$s na celý deň do %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Každé %1$d roky v %2$s od %3$s medzi %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Každé %1$d roky v %2$s od %3$s medzi %4$s - %5$s do %6$s",
+ "On specific dates for the entire day until %1$s" : "V konkrétnych dátumoch na celý deň %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "V konkrétnych dátumoch medzi %1$s - %2$s do %3$s",
+ "In the past on %1$s" : "V minulosti %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["O minútu %1$s","Za %n minúty %1$s","Za %n minút %1$s","Za %n minút %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Za hodinu %1$s","Za %n hodiny %1$s","Za %n hodín %1$s","Za %n hodín %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Za deň %1$s","Za %n dni %1$s","Za %n dní %1$s","Za %n dní %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Za týždeň %1$s","Za %n týždne %1$s","Za %n týždňov %1$s","Za %n týždňov %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Za mesiac %1$s","Za %n mesiace %1$s","Za %n mesiacov %1$s","Za %n mesiacov %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Za rok %1$s","Za %n roky %1$s","Za %n rokov %1$s","Za %n rokov %1$s"],
+ "In the past on %1$s then on %2$s" : "V minulosti %1$s a potom %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Za minútu %1$s a potom %2$s","Za %n minúty %1$s a potom %2$s","Za %n minút %1$s a potom %2$s","Za %n minút %1$s a potom %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Za hodinu %1$s a potom %2$s","Za %n hodiny %1$s a potom %2$s","Za %n hodín %1$s a potom %2$s","Za %n hodín %1$s a potom %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Za deň %1$s a potom %2$s","Za %n dni %1$s a potom %2$s","Za %n dní %1$s a potom %2$s","Za %n dní %1$s a potom %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Za týždeň %1$s a potom %2$s","Za %n týždne %1$s a potom %2$s","Za %n týždňov %1$s a potom %2$s","Za %n týždňov %1$s a potom %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Za mesiac %1$s a potom %2$s","Za %n mesiace %1$s a potom %2$s","Za %n mesiacov %1$s a potom %2$s","Za %n mesiacov %1$s a potom %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Za rok %1$s a potom %2$s","Za %n roky %1$s a potom %2$s","Za %n rokov %1$s a potom %2$s","Za %n rokov %1$s a potom %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "V minulosti %1$s potom %2$s a %3$s",
+ "_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_" : ["Za minútu %1$s potom %2$s a %3$s","Za %n minúty %1$s potom %2$s a %3$s","Za %n minút %1$s potom %2$s a %3$s","Za %n minút %1$s potom %2$s a %3$s"],
+ "_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_" : ["Za hodinu %1$s potom %2$s a %3$s","Za %n hodiny %1$s potom %2$s a %3$s","Za %n hodín %1$s potom %2$s a %3$s","Za %n hodín %1$s potom %2$s a %3$s"],
+ "_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_" : ["Za deň %1$s potom %2$s a %3$s","Za %n dni %1$s potom %2$s a %3$s","Za %n dní %1$s potom %2$s a %3$s","Za %n dní %1$s potom %2$s a %3$s"],
+ "_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_" : ["Za týždeň %1$s potom %2$s a %3$s","Za %n týždne %1$s potom %2$s a %3$s","Za %n týždňov %1$s potom %2$s a %3$s","Za %n týždňov %1$s potom %2$s a %3$s"],
+ "_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_" : ["Za mesiac %1$s potom %2$s a %3$s","Za %n mesiace %1$s potom %2$s a %3$s","Za %n mesiacov %1$s potom %2$s a %3$s","Za %n mesiacov %1$s potom %2$s a %3$s"],
+ "_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_" : ["Za rok %1$s potom %2$s a %3$s","Za %n roky %1$s potom %2$s a %3$s","Za %n rokov %1$s potom %2$s a %3$s","Za %n rokov %1$s potom %2$s a %3$s"],
+ "Could not generate next recurrence statement" : "Nepodarilo sa vygenerovať ďalšie opakovanie",
"Cancelled: %1$s" : "Zrušené: %1$s",
- "Invitation canceled" : "Pozvánka bola zrušená",
+ "\"%1$s\" has been canceled" : "\"%1$s\" bolo zrušené",
"Re: %1$s" : "Re: %1$s",
- "Invitation updated" : "Pozvánka bola aktualizovaná",
+ "%1$s has accepted your invitation" : "%1$s prijal/a vaše pozvanie",
+ "%1$s has tentatively accepted your invitation" : "%1$s predbežne prijal vaše pozvanie.",
+ "%1$s has declined your invitation" : "%1$s odmietol/a vaše pozvanie",
+ "%1$s has responded to your invitation" : "%1$s zareagoval/a na vašu pozvánku",
+ "Invitation updated: %1$s" : "Pozvánka aktualizovaná: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s aktualizoval udalosť \"%2$s\"",
"Invitation: %1$s" : "Pozvánka: %1$s",
- "Invitation" : "Pozvánka",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s by vás chcel/a pozvať na \"%2$s\"",
+ "Organizer:" : "Organizátor:",
+ "Attendees:" : "Účastníci:",
"Title:" : "Názov:",
- "Time:" : "Čas:",
+ "When:" : "Kedy:",
"Location:" : "Miesto:",
"Link:" : "Odkaz:",
- "Organizer:" : "Organizátor:",
- "Attendees:" : "Účastníci:",
+ "Occurring:" : "Výskyt:",
"Accept" : "Schváliť",
"Decline" : "Odmietnuť",
"More options …" : "Ďalšie nastavenia ...",
"More options at %s" : "Ďalšie nastavenia %s",
+ "Monday" : "Pondelok",
+ "Tuesday" : "Utorok",
+ "Wednesday" : "Streda",
+ "Thursday" : "Štvrtok",
+ "Friday" : "Piatok",
+ "Saturday" : "Sobota",
+ "Sunday" : "Nedeľa",
+ "January" : "Január",
+ "February" : "Február",
+ "March" : "Marec",
+ "April" : "Apríl",
+ "May" : "Máj",
+ "June" : "Jún",
+ "July" : "Júl",
+ "August" : "August",
+ "September" : "September",
+ "October" : "Október",
+ "November" : "November",
+ "December" : "December",
+ "First" : "Prvé",
+ "Second" : "Druhý",
+ "Third" : "Tretí",
+ "Fourth" : "Štvrtý",
+ "Fifth" : "Piate",
+ "Last" : "Posledné",
+ "Second Last" : "Predposledný",
+ "Third Last" : "Tretí odzadu",
+ "Fourth Last" : "Štvrtý odzadu",
+ "Fifth Last" : "Piatu odzadu",
"Contacts" : "Kontakty",
"{actor} created address book {addressbook}" : "{actor} vytvoril adresár {addressbook}",
"You created address book {addressbook}" : "Vytvorili ste adresár {addressbook}",
@@ -108,7 +220,10 @@ OC.L10N.register(
"{actor} updated contact {card} in address book {addressbook}" : "{actor} upravil kontakt {card} v adresári {addressbook}",
"You updated contact {card} in address book {addressbook}" : "Upravili ste kontakt {card} v adresári {addressbook}",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>kontakt</strong> alebo <strong>adresár</strong> bol upravený",
+ "Accounts" : "Účty",
+ "System address book which holds all accounts" : "Systémový adresár, ktorý obsahuje všetky účty.",
"File is not updatable: %1$s" : "Súbor nie je možné aktualizovať: %1$s",
+ "Failed to get storage for file" : "Nepodarilo sa získať miesto pre súbor",
"Could not write to final file, canceled by hook" : "Nepodarilo sa zapísať do konečného súboru, zrušené háčikom (hook)",
"Could not write file contents" : "Nepodarilo sa zapísať obsah súboru",
"_%n byte_::_%n bytes_" : ["%n bajt","%n bajty","%n bajtov","%n bajtov"],
@@ -117,61 +232,95 @@ OC.L10N.register(
"Could not rename part file to final file, canceled by hook" : "Nepodarilo sa premenovať dočasný súbor na finálny, zrušené háčikom (hook)",
"Could not rename part file to final file" : "Nepodarilo sa premenovať dočasný súbor na finálny.",
"Failed to check file size: %1$s" : "Kontrola veľkosti súboru zlyhala: %1$s",
- "Could not open file" : "Súbor sa nepodarilo otvoriť",
+ "Could not open file: %1$s, file does seem to exist" : "Nie je možné otvoriť súbor: %1$s, vyzerá to že súbor neexistuje",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Nie je možné otvoriť súbor: %1$s, vyzerá to že súbor neexistuje",
"Encryption not ready: %1$s" : "Šifrovanie nie je dostupné: %1$s",
"Failed to open file: %1$s" : "Otvorenie súboru zlyhalo: %1$s",
"Failed to unlink: %1$s" : "Odpojenie zlyhalo: %1$s",
- "Invalid chunk name" : "Neplatný názov bloku",
- "Could not rename part file assembled from chunks" : "Nepodarilo sa premenovať dočasný súbor vytvorený z blokov",
"Failed to write file contents: %1$s" : "Zapisovanie obsahu súboru zlyhalo: %1$s",
"File not found: %1$s" : "Súbor nebol nájdený: %1$s",
+ "Invalid target path" : "Neplatná cesta cieľa",
"System is in maintenance mode." : "Systém je v režime údržby.",
"Upgrade needed" : "Je potrebná aktualizácia",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Ak chcete používať CalDAV alebo CardDAV v iOS alebo macOS, musí byť %s nakonfigurovaný na používanie HTTPS.",
"Configures a CalDAV account" : "Nakonfiguruje účet CalDAV",
"Configures a CardDAV account" : "Nakonfiguruje účet CardDAV",
"Events" : "Udalosti",
- "Tasks" : "Úlohy",
"Untitled task" : "Úloha bez názvu",
"Completed on %s" : "Dokončené %s",
"Due on %s by %s" : "Termín od %s do %s",
"Due on %s" : "Termín do %s",
+ "System Address Book" : "Systémový Adresár",
+ "The system address book contains contact information for all users in your instance." : "Systémový adresár obsahuje kontaktné informácie o všetkých užívateľov vo vašej inštancii.",
+ "Enable System Address Book" : "Povoliť systémový Adresár",
+ "DAV system address book" : "Systémový DAV adresár",
+ "No outstanding DAV system address book sync." : "Žiadna zostávajúca synchronizácia adresára systému DAV.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV synchronizácia systémového adresára ešte nebola spustená, pretože vaša inštancia má viac ako 1000 užívateľov alebo sa vyskytla chyba. Prosím, spustite ju manuálne volaním \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "Koncový bod WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Nepodarilo sa skontrolovať, či je váš webový server správne nastavený tak, aby umožňoval synchronizáciu súborov cez WebDAV. Skontrolujte prosím manuálne.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Váš webový server nie je zatiaľ správne nastavený, aby umožnil synchronizáciu súborov, pretože rozhranie WebDAV sa zdá byť nefunkčné.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Váš webový server je správne nastavený tak, aby umožňoval synchronizáciu súborov cez WebDAV.",
"Migrated calendar (%1$s)" : "Migrovaný kalendár (%1$s)",
+ "Calendars including events, details and attendees" : "Kalendáre vrátane udalostí, podrobností a účastníkov",
"Contacts and groups" : "Kontakty a skupiny",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "Koncový bod WebDAV",
- "Availability" : "Dostupnosť",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Ak nakonfigurujete svoj pracovný čas, ostatní používatelia uvidia, keď si rezervujete schôdzku, keď nebudete v práci.",
+ "Absence saved" : "Neprítomnosť uložená",
+ "Failed to save your absence settings" : "Nepodarilo sa uložiť vaše nastavenia neprítomnosti.",
+ "Absence cleared" : "Neprítomnosť odstránená",
+ "Failed to clear your absence settings" : "Nepodarilo sa vymazať vaše nastavenia neprítomnosti.",
+ "First day" : "Prvý deň",
+ "Last day (inclusive)" : "Posledný deň (vrátane)",
+ "Out of office replacement (optional)" : "Zástup keď je mimo kancelárie (voliteľné)",
+ "Name of the replacement" : "Názov náhrady",
+ "No results." : "Žiadne výsledky.",
+ "Start typing." : "Začnite písať.",
+ "Short absence status" : "Status pre Krátku neprítomnosť",
+ "Long absence Message" : "Správa pri Dlhej neprítomnosti",
+ "Save" : "Uložiť",
+ "Disable absence" : "Zakázať neprítomnosť",
+ "Failed to load availability" : "Nepodarilo sa načítať dostupnosť",
+ "Saved availability" : "Dostupnosť bola uložená",
+ "Failed to save availability" : "Nepodarilo sa uložiť dostupnosť",
"Time zone:" : "Časová zóna:",
"to" : "do",
"Delete slot" : "Odstrániť slot",
"No working hours set" : "Nenastavená pracovná doba",
"Add slot" : "Pridať slot",
- "Monday" : "Pondelok",
- "Tuesday" : "Utorok",
- "Wednesday" : "Streda",
- "Thursday" : "Štvrtok",
- "Friday" : "Piatok",
- "Saturday" : "Sobota",
- "Sunday" : "Nedeľa",
- "Save" : "Uložiť",
+ "Weekdays" : "Pracovné dni",
+ "Pick a start time for {dayName}" : "Vyberte začiatočný čas pre {dayName}",
+ "Pick a end time for {dayName}" : "Vyberte koncový čas pre {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Automaticky nastaviť stav používateľa na „Nerušiť“ ak nie ste dostupný, pre stlmenie všetkých upozornení.",
+ "Cancel" : "Zrušiť",
+ "Import" : "Import",
+ "Error while saving settings" : "Chyba pri ukladaní nastavení",
+ "Contact reset successfully" : "Kontakt bol úspešne resetovaný",
+ "Error while resetting contact" : "Chyba počas resetovania kontaktu ",
+ "Contact imported successfully" : "Kontakt bol úspešne importovaný",
+ "Error while importing contact" : "Chyba pri importovaní kontaktu",
+ "Import contact" : "Importovať kontakt",
+ "Reset to default" : "Nastaviť predvolené",
+ "Import contacts" : "Importovať kontakty",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Importovaním nového súboru .vcf sa vymaže existujúci predvolený kontakt a nahradí sa novým. Chcete pokračovať?",
+ "Availability" : "Dostupnosť",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Ak nakonfigurujete svoj pracovný čas, ostatní užívatelia vás uvidia ako neprítomného, keď si rezervujete schôdzku",
+ "Absence" : "Neprítomnosť",
+ "Configure your next absence period." : "Nastavte si ďalšie obdobie svojej neprítomnosti.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Tiež nainštalujte {calendarappstoreopen}apku Kalendár{linkclose} alebo {calendardocopen}pripojte svoj počítač a smartfón pre synchronizáciu ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Uistite sa, že ste správne nastavili {emailopen}e-mailový server{linkclose}.",
"Calendar server" : "Kalendárový server",
"Send invitations to attendees" : "Odoslanie pozvánok účastníkom",
"Automatically generate a birthday calendar" : "Automaticky generovať narodeninový kalendár",
"Birthday calendars will be generated by a background job." : "Narodeninové kalendáre budú generované úlohou na pozadí.",
"Hence they will not be available immediately after enabling but will show up after some time." : "Preto nebudú dostupné hneď po povolení, ale zobrazia sa po určitom čase",
- "Send notifications for events" : "Zaslať upozornenia na udalosti",
+ "Send notifications for events" : "Zasielať upozornenia na udalosti",
"Notifications are sent via background jobs, so these must occur often enough." : "Upozornenia sa odosielajú prostredníctvom úloh na pozadí - preto je potrebné, aby tieto prebiehali dostatočne často.",
+ "Send reminder notifications to calendar sharees as well" : "Posielať upozornenia na pripomienky aj zdieľaným osobám v kalendári",
+ "Reminders are always sent to organizers and attendees." : "Upozornenia sa vždy posielajú organizátorom a účastníkom.",
"Enable notifications for events via push" : "Zapnúť oznámenia o udalostiach prostredníctvom technológie push.",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Tiež nainštalujte {calendarappstoreopen}apku Kalendár{linkclose} alebo {calendardocopen}pripojte svoj počítač a smartfón pre synchronizáciu ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Uistite sa, že ste správne nastavili {emailopen}e-mailový server{linkclose}.",
"There was an error updating your attendance status." : "Nastal problém pri aktualizácii Vašej účasti.",
"Please contact the organizer directly." : "Prosím kontaktujte priamo organizátora.",
"Are you accepting the invitation?" : "Príjmate pozvánku?",
"Tentative" : "Neistý",
- "Number of guests" : "Počet návštevníkov",
- "Comment" : "Komentár",
- "Your attendance was updated successfully." : "Vaša účasť bola aktualizovaná úspešne.",
- "Calendar and tasks" : "Kalendár a úlohy"
+ "Your attendance was updated successfully." : "Vaša účasť bola aktualizovaná úspešne."
},
"nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n >= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);");
diff --git a/apps/dav/l10n/sk.json b/apps/dav/l10n/sk.json
index 97bffe30e65..a0da2d19cd3 100644
--- a/apps/dav/l10n/sk.json
+++ b/apps/dav/l10n/sk.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Kalendár",
- "Todos" : "Úlohy",
+ "Tasks" : "Úlohy",
"Personal" : "Osobné",
"{actor} created calendar {calendar}" : "[actor] vytvoril kalendár [calendar]",
"You created calendar {calendar}" : "Vytvorili ste kalendár {calendar}",
@@ -23,36 +23,41 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} vyzdieľal kalendár {calendar} so skupinou {group}",
"You unshared calendar {calendar} from group {group}" : "Zrušili ste zdieľanie kalendára {calendar} so skupinou {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} zrušil zdieľanie kalendára {calendar} so skupinou {group}",
+ "Untitled event" : "Udalosť bez názvu",
"{actor} created event {event} in calendar {calendar}" : "{actor} vytvoril udalosť {event} v kalendári {calendar}",
"You created event {event} in calendar {calendar}" : "Vytvorili ste udalosť {event} v kalendári {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} zmazal udalosť {event} from z kalendára {calendar}",
"You deleted event {event} from calendar {calendar}" : "Zmazali ste udalosť {event} z kalendára {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} aktualizoval udalosť {event} v kalendári {calendar}",
"You updated event {event} in calendar {calendar}" : "Aktualizovali ste udalosť {event} v kalendári {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} presunul udalosť {event} z kalendára {sourceCalendar} do kalendára {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Presunuli ste udalosť {event} z kalendára {sourceCalendar} do kalendára {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} obnovil udalosť {event} v kalendári {calendar}",
"You restored event {event} of calendar {calendar}" : "Obnovili ste udalosť {event} v kalendári {calendar}",
"Busy" : "Zaneprázdnený",
- "{actor} created todo {todo} in list {calendar}" : "{actor} vytvoril úlohu {todo} v {calendar}",
- "You created todo {todo} in list {calendar}" : "Vytvorili ste úlohu {todo} v {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} zmazal úlohu {todo} z {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Zmazali ste úlohu {todo} z {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} upravil úlohu {todo} v {calendar}",
- "You updated todo {todo} in list {calendar}" : "Upravili ste úlohu {todo} v {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} vyriešil úlohu {todo} v {calendar}",
- "You solved todo {todo} in list {calendar}" : "Vyriešili ste úlohu {todo} v {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} znovu otvoril úlohu {todo} v {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Otvorili ste znovu úlohu {todo} v {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} vytvoril úlohu {todo} v zozname {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Vytvorili ste úlohu {todo} v zozname {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} zmazal úlohu {todo} zo zoznamu {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Zmazali ste úlohu {todo} zo zoznamu {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} upravil úlohu {todo} v zozname {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Upravili ste úlohu {todo} v zozname {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} vyriešil úlohu {todo} v zozname {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Vyriešili ste úlohu {todo} v zozname {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} znovu otvoril úlohu {todo} v zozname {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Otvorili ste znovu úlohu {todo} v zozname {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} presunul udalosť {todo} zo zoznamu {sourceCalendar} do zoznamu {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Presunuli ste udalosť {todo} zo zoznamu {sourceCalendar} do zoznamu {targetCalendar}",
"Calendar, contacts and tasks" : "Kalendár, kontakty a úlohy",
- "A <strong>calendar</strong> was modified" : "<strong>kalendár</strong> bol upravený",
+ "A <strong>calendar</strong> was modified" : "<strong>Kalendár</strong> bol upravený",
"A calendar <strong>event</strong> was modified" : "<strong>Udalosť</strong> v kalendári bola upravená",
- "A calendar <strong>todo</strong> was modified" : "<strong>Pripomienka</strong> v kalendári bola upravená",
+ "A calendar <strong>to-do</strong> was modified" : "Kalendár <strong>úloh</strong> bol upravený",
"Contact birthdays" : "Narodeniny kontaktu",
"Death of %s" : "Dátum úmrtia %s",
+ "Untitled calendar" : "Kalendár bez názvu",
"Calendar:" : "Kalendár:",
"Date:" : "Dátum:",
"Where:" : "Kde:",
"Description:" : "Popis:",
- "Untitled event" : "Udalosť bez názvu",
"_%n year_::_%n years_" : ["%n rokov","%n rokov","%n rokov","%n rokov"],
"_%n month_::_%n months_" : ["%n mesiac","%n mesiace","%n mesiacov","%n mesiacov"],
"_%n day_::_%n days_" : ["%n deň","%n dni","%n dní","%n dní"],
@@ -65,22 +70,129 @@
"Description: %s" : "Popis: %s",
"Where: %s" : "Kde: %s",
"%1$s via %2$s" : "%1$s cez %2$s",
+ "In the past on %1$s for the entire day" : "V minulosti %1$s na celý deň",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Za %n minútu %1$s na celý deň","Za %n minúty %1$s na celý deň","Za %n minúť %1$s na celý deň","Za %n minúť %1$s na celý deň"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Za %n hodinu %1$s na celý deň","Za %n hodiny %1$s na celý deň","Za %n hodín %1$s na celý deň","Za %n hodín %1$s na celý deň"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Za %n deň %1$s na celý deň","Za %n dni %1$s na celý deň","Za %n dní %1$s na celý deň","Za %n dní %1$s na celý deň"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Za %n týždeň %1$s na celý deň","Za %n týždne %1$s na celý deň","Za %n týždňov %1$s na celý deň","Za %n týždňov %1$s na celý deň"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Za %n mesiac %1$s na celý deň","Za %n mesiace %1$s na celý deň","Za %n mesiacov %1$s na celý deň","Za %n mesiacov %1$s na celý deň"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Za %n rok %1$s na celý deň","Za %n roky %1$s na celý deň","Za %n rokov %1$s na celý deň","Za %n rokov %1$s na celý deň"],
+ "In the past on %1$s between %2$s - %3$s" : "V minulosti %1$s medzi %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Za %n minútu %1$s %2$s medzi %2$s - %3$s","Za %n minúty %1$s %2$s medzi %2$s - %3$s","Za %n minút %1$s %2$s medzi %2$s - %3$s","Za %n minút %1$s %2$s medzi %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Za %n hodinu %1$s %2$s medzi %2$s - %3$s","Za %n hodiny %1$s %2$s medzi %2$s - %3$s","Za %n hodín %1$s %2$s medzi %2$s - %3$s","Za %n hodín %1$s %2$s medzi %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Za %n deň %1$s %2$s medzi %2$s - %3$s","Za %n dni %1$s %2$s medzi %2$s - %3$s","Za %n dní %1$s %2$s medzi %2$s - %3$s","Za %n dní %1$s %2$s medzi %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Za %n týždeň %1$s %2$s medzi %2$s - %3$s","Za %n týždne %1$s %2$s medzi %2$s - %3$s","Za %n týždňov %1$s %2$s medzi %2$s - %3$s","Za %n týždňov %1$s %2$s medzi %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Za %n mesiac %1$s %2$s medzi %2$s - %3$s","Za %n mesiace %1$s %2$s medzi %2$s - %3$s","Za %n mesiacov %1$s %2$s medzi %2$s - %3$s","Za %n mesiacov %1$s %2$s medzi %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Za %n rok %1$s %2$s medzi %2$s - %3$s","Za %n roky %1$s %2$s medzi %2$s - %3$s","Za %n rokov %1$s %2$s medzi %2$s - %3$s","Za %n rokov %1$s %2$s medzi %2$s - %3$s"],
+ "Could not generate when statement" : "Nepodarilo sa vygenerovať vyhlásenie kedy",
+ "Every Day for the entire day" : "Každý deň, na celý deň",
+ "Every Day for the entire day until %1$s" : "Každý deň, na celý deň, do %1$s",
+ "Every Day between %1$s - %2$s" : "Každý deň medzi %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Každý deň medzi %1$s - %2$s do %3$s",
+ "Every %1$d Days for the entire day" : "Každých %1$d dní, na celý deň",
+ "Every %1$d Days for the entire day until %2$s" : "Každých %1$d dní, na celý deň, do %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Každých %1$d dní, medzi %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Každých %1$d dní, medzi %2$s - %3$s do %4$s",
+ "Could not generate event recurrence statement" : "Nepodarilo sa vygenerovať vyhlásenie o opakovaní udalosti",
+ "Every Week on %1$s for the entire day" : "Každý týždeň %1$s na celý deň",
+ "Every Week on %1$s for the entire day until %2$s" : "Každý týždeň %1$s na celý deň do %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Každý týždeň %1$s medzi %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Každý týždeň %1$s medzi %2$s - %3$s do %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Každých %1$d týždňov %2$s, na celý deň",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Každých %1$d týždňov od %2$s na celý deň do %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Každých %1$d týždňov od %2$s na celý deň medzi %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Každých %1$d týždňov od %2$s na celý deň medzi %3$s - %4$s do %5$s",
+ "Every Month on the %1$s for the entire day" : "Každý mesiac od %1$s na celý deň",
+ "Every Month on the %1$s for the entire day until %2$s" : "Každý mesiac od %1$s na celý deň do %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Každý mesiac od %1$s medzi %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Každý mesiac od %1$s medzi %2$s - %3$s do %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Každých %1$d mesiacov od %2$s na celý deň",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Každých %1$d mesiacov od %2$s na celý deň do %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Každých %1$d mesiacov od %2$s medzi %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Každých %1$d mesiacov od %2$s medzi %3$s - %4$s do %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Každý rok v %1$s od %2$s na celý deň",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Každý rok v %1$s od %2$s na celý deň do %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Každý rok v %1$s od %2$s medzi %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Každý rok v %1$s od %2$s medzi %3$s - %4$s do %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Každé %1$d roky v %2$s od %3$s na celý deň",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Každé %1$d roky v %2$s od %3$s na celý deň do %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Každé %1$d roky v %2$s od %3$s medzi %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Každé %1$d roky v %2$s od %3$s medzi %4$s - %5$s do %6$s",
+ "On specific dates for the entire day until %1$s" : "V konkrétnych dátumoch na celý deň %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "V konkrétnych dátumoch medzi %1$s - %2$s do %3$s",
+ "In the past on %1$s" : "V minulosti %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["O minútu %1$s","Za %n minúty %1$s","Za %n minút %1$s","Za %n minút %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Za hodinu %1$s","Za %n hodiny %1$s","Za %n hodín %1$s","Za %n hodín %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Za deň %1$s","Za %n dni %1$s","Za %n dní %1$s","Za %n dní %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Za týždeň %1$s","Za %n týždne %1$s","Za %n týždňov %1$s","Za %n týždňov %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Za mesiac %1$s","Za %n mesiace %1$s","Za %n mesiacov %1$s","Za %n mesiacov %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Za rok %1$s","Za %n roky %1$s","Za %n rokov %1$s","Za %n rokov %1$s"],
+ "In the past on %1$s then on %2$s" : "V minulosti %1$s a potom %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Za minútu %1$s a potom %2$s","Za %n minúty %1$s a potom %2$s","Za %n minút %1$s a potom %2$s","Za %n minút %1$s a potom %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Za hodinu %1$s a potom %2$s","Za %n hodiny %1$s a potom %2$s","Za %n hodín %1$s a potom %2$s","Za %n hodín %1$s a potom %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Za deň %1$s a potom %2$s","Za %n dni %1$s a potom %2$s","Za %n dní %1$s a potom %2$s","Za %n dní %1$s a potom %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Za týždeň %1$s a potom %2$s","Za %n týždne %1$s a potom %2$s","Za %n týždňov %1$s a potom %2$s","Za %n týždňov %1$s a potom %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Za mesiac %1$s a potom %2$s","Za %n mesiace %1$s a potom %2$s","Za %n mesiacov %1$s a potom %2$s","Za %n mesiacov %1$s a potom %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Za rok %1$s a potom %2$s","Za %n roky %1$s a potom %2$s","Za %n rokov %1$s a potom %2$s","Za %n rokov %1$s a potom %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "V minulosti %1$s potom %2$s a %3$s",
+ "_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_" : ["Za minútu %1$s potom %2$s a %3$s","Za %n minúty %1$s potom %2$s a %3$s","Za %n minút %1$s potom %2$s a %3$s","Za %n minút %1$s potom %2$s a %3$s"],
+ "_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_" : ["Za hodinu %1$s potom %2$s a %3$s","Za %n hodiny %1$s potom %2$s a %3$s","Za %n hodín %1$s potom %2$s a %3$s","Za %n hodín %1$s potom %2$s a %3$s"],
+ "_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_" : ["Za deň %1$s potom %2$s a %3$s","Za %n dni %1$s potom %2$s a %3$s","Za %n dní %1$s potom %2$s a %3$s","Za %n dní %1$s potom %2$s a %3$s"],
+ "_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_" : ["Za týždeň %1$s potom %2$s a %3$s","Za %n týždne %1$s potom %2$s a %3$s","Za %n týždňov %1$s potom %2$s a %3$s","Za %n týždňov %1$s potom %2$s a %3$s"],
+ "_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_" : ["Za mesiac %1$s potom %2$s a %3$s","Za %n mesiace %1$s potom %2$s a %3$s","Za %n mesiacov %1$s potom %2$s a %3$s","Za %n mesiacov %1$s potom %2$s a %3$s"],
+ "_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_" : ["Za rok %1$s potom %2$s a %3$s","Za %n roky %1$s potom %2$s a %3$s","Za %n rokov %1$s potom %2$s a %3$s","Za %n rokov %1$s potom %2$s a %3$s"],
+ "Could not generate next recurrence statement" : "Nepodarilo sa vygenerovať ďalšie opakovanie",
"Cancelled: %1$s" : "Zrušené: %1$s",
- "Invitation canceled" : "Pozvánka bola zrušená",
+ "\"%1$s\" has been canceled" : "\"%1$s\" bolo zrušené",
"Re: %1$s" : "Re: %1$s",
- "Invitation updated" : "Pozvánka bola aktualizovaná",
+ "%1$s has accepted your invitation" : "%1$s prijal/a vaše pozvanie",
+ "%1$s has tentatively accepted your invitation" : "%1$s predbežne prijal vaše pozvanie.",
+ "%1$s has declined your invitation" : "%1$s odmietol/a vaše pozvanie",
+ "%1$s has responded to your invitation" : "%1$s zareagoval/a na vašu pozvánku",
+ "Invitation updated: %1$s" : "Pozvánka aktualizovaná: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s aktualizoval udalosť \"%2$s\"",
"Invitation: %1$s" : "Pozvánka: %1$s",
- "Invitation" : "Pozvánka",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s by vás chcel/a pozvať na \"%2$s\"",
+ "Organizer:" : "Organizátor:",
+ "Attendees:" : "Účastníci:",
"Title:" : "Názov:",
- "Time:" : "Čas:",
+ "When:" : "Kedy:",
"Location:" : "Miesto:",
"Link:" : "Odkaz:",
- "Organizer:" : "Organizátor:",
- "Attendees:" : "Účastníci:",
+ "Occurring:" : "Výskyt:",
"Accept" : "Schváliť",
"Decline" : "Odmietnuť",
"More options …" : "Ďalšie nastavenia ...",
"More options at %s" : "Ďalšie nastavenia %s",
+ "Monday" : "Pondelok",
+ "Tuesday" : "Utorok",
+ "Wednesday" : "Streda",
+ "Thursday" : "Štvrtok",
+ "Friday" : "Piatok",
+ "Saturday" : "Sobota",
+ "Sunday" : "Nedeľa",
+ "January" : "Január",
+ "February" : "Február",
+ "March" : "Marec",
+ "April" : "Apríl",
+ "May" : "Máj",
+ "June" : "Jún",
+ "July" : "Júl",
+ "August" : "August",
+ "September" : "September",
+ "October" : "Október",
+ "November" : "November",
+ "December" : "December",
+ "First" : "Prvé",
+ "Second" : "Druhý",
+ "Third" : "Tretí",
+ "Fourth" : "Štvrtý",
+ "Fifth" : "Piate",
+ "Last" : "Posledné",
+ "Second Last" : "Predposledný",
+ "Third Last" : "Tretí odzadu",
+ "Fourth Last" : "Štvrtý odzadu",
+ "Fifth Last" : "Piatu odzadu",
"Contacts" : "Kontakty",
"{actor} created address book {addressbook}" : "{actor} vytvoril adresár {addressbook}",
"You created address book {addressbook}" : "Vytvorili ste adresár {addressbook}",
@@ -106,7 +218,10 @@
"{actor} updated contact {card} in address book {addressbook}" : "{actor} upravil kontakt {card} v adresári {addressbook}",
"You updated contact {card} in address book {addressbook}" : "Upravili ste kontakt {card} v adresári {addressbook}",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>kontakt</strong> alebo <strong>adresár</strong> bol upravený",
+ "Accounts" : "Účty",
+ "System address book which holds all accounts" : "Systémový adresár, ktorý obsahuje všetky účty.",
"File is not updatable: %1$s" : "Súbor nie je možné aktualizovať: %1$s",
+ "Failed to get storage for file" : "Nepodarilo sa získať miesto pre súbor",
"Could not write to final file, canceled by hook" : "Nepodarilo sa zapísať do konečného súboru, zrušené háčikom (hook)",
"Could not write file contents" : "Nepodarilo sa zapísať obsah súboru",
"_%n byte_::_%n bytes_" : ["%n bajt","%n bajty","%n bajtov","%n bajtov"],
@@ -115,61 +230,95 @@
"Could not rename part file to final file, canceled by hook" : "Nepodarilo sa premenovať dočasný súbor na finálny, zrušené háčikom (hook)",
"Could not rename part file to final file" : "Nepodarilo sa premenovať dočasný súbor na finálny.",
"Failed to check file size: %1$s" : "Kontrola veľkosti súboru zlyhala: %1$s",
- "Could not open file" : "Súbor sa nepodarilo otvoriť",
+ "Could not open file: %1$s, file does seem to exist" : "Nie je možné otvoriť súbor: %1$s, vyzerá to že súbor neexistuje",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Nie je možné otvoriť súbor: %1$s, vyzerá to že súbor neexistuje",
"Encryption not ready: %1$s" : "Šifrovanie nie je dostupné: %1$s",
"Failed to open file: %1$s" : "Otvorenie súboru zlyhalo: %1$s",
"Failed to unlink: %1$s" : "Odpojenie zlyhalo: %1$s",
- "Invalid chunk name" : "Neplatný názov bloku",
- "Could not rename part file assembled from chunks" : "Nepodarilo sa premenovať dočasný súbor vytvorený z blokov",
"Failed to write file contents: %1$s" : "Zapisovanie obsahu súboru zlyhalo: %1$s",
"File not found: %1$s" : "Súbor nebol nájdený: %1$s",
+ "Invalid target path" : "Neplatná cesta cieľa",
"System is in maintenance mode." : "Systém je v režime údržby.",
"Upgrade needed" : "Je potrebná aktualizácia",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Ak chcete používať CalDAV alebo CardDAV v iOS alebo macOS, musí byť %s nakonfigurovaný na používanie HTTPS.",
"Configures a CalDAV account" : "Nakonfiguruje účet CalDAV",
"Configures a CardDAV account" : "Nakonfiguruje účet CardDAV",
"Events" : "Udalosti",
- "Tasks" : "Úlohy",
"Untitled task" : "Úloha bez názvu",
"Completed on %s" : "Dokončené %s",
"Due on %s by %s" : "Termín od %s do %s",
"Due on %s" : "Termín do %s",
+ "System Address Book" : "Systémový Adresár",
+ "The system address book contains contact information for all users in your instance." : "Systémový adresár obsahuje kontaktné informácie o všetkých užívateľov vo vašej inštancii.",
+ "Enable System Address Book" : "Povoliť systémový Adresár",
+ "DAV system address book" : "Systémový DAV adresár",
+ "No outstanding DAV system address book sync." : "Žiadna zostávajúca synchronizácia adresára systému DAV.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV synchronizácia systémového adresára ešte nebola spustená, pretože vaša inštancia má viac ako 1000 užívateľov alebo sa vyskytla chyba. Prosím, spustite ju manuálne volaním \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "Koncový bod WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Nepodarilo sa skontrolovať, či je váš webový server správne nastavený tak, aby umožňoval synchronizáciu súborov cez WebDAV. Skontrolujte prosím manuálne.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Váš webový server nie je zatiaľ správne nastavený, aby umožnil synchronizáciu súborov, pretože rozhranie WebDAV sa zdá byť nefunkčné.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Váš webový server je správne nastavený tak, aby umožňoval synchronizáciu súborov cez WebDAV.",
"Migrated calendar (%1$s)" : "Migrovaný kalendár (%1$s)",
+ "Calendars including events, details and attendees" : "Kalendáre vrátane udalostí, podrobností a účastníkov",
"Contacts and groups" : "Kontakty a skupiny",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "Koncový bod WebDAV",
- "Availability" : "Dostupnosť",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Ak nakonfigurujete svoj pracovný čas, ostatní používatelia uvidia, keď si rezervujete schôdzku, keď nebudete v práci.",
+ "Absence saved" : "Neprítomnosť uložená",
+ "Failed to save your absence settings" : "Nepodarilo sa uložiť vaše nastavenia neprítomnosti.",
+ "Absence cleared" : "Neprítomnosť odstránená",
+ "Failed to clear your absence settings" : "Nepodarilo sa vymazať vaše nastavenia neprítomnosti.",
+ "First day" : "Prvý deň",
+ "Last day (inclusive)" : "Posledný deň (vrátane)",
+ "Out of office replacement (optional)" : "Zástup keď je mimo kancelárie (voliteľné)",
+ "Name of the replacement" : "Názov náhrady",
+ "No results." : "Žiadne výsledky.",
+ "Start typing." : "Začnite písať.",
+ "Short absence status" : "Status pre Krátku neprítomnosť",
+ "Long absence Message" : "Správa pri Dlhej neprítomnosti",
+ "Save" : "Uložiť",
+ "Disable absence" : "Zakázať neprítomnosť",
+ "Failed to load availability" : "Nepodarilo sa načítať dostupnosť",
+ "Saved availability" : "Dostupnosť bola uložená",
+ "Failed to save availability" : "Nepodarilo sa uložiť dostupnosť",
"Time zone:" : "Časová zóna:",
"to" : "do",
"Delete slot" : "Odstrániť slot",
"No working hours set" : "Nenastavená pracovná doba",
"Add slot" : "Pridať slot",
- "Monday" : "Pondelok",
- "Tuesday" : "Utorok",
- "Wednesday" : "Streda",
- "Thursday" : "Štvrtok",
- "Friday" : "Piatok",
- "Saturday" : "Sobota",
- "Sunday" : "Nedeľa",
- "Save" : "Uložiť",
+ "Weekdays" : "Pracovné dni",
+ "Pick a start time for {dayName}" : "Vyberte začiatočný čas pre {dayName}",
+ "Pick a end time for {dayName}" : "Vyberte koncový čas pre {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Automaticky nastaviť stav používateľa na „Nerušiť“ ak nie ste dostupný, pre stlmenie všetkých upozornení.",
+ "Cancel" : "Zrušiť",
+ "Import" : "Import",
+ "Error while saving settings" : "Chyba pri ukladaní nastavení",
+ "Contact reset successfully" : "Kontakt bol úspešne resetovaný",
+ "Error while resetting contact" : "Chyba počas resetovania kontaktu ",
+ "Contact imported successfully" : "Kontakt bol úspešne importovaný",
+ "Error while importing contact" : "Chyba pri importovaní kontaktu",
+ "Import contact" : "Importovať kontakt",
+ "Reset to default" : "Nastaviť predvolené",
+ "Import contacts" : "Importovať kontakty",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Importovaním nového súboru .vcf sa vymaže existujúci predvolený kontakt a nahradí sa novým. Chcete pokračovať?",
+ "Availability" : "Dostupnosť",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Ak nakonfigurujete svoj pracovný čas, ostatní užívatelia vás uvidia ako neprítomného, keď si rezervujete schôdzku",
+ "Absence" : "Neprítomnosť",
+ "Configure your next absence period." : "Nastavte si ďalšie obdobie svojej neprítomnosti.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Tiež nainštalujte {calendarappstoreopen}apku Kalendár{linkclose} alebo {calendardocopen}pripojte svoj počítač a smartfón pre synchronizáciu ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Uistite sa, že ste správne nastavili {emailopen}e-mailový server{linkclose}.",
"Calendar server" : "Kalendárový server",
"Send invitations to attendees" : "Odoslanie pozvánok účastníkom",
"Automatically generate a birthday calendar" : "Automaticky generovať narodeninový kalendár",
"Birthday calendars will be generated by a background job." : "Narodeninové kalendáre budú generované úlohou na pozadí.",
"Hence they will not be available immediately after enabling but will show up after some time." : "Preto nebudú dostupné hneď po povolení, ale zobrazia sa po určitom čase",
- "Send notifications for events" : "Zaslať upozornenia na udalosti",
+ "Send notifications for events" : "Zasielať upozornenia na udalosti",
"Notifications are sent via background jobs, so these must occur often enough." : "Upozornenia sa odosielajú prostredníctvom úloh na pozadí - preto je potrebné, aby tieto prebiehali dostatočne často.",
+ "Send reminder notifications to calendar sharees as well" : "Posielať upozornenia na pripomienky aj zdieľaným osobám v kalendári",
+ "Reminders are always sent to organizers and attendees." : "Upozornenia sa vždy posielajú organizátorom a účastníkom.",
"Enable notifications for events via push" : "Zapnúť oznámenia o udalostiach prostredníctvom technológie push.",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Tiež nainštalujte {calendarappstoreopen}apku Kalendár{linkclose} alebo {calendardocopen}pripojte svoj počítač a smartfón pre synchronizáciu ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Uistite sa, že ste správne nastavili {emailopen}e-mailový server{linkclose}.",
"There was an error updating your attendance status." : "Nastal problém pri aktualizácii Vašej účasti.",
"Please contact the organizer directly." : "Prosím kontaktujte priamo organizátora.",
"Are you accepting the invitation?" : "Príjmate pozvánku?",
"Tentative" : "Neistý",
- "Number of guests" : "Počet návštevníkov",
- "Comment" : "Komentár",
- "Your attendance was updated successfully." : "Vaša účasť bola aktualizovaná úspešne.",
- "Calendar and tasks" : "Kalendár a úlohy"
+ "Your attendance was updated successfully." : "Vaša účasť bola aktualizovaná úspešne."
},"pluralForm" :"nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n >= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);"
} \ No newline at end of file
diff --git a/apps/dav/l10n/sl.js b/apps/dav/l10n/sl.js
index 4ee502794bb..f33a677c95c 100644
--- a/apps/dav/l10n/sl.js
+++ b/apps/dav/l10n/sl.js
@@ -2,15 +2,17 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Koledar",
- "Todos" : "Naloge",
+ "Tasks" : "Naloge",
"Personal" : "Osebno",
"{actor} created calendar {calendar}" : "{actor} ustvari koledar {calendar}",
- "You created calendar {calendar}" : "Ustvarim koledar {calendar}",
+ "You created calendar {calendar}" : "Ustvarite koledar {calendar}",
"{actor} deleted calendar {calendar}" : "{actor} izbriše koledar {calendar}",
- "You deleted calendar {calendar}" : "Izbrišem koledar {calendar}",
+ "You deleted calendar {calendar}" : "Izbrišete koledar {calendar}",
"{actor} updated calendar {calendar}" : "{actor} posodobi koledar {calendar}",
- "You updated calendar {calendar}" : "Posodobim koledar {calendar}",
- "You shared calendar {calendar} as public link" : "Omogočim souporabo koledarja {calendar} z javno povezavo",
+ "You updated calendar {calendar}" : "Posodobite koledar {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} obnovi koledar {calendar}",
+ "You restored calendar {calendar}" : "Ustvarite koledar {calendar}",
+ "You shared calendar {calendar} as public link" : "Omogočite souporabo koledarja {calendar} z javno povezavo",
"You removed public link for calendar {calendar}" : "Odstranite javno povezavo koledarja {calendar}",
"{actor} shared calendar {calendar} with you" : "{actor} mi omogoči souporabo koledarja {calendar}",
"You shared calendar {calendar} with {user}" : "Omogočite souporabo koledarja {calendar} z uporabnikom {user}",
@@ -23,34 +25,41 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} omogoči souporabo koledarja {calendar} s skupino {group}",
"You unshared calendar {calendar} from group {group}" : "Onemogočite souporabo koledarja {calendar} s skupino {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} onemogoči souporabo koledarja {calendar} s skupino {group}",
+ "Untitled event" : "Neimenovan dogodek¨",
"{actor} created event {event} in calendar {calendar}" : "{actor} ustvari dogodek {event} v koledarju {calendar}",
"You created event {event} in calendar {calendar}" : "Ustvarite dogodek {event} v koledarju {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} izbriše dogodek {event} iz koledarja {calendar}",
"You deleted event {event} from calendar {calendar}" : "Izbrišete dogodek {event} v koledarju {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} posodobi dogodek {event} v koledarju {calendar}",
"You updated event {event} in calendar {calendar}" : "Posodobite dogodek {event} v koledarju {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} premakne dogodek {event} iz koledarja {sourceCalendar} v koledar {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Premaknete dogodek {event} iz koledarja {sourceCalendar} v koledar {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} obnovi dogodek {event} v koledarju {calendar}",
+ "You restored event {event} of calendar {calendar}" : "Obnovite dogodek {event} v koledarju {calendar}",
"Busy" : "Zasedeno",
- "{actor} created todo {todo} in list {calendar}" : "{actor} ustvari nalogo {todo} v koledarju {calendar}",
- "You created todo {todo} in list {calendar}" : "Ustvarite nalogo {todo} v koledarju {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} izbriše nalogo {todo} iz koledara {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Izbrišete nalogo {todo} iz koledarja {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} posodobi nalogo {todo} v koledarju {calendar}",
- "You updated todo {todo} in list {calendar}" : "Posodobite nalogo {todo} v seznamu {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} razreši nalogo {todo} v koledarju {calendar}",
- "You solved todo {todo} in list {calendar}" : "Razrešite nalogo {todo} v seznamu {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} ponovno odpre nalogo {todo} v koledarju {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Ponovno odprete nalogo {todo} v seznamu {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} ustvari nalogo {todo} v koledarju {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Ustvarite nalogo {todo} v koledarju {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} izbriše nalogo {todo} iz koledarja {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Izbrišete nalogo {todo} iz koledarja {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} posodobi nalogo {todo} v koledarju {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Posodobite nalogo {todo} v seznamu {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} razreši nalogo {todo} v koledarju {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Razrešite nalogo {todo} v seznamu {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} ponovno odpre nalogo {todo} v koledarju {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Ponovno odprete nalogo {todo} v seznamu {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} premakne nalogo {todo} iz seznama koledarja {sourceCalendar} v koledar {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Premaknete nalogo {todo} iz seznama koledarja {sourceCalendar} v koledar {targetCalendar}",
"Calendar, contacts and tasks" : "Koledar, stiki in naloge",
"A <strong>calendar</strong> was modified" : "V <strong>koledar</strong> je vpisana sprememba",
"A calendar <strong>event</strong> was modified" : "Spremenjen je <strong>dogodek</strong> v koledarju",
- "A calendar <strong>todo</strong> was modified" : "Spremenjena je <strong>naloga</strong> koledarja",
+ "A calendar <strong>to-do</strong> was modified" : "Spremenjena je <strong>naloga</strong> koledarja",
"Contact birthdays" : "Obletnice stikov",
"Death of %s" : "%s (obletnica smrti)",
+ "Untitled calendar" : "Neimenovan koledar",
"Calendar:" : "Koledar:",
"Date:" : "Datum:",
"Where:" : "Kje:",
"Description:" : "Opis:",
- "Untitled event" : "Neimenovan dogodek¨",
"_%n year_::_%n years_" : ["%n leto","%n leti","%n leta","%n let"],
"_%n month_::_%n months_" : ["%n mesec","%n meseca","%n meseci","%n mesecev"],
"_%n day_::_%n days_" : ["%n dan","%n dneva","%n dni","%n dni"],
@@ -64,51 +73,141 @@ OC.L10N.register(
"Where: %s" : "Kje: %s",
"%1$s via %2$s" : "%1$s prek %2$s",
"Cancelled: %1$s" : "Preklicano: %1$s",
- "Invitation canceled" : "Povabilo je preklicano",
+ "\"%1$s\" has been canceled" : "Dogodek »%1$s« je preklican",
"Re: %1$s" : "Odg: %1$s",
- "Invitation updated" : "Povabilo je posodobljeno",
+ "%1$s has accepted your invitation" : "%1$ssprejme vaše povabilo",
+ "%1$s has tentatively accepted your invitation" : "%1$s pogojno sprejme vabilo",
+ "%1$s has declined your invitation" : "%1$s zavrne vaše povabilo.",
+ "%1$s has responded to your invitation" : "%1$s pošlje odziv na vaše povabilo.",
+ "Invitation updated: %1$s" : "Povabilo je posodobljeno: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s posodobi dogodek »%2$s«",
"Invitation: %1$s" : "Povabilo: %1$s",
- "Invitation" : "Povabilo",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s vas želi povabiti na »%2$s«",
+ "Organizer:" : "Organizator:",
+ "Attendees:" : "Udeleženci:",
"Title:" : "Naslov:",
- "Time:" : "Čas:",
+ "When:" : "Kdaj:",
"Location:" : "Mesto:",
"Link:" : "Povezava:",
- "Organizer:" : "Organizator:",
- "Attendees:" : "Udeleženci:",
"Accept" : "Sprejmi",
"Decline" : "Zavrni",
"More options …" : "Več možnosti ...",
"More options at %s" : "Več možnosti je na %s",
+ "Monday" : "ponedeljek",
+ "Tuesday" : "torek",
+ "Wednesday" : "sreda",
+ "Thursday" : "četrtek",
+ "Friday" : "petek",
+ "Saturday" : "sobota",
+ "Sunday" : "nedelja",
+ "January" : "januar",
+ "February" : "februar",
+ "March" : "marec",
+ "April" : "april",
+ "May" : "maj",
+ "June" : "junij",
+ "July" : "julij",
+ "August" : "avgust",
+ "September" : "september",
+ "October" : "oktober",
+ "November" : "november",
+ "December" : "december",
+ "First" : "Prvi",
+ "Last" : "Zadnji",
"Contacts" : "Stiki",
+ "{actor} created address book {addressbook}" : "{actor} ustvari imenik {addressbook}",
+ "You created address book {addressbook}" : "Ustvarite imenik {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} izbriše imenik {addressbook}",
"You deleted address book {addressbook}" : "Izbrišete imenik {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} posodobi imenik {addressbook}",
+ "You updated address book {addressbook}" : "Posodobite imenik {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} vam omogoči souporabo imenika {addressbook}",
+ "You shared address book {addressbook} with {user}" : "Omogočite souporabo imenika {addressbook} z uporabnikom {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} omogoči souporabo imenika {addressbook} z uporabnikom {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} vam onemogoči souporabo imenika {addressbook}",
+ "You unshared address book {addressbook} from {user}" : "Onemogočite souporabo imenika {addressbook} z uporabnikom {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} onemogoči souporabo imenika {addressbook} z uporabnikom {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} si je onemogočil souporabo imenika {addressbook}",
+ "You shared address book {addressbook} with group {group}" : "Omogočili ste souporabo imenika {addressbook} z vašo skupino {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} je omogočil souporabo imenika {addressbook} z vašo skupino {group}",
+ "You unshared address book {addressbook} from group {group}" : "Onemogočili ste souporabo imenika {addressbook} z vašo skupino {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} je onemogočil souporabo imenika {addressbook} s skupino {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} je dadal nov stik {card} v imenik {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Dodali ste nov stik {card} v imenik {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} je izbrisal stik {card} iz imenika {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Odstranili ste stik {card} iz imenika {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} je posodobil stik {card} v imeniku {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Posodobili ste stik {card} v imeniku {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Spremenjen je bil <strong>stik</strong> oziroma <strong>imenik</strong>",
+ "Accounts" : "Računi",
+ "System address book which holds all accounts" : "Sistemski imenik, ki združuje vse račune",
+ "File is not updatable: %1$s" : "Datoteke ni mogoče posodobiti: %1$s",
+ "Could not write to final file, canceled by hook" : "Ni bilo mogoče pisati v končno datoteko. Opravilo je bilo preklicano.",
+ "Could not write file contents" : "Ni mogoče zapisati vsebine datoteke",
+ "_%n byte_::_%n bytes_" : ["%n bajt","%n bajta","%n bajti","%n bajtov"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Prišlo je do napake med kopiranjem datoteke na ciljno mesto (kopirano: %1$s, pričakovana velikost pa je: %2$s)",
+ "Could not rename part file to final file, canceled by hook" : "Ni mogoče preimenovati delne datoteke v končno ime. Opravilo je bilo preklicano.",
+ "Could not rename part file to final file" : "Ni mogoče preimenovati delne datoteke v končno ime.",
+ "Failed to check file size: %1$s" : "Preverjanje velikosti je spodletelo: %1$s",
+ "Encryption not ready: %1$s" : "Šifriranje ni mogoče: %1$s",
+ "Failed to open file: %1$s" : "Odpiranje datoteke je spodletelo: %1$s",
+ "Failed to unlink: %1$s" : "Odstranjevanje povezave je spodletelo: %1$s",
+ "Failed to write file contents: %1$s" : "Zapisovanje vsebine datoteke je spodletelo: %1$s",
+ "File not found: %1$s" : "Datoteke ni mogoče najti: %1$s",
"System is in maintenance mode." : "Sistem je v vzdrževalnem načinu.",
"Upgrade needed" : "Zahtevana je posodobitev",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Za uporabo CalDAV in CardDAV v okoljih iOS/macOS je treba %s nastaviti za uporabo HTTPS.",
"Configures a CalDAV account" : "Nastavi račun CalDAV",
"Configures a CardDAV account" : "Nastavi račun CardDAV",
"Events" : "Dogodki",
- "Tasks" : "Naloge",
"Untitled task" : "Neimenovana naloga",
"Completed on %s" : "Končana %s",
"Due on %s by %s" : "Poteče %s ob %s",
"Due on %s" : "Poteče %s",
+ "DAV system address book" : "Imenik sistema DAV",
+ "No outstanding DAV system address book sync." : "Usklajevanje DAV ni omogočeno.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Usklajevanje imenika DAV še ni bilo izvedeno, ker je na strežniku več kot 1000 uporabnikov, ali pa je morda prišlo do napake. Možnost je treba zagnati ročnoz ukazom »occ dav:sync-system-addressbook«.",
+ "WebDAV endpoint" : "Končna točka WebDAV",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Spletni stražnik še ni ustrezno nastavljen in ne omogoča usklajevanja, kaže, da je vmesnik WebDAV okvarjen.",
+ "Migrated calendar (%1$s)" : "Prenesen koledar (%1$s)",
+ "Calendars including events, details and attendees" : "Koledarji z dogodki, podrobnostmi in udeleženci",
"Contacts and groups" : "Stiki in skupine",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "Končna točka WebDAV",
- "Availability" : "Razpoložljivost",
+ "Absence saved" : "Odsotnost je shranjena",
+ "Failed to save your absence settings" : "Shranjevanje nastavitev odsotnosti je spodletelo",
+ "Absence cleared" : "Odsotnos je preklicana",
+ "Failed to clear your absence settings" : "Počiščenje nastavitev odsotnosti je spodletelo",
+ "First day" : "Prvi dan",
+ "Last day (inclusive)" : "Zadnji dan (vključujoče)",
+ "Name of the replacement" : "Ime zamenjave",
+ "No results." : "Ni zadetkov",
+ "Start typing." : "Začnite vpisovati.",
+ "Short absence status" : "Stanje kratke odsotnosti",
+ "Long absence Message" : "Sporočilo dolge odsostnosti",
+ "Save" : "Shrani",
+ "Disable absence" : "Onemogoči odsostnost",
+ "Failed to load availability" : "Nalaganje seznama razpoložljivih polj je spodletelo",
+ "Saved availability" : "Čas razpoložljivosti je shranjen",
+ "Failed to save availability" : "Shranjevanje časa razpoložljivosti je spodletelo",
"Time zone:" : "Časovni pas:",
"to" : "do",
"Delete slot" : "Izbriši možnost",
"No working hours set" : "Ni navedenih delovnih ur",
- "Add slot" : "Dodaj polje",
- "Monday" : "ponedeljek",
- "Tuesday" : "torek",
- "Wednesday" : "sreda",
- "Thursday" : "četrtek",
- "Friday" : "petek",
- "Saturday" : "sobota",
- "Sunday" : "nedelja",
- "Save" : "Shrani",
+ "Add slot" : "Dodaj možnost",
+ "Weekdays" : "Delovni dnevi",
+ "Pick a start time for {dayName}" : "Izbor časa začetka za {dayName}",
+ "Pick a end time for {dayName}" : "Izbor časa konca za {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Samodejno nastavi stanje uporabnika na »Ne moti« in zavračaj prikaz obvestil izven časa razpoložljivosti.",
+ "Cancel" : "Prekliči",
+ "Import" : "Uvozi",
+ "Error while saving settings" : "Prišlo je do napake med shranjevanjem nastavitev",
+ "Reset to default" : "Ponastavi na privzeto",
+ "Availability" : "Razpoložljivost",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Če nastavite delovni čas, bodo drugi uporabniki pri izbiri časa sestanka videli, kdaj ste zasedeni.",
+ "Absence" : "Odsotnost",
+ "Configure your next absence period." : "Nastavitev naslednjega časa odsotnosti",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Namestite tudi {calendarappstoreopen}Koledar{linkclose}, ali pa se povežite z {calendardocopen}namiznim oziroma mobilnim usklajevalnikom ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Prepričajte se, da je {emailopen}poštni strežnik{linkclose} pravilno nastavljen.",
"Calendar server" : "Strežnik koledarja",
"Send invitations to attendees" : "Pošlji povabilo udeležencem",
"Automatically generate a birthday calendar" : "Samodejno ustvari koledar rojstnih dni",
@@ -116,16 +215,13 @@ OC.L10N.register(
"Hence they will not be available immediately after enabling but will show up after some time." : "To pomeni, da ne bodo na voljo takoj po omogočanju, ampak šele po krajšem času.",
"Send notifications for events" : "Pošlji obvestila za dogodke",
"Notifications are sent via background jobs, so these must occur often enough." : "Obvestila so poslana samodejno z opravili v ozadju, ker pomeni, da se morajo ta izvajati dovolj pogosto.",
+ "Send reminder notifications to calendar sharees as well" : "Pošiljanje opomnikov tudi naročnikom koledarja.",
+ "Reminders are always sent to organizers and attendees." : "Opomniki bodo vedno poslani organizatorjem in udeležencem.",
"Enable notifications for events via push" : "Omogoči potisna obvestila za dogodke",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Namestite tudi {calendarappstoreopen}Koledar{linkclose}, ali pa se povežite z {calendardocopen}namiznim oziroma mobilnim usklajevalnikom ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Prepričajte se, da je {emailopen}poštni strežnik{linkclose} pravilno nastavljen.",
"There was an error updating your attendance status." : "Prišlo je do napake med posodabljanjem vaše udeležbe.",
- "Please contact the organizer directly." : "Z organizatorjem stopite v stik neposredno.",
+ "Please contact the organizer directly." : "Z organizatorjem stopite neposredno v stik.",
"Are you accepting the invitation?" : "Ali želite sprejeti povabilo?",
"Tentative" : "Začasno",
- "Number of guests" : "Število gostov",
- "Comment" : "Opomba",
- "Your attendance was updated successfully." : "Vaša prisotnost je uspešno posodobljena.",
- "Calendar and tasks" : "Koledar in naloge"
+ "Your attendance was updated successfully." : "Vaša prisotnost je uspešno posodobljena."
},
"nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);");
diff --git a/apps/dav/l10n/sl.json b/apps/dav/l10n/sl.json
index a070343233e..423b215a669 100644
--- a/apps/dav/l10n/sl.json
+++ b/apps/dav/l10n/sl.json
@@ -1,14 +1,16 @@
{ "translations": {
"Calendar" : "Koledar",
- "Todos" : "Naloge",
+ "Tasks" : "Naloge",
"Personal" : "Osebno",
"{actor} created calendar {calendar}" : "{actor} ustvari koledar {calendar}",
- "You created calendar {calendar}" : "Ustvarim koledar {calendar}",
+ "You created calendar {calendar}" : "Ustvarite koledar {calendar}",
"{actor} deleted calendar {calendar}" : "{actor} izbriše koledar {calendar}",
- "You deleted calendar {calendar}" : "Izbrišem koledar {calendar}",
+ "You deleted calendar {calendar}" : "Izbrišete koledar {calendar}",
"{actor} updated calendar {calendar}" : "{actor} posodobi koledar {calendar}",
- "You updated calendar {calendar}" : "Posodobim koledar {calendar}",
- "You shared calendar {calendar} as public link" : "Omogočim souporabo koledarja {calendar} z javno povezavo",
+ "You updated calendar {calendar}" : "Posodobite koledar {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} obnovi koledar {calendar}",
+ "You restored calendar {calendar}" : "Ustvarite koledar {calendar}",
+ "You shared calendar {calendar} as public link" : "Omogočite souporabo koledarja {calendar} z javno povezavo",
"You removed public link for calendar {calendar}" : "Odstranite javno povezavo koledarja {calendar}",
"{actor} shared calendar {calendar} with you" : "{actor} mi omogoči souporabo koledarja {calendar}",
"You shared calendar {calendar} with {user}" : "Omogočite souporabo koledarja {calendar} z uporabnikom {user}",
@@ -21,34 +23,41 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} omogoči souporabo koledarja {calendar} s skupino {group}",
"You unshared calendar {calendar} from group {group}" : "Onemogočite souporabo koledarja {calendar} s skupino {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} onemogoči souporabo koledarja {calendar} s skupino {group}",
+ "Untitled event" : "Neimenovan dogodek¨",
"{actor} created event {event} in calendar {calendar}" : "{actor} ustvari dogodek {event} v koledarju {calendar}",
"You created event {event} in calendar {calendar}" : "Ustvarite dogodek {event} v koledarju {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} izbriše dogodek {event} iz koledarja {calendar}",
"You deleted event {event} from calendar {calendar}" : "Izbrišete dogodek {event} v koledarju {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} posodobi dogodek {event} v koledarju {calendar}",
"You updated event {event} in calendar {calendar}" : "Posodobite dogodek {event} v koledarju {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} premakne dogodek {event} iz koledarja {sourceCalendar} v koledar {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Premaknete dogodek {event} iz koledarja {sourceCalendar} v koledar {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} obnovi dogodek {event} v koledarju {calendar}",
+ "You restored event {event} of calendar {calendar}" : "Obnovite dogodek {event} v koledarju {calendar}",
"Busy" : "Zasedeno",
- "{actor} created todo {todo} in list {calendar}" : "{actor} ustvari nalogo {todo} v koledarju {calendar}",
- "You created todo {todo} in list {calendar}" : "Ustvarite nalogo {todo} v koledarju {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} izbriše nalogo {todo} iz koledara {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Izbrišete nalogo {todo} iz koledarja {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} posodobi nalogo {todo} v koledarju {calendar}",
- "You updated todo {todo} in list {calendar}" : "Posodobite nalogo {todo} v seznamu {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} razreši nalogo {todo} v koledarju {calendar}",
- "You solved todo {todo} in list {calendar}" : "Razrešite nalogo {todo} v seznamu {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} ponovno odpre nalogo {todo} v koledarju {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Ponovno odprete nalogo {todo} v seznamu {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} ustvari nalogo {todo} v koledarju {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Ustvarite nalogo {todo} v koledarju {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} izbriše nalogo {todo} iz koledarja {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Izbrišete nalogo {todo} iz koledarja {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} posodobi nalogo {todo} v koledarju {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Posodobite nalogo {todo} v seznamu {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} razreši nalogo {todo} v koledarju {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Razrešite nalogo {todo} v seznamu {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} ponovno odpre nalogo {todo} v koledarju {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Ponovno odprete nalogo {todo} v seznamu {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} premakne nalogo {todo} iz seznama koledarja {sourceCalendar} v koledar {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Premaknete nalogo {todo} iz seznama koledarja {sourceCalendar} v koledar {targetCalendar}",
"Calendar, contacts and tasks" : "Koledar, stiki in naloge",
"A <strong>calendar</strong> was modified" : "V <strong>koledar</strong> je vpisana sprememba",
"A calendar <strong>event</strong> was modified" : "Spremenjen je <strong>dogodek</strong> v koledarju",
- "A calendar <strong>todo</strong> was modified" : "Spremenjena je <strong>naloga</strong> koledarja",
+ "A calendar <strong>to-do</strong> was modified" : "Spremenjena je <strong>naloga</strong> koledarja",
"Contact birthdays" : "Obletnice stikov",
"Death of %s" : "%s (obletnica smrti)",
+ "Untitled calendar" : "Neimenovan koledar",
"Calendar:" : "Koledar:",
"Date:" : "Datum:",
"Where:" : "Kje:",
"Description:" : "Opis:",
- "Untitled event" : "Neimenovan dogodek¨",
"_%n year_::_%n years_" : ["%n leto","%n leti","%n leta","%n let"],
"_%n month_::_%n months_" : ["%n mesec","%n meseca","%n meseci","%n mesecev"],
"_%n day_::_%n days_" : ["%n dan","%n dneva","%n dni","%n dni"],
@@ -62,51 +71,141 @@
"Where: %s" : "Kje: %s",
"%1$s via %2$s" : "%1$s prek %2$s",
"Cancelled: %1$s" : "Preklicano: %1$s",
- "Invitation canceled" : "Povabilo je preklicano",
+ "\"%1$s\" has been canceled" : "Dogodek »%1$s« je preklican",
"Re: %1$s" : "Odg: %1$s",
- "Invitation updated" : "Povabilo je posodobljeno",
+ "%1$s has accepted your invitation" : "%1$ssprejme vaše povabilo",
+ "%1$s has tentatively accepted your invitation" : "%1$s pogojno sprejme vabilo",
+ "%1$s has declined your invitation" : "%1$s zavrne vaše povabilo.",
+ "%1$s has responded to your invitation" : "%1$s pošlje odziv na vaše povabilo.",
+ "Invitation updated: %1$s" : "Povabilo je posodobljeno: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s posodobi dogodek »%2$s«",
"Invitation: %1$s" : "Povabilo: %1$s",
- "Invitation" : "Povabilo",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s vas želi povabiti na »%2$s«",
+ "Organizer:" : "Organizator:",
+ "Attendees:" : "Udeleženci:",
"Title:" : "Naslov:",
- "Time:" : "Čas:",
+ "When:" : "Kdaj:",
"Location:" : "Mesto:",
"Link:" : "Povezava:",
- "Organizer:" : "Organizator:",
- "Attendees:" : "Udeleženci:",
"Accept" : "Sprejmi",
"Decline" : "Zavrni",
"More options …" : "Več možnosti ...",
"More options at %s" : "Več možnosti je na %s",
+ "Monday" : "ponedeljek",
+ "Tuesday" : "torek",
+ "Wednesday" : "sreda",
+ "Thursday" : "četrtek",
+ "Friday" : "petek",
+ "Saturday" : "sobota",
+ "Sunday" : "nedelja",
+ "January" : "januar",
+ "February" : "februar",
+ "March" : "marec",
+ "April" : "april",
+ "May" : "maj",
+ "June" : "junij",
+ "July" : "julij",
+ "August" : "avgust",
+ "September" : "september",
+ "October" : "oktober",
+ "November" : "november",
+ "December" : "december",
+ "First" : "Prvi",
+ "Last" : "Zadnji",
"Contacts" : "Stiki",
+ "{actor} created address book {addressbook}" : "{actor} ustvari imenik {addressbook}",
+ "You created address book {addressbook}" : "Ustvarite imenik {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} izbriše imenik {addressbook}",
"You deleted address book {addressbook}" : "Izbrišete imenik {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} posodobi imenik {addressbook}",
+ "You updated address book {addressbook}" : "Posodobite imenik {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} vam omogoči souporabo imenika {addressbook}",
+ "You shared address book {addressbook} with {user}" : "Omogočite souporabo imenika {addressbook} z uporabnikom {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} omogoči souporabo imenika {addressbook} z uporabnikom {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} vam onemogoči souporabo imenika {addressbook}",
+ "You unshared address book {addressbook} from {user}" : "Onemogočite souporabo imenika {addressbook} z uporabnikom {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} onemogoči souporabo imenika {addressbook} z uporabnikom {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} si je onemogočil souporabo imenika {addressbook}",
+ "You shared address book {addressbook} with group {group}" : "Omogočili ste souporabo imenika {addressbook} z vašo skupino {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} je omogočil souporabo imenika {addressbook} z vašo skupino {group}",
+ "You unshared address book {addressbook} from group {group}" : "Onemogočili ste souporabo imenika {addressbook} z vašo skupino {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} je onemogočil souporabo imenika {addressbook} s skupino {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} je dadal nov stik {card} v imenik {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Dodali ste nov stik {card} v imenik {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} je izbrisal stik {card} iz imenika {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Odstranili ste stik {card} iz imenika {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} je posodobil stik {card} v imeniku {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Posodobili ste stik {card} v imeniku {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Spremenjen je bil <strong>stik</strong> oziroma <strong>imenik</strong>",
+ "Accounts" : "Računi",
+ "System address book which holds all accounts" : "Sistemski imenik, ki združuje vse račune",
+ "File is not updatable: %1$s" : "Datoteke ni mogoče posodobiti: %1$s",
+ "Could not write to final file, canceled by hook" : "Ni bilo mogoče pisati v končno datoteko. Opravilo je bilo preklicano.",
+ "Could not write file contents" : "Ni mogoče zapisati vsebine datoteke",
+ "_%n byte_::_%n bytes_" : ["%n bajt","%n bajta","%n bajti","%n bajtov"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Prišlo je do napake med kopiranjem datoteke na ciljno mesto (kopirano: %1$s, pričakovana velikost pa je: %2$s)",
+ "Could not rename part file to final file, canceled by hook" : "Ni mogoče preimenovati delne datoteke v končno ime. Opravilo je bilo preklicano.",
+ "Could not rename part file to final file" : "Ni mogoče preimenovati delne datoteke v končno ime.",
+ "Failed to check file size: %1$s" : "Preverjanje velikosti je spodletelo: %1$s",
+ "Encryption not ready: %1$s" : "Šifriranje ni mogoče: %1$s",
+ "Failed to open file: %1$s" : "Odpiranje datoteke je spodletelo: %1$s",
+ "Failed to unlink: %1$s" : "Odstranjevanje povezave je spodletelo: %1$s",
+ "Failed to write file contents: %1$s" : "Zapisovanje vsebine datoteke je spodletelo: %1$s",
+ "File not found: %1$s" : "Datoteke ni mogoče najti: %1$s",
"System is in maintenance mode." : "Sistem je v vzdrževalnem načinu.",
"Upgrade needed" : "Zahtevana je posodobitev",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Za uporabo CalDAV in CardDAV v okoljih iOS/macOS je treba %s nastaviti za uporabo HTTPS.",
"Configures a CalDAV account" : "Nastavi račun CalDAV",
"Configures a CardDAV account" : "Nastavi račun CardDAV",
"Events" : "Dogodki",
- "Tasks" : "Naloge",
"Untitled task" : "Neimenovana naloga",
"Completed on %s" : "Končana %s",
"Due on %s by %s" : "Poteče %s ob %s",
"Due on %s" : "Poteče %s",
+ "DAV system address book" : "Imenik sistema DAV",
+ "No outstanding DAV system address book sync." : "Usklajevanje DAV ni omogočeno.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Usklajevanje imenika DAV še ni bilo izvedeno, ker je na strežniku več kot 1000 uporabnikov, ali pa je morda prišlo do napake. Možnost je treba zagnati ročnoz ukazom »occ dav:sync-system-addressbook«.",
+ "WebDAV endpoint" : "Končna točka WebDAV",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Spletni stražnik še ni ustrezno nastavljen in ne omogoča usklajevanja, kaže, da je vmesnik WebDAV okvarjen.",
+ "Migrated calendar (%1$s)" : "Prenesen koledar (%1$s)",
+ "Calendars including events, details and attendees" : "Koledarji z dogodki, podrobnostmi in udeleženci",
"Contacts and groups" : "Stiki in skupine",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "Končna točka WebDAV",
- "Availability" : "Razpoložljivost",
+ "Absence saved" : "Odsotnost je shranjena",
+ "Failed to save your absence settings" : "Shranjevanje nastavitev odsotnosti je spodletelo",
+ "Absence cleared" : "Odsotnos je preklicana",
+ "Failed to clear your absence settings" : "Počiščenje nastavitev odsotnosti je spodletelo",
+ "First day" : "Prvi dan",
+ "Last day (inclusive)" : "Zadnji dan (vključujoče)",
+ "Name of the replacement" : "Ime zamenjave",
+ "No results." : "Ni zadetkov",
+ "Start typing." : "Začnite vpisovati.",
+ "Short absence status" : "Stanje kratke odsotnosti",
+ "Long absence Message" : "Sporočilo dolge odsostnosti",
+ "Save" : "Shrani",
+ "Disable absence" : "Onemogoči odsostnost",
+ "Failed to load availability" : "Nalaganje seznama razpoložljivih polj je spodletelo",
+ "Saved availability" : "Čas razpoložljivosti je shranjen",
+ "Failed to save availability" : "Shranjevanje časa razpoložljivosti je spodletelo",
"Time zone:" : "Časovni pas:",
"to" : "do",
"Delete slot" : "Izbriši možnost",
"No working hours set" : "Ni navedenih delovnih ur",
- "Add slot" : "Dodaj polje",
- "Monday" : "ponedeljek",
- "Tuesday" : "torek",
- "Wednesday" : "sreda",
- "Thursday" : "četrtek",
- "Friday" : "petek",
- "Saturday" : "sobota",
- "Sunday" : "nedelja",
- "Save" : "Shrani",
+ "Add slot" : "Dodaj možnost",
+ "Weekdays" : "Delovni dnevi",
+ "Pick a start time for {dayName}" : "Izbor časa začetka za {dayName}",
+ "Pick a end time for {dayName}" : "Izbor časa konca za {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Samodejno nastavi stanje uporabnika na »Ne moti« in zavračaj prikaz obvestil izven časa razpoložljivosti.",
+ "Cancel" : "Prekliči",
+ "Import" : "Uvozi",
+ "Error while saving settings" : "Prišlo je do napake med shranjevanjem nastavitev",
+ "Reset to default" : "Ponastavi na privzeto",
+ "Availability" : "Razpoložljivost",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Če nastavite delovni čas, bodo drugi uporabniki pri izbiri časa sestanka videli, kdaj ste zasedeni.",
+ "Absence" : "Odsotnost",
+ "Configure your next absence period." : "Nastavitev naslednjega časa odsotnosti",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Namestite tudi {calendarappstoreopen}Koledar{linkclose}, ali pa se povežite z {calendardocopen}namiznim oziroma mobilnim usklajevalnikom ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Prepričajte se, da je {emailopen}poštni strežnik{linkclose} pravilno nastavljen.",
"Calendar server" : "Strežnik koledarja",
"Send invitations to attendees" : "Pošlji povabilo udeležencem",
"Automatically generate a birthday calendar" : "Samodejno ustvari koledar rojstnih dni",
@@ -114,16 +213,13 @@
"Hence they will not be available immediately after enabling but will show up after some time." : "To pomeni, da ne bodo na voljo takoj po omogočanju, ampak šele po krajšem času.",
"Send notifications for events" : "Pošlji obvestila za dogodke",
"Notifications are sent via background jobs, so these must occur often enough." : "Obvestila so poslana samodejno z opravili v ozadju, ker pomeni, da se morajo ta izvajati dovolj pogosto.",
+ "Send reminder notifications to calendar sharees as well" : "Pošiljanje opomnikov tudi naročnikom koledarja.",
+ "Reminders are always sent to organizers and attendees." : "Opomniki bodo vedno poslani organizatorjem in udeležencem.",
"Enable notifications for events via push" : "Omogoči potisna obvestila za dogodke",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Namestite tudi {calendarappstoreopen}Koledar{linkclose}, ali pa se povežite z {calendardocopen}namiznim oziroma mobilnim usklajevalnikom ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Prepričajte se, da je {emailopen}poštni strežnik{linkclose} pravilno nastavljen.",
"There was an error updating your attendance status." : "Prišlo je do napake med posodabljanjem vaše udeležbe.",
- "Please contact the organizer directly." : "Z organizatorjem stopite v stik neposredno.",
+ "Please contact the organizer directly." : "Z organizatorjem stopite neposredno v stik.",
"Are you accepting the invitation?" : "Ali želite sprejeti povabilo?",
"Tentative" : "Začasno",
- "Number of guests" : "Število gostov",
- "Comment" : "Opomba",
- "Your attendance was updated successfully." : "Vaša prisotnost je uspešno posodobljena.",
- "Calendar and tasks" : "Koledar in naloge"
+ "Your attendance was updated successfully." : "Vaša prisotnost je uspešno posodobljena."
},"pluralForm" :"nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);"
} \ No newline at end of file
diff --git a/apps/dav/l10n/sq.js b/apps/dav/l10n/sq.js
deleted file mode 100644
index 0fc1d01d825..00000000000
--- a/apps/dav/l10n/sq.js
+++ /dev/null
@@ -1,63 +0,0 @@
-OC.L10N.register(
- "dav",
- {
- "Calendar" : "Kalendar",
- "Todos" : "Për tu bërë",
- "Personal" : "Personale",
- "{actor} created calendar {calendar}" : "{aktori} krijoi kalendarin {kalendarin}",
- "You created calendar {calendar}" : "Ju krijuat kalendarin {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} fshiu kalendarin {calendar}",
- "You deleted calendar {calendar}" : "Ju fshit kalendarin {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} përditësoi kalendarin {calendar}",
- "You updated calendar {calendar}" : "Ju përditësuat kalendarin {calendar}",
- "{actor} shared calendar {calendar} with you" : " {actor} ndau kalendarin {calendar} me ju",
- "You shared calendar {calendar} with {user}" : "Ju ndat kalendarin {calendar} me {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} ndau kalendarin {calendar} me {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ndaloj së ndari kalendarin {calendar} me ju",
- "You unshared calendar {calendar} from {user}" : "Ju ndaluat së ndari kalendarin {calendar} me {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} ndaloj së ndari kalendarin {calendar} me {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} ndaloj së ndari kalendarin {calendar} me veten",
- "You shared calendar {calendar} with group {group}" : "Ju ndat kalendarin {calendar} me grupin {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} ndau kalendarin {calendar} me grupin {group}",
- "You unshared calendar {calendar} from group {group}" : "Ju ndaluat së ndari kalendarin {calendar} me grupin {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} ndaloj së ndari kalendarin {calendar} me grupin {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} krijoj eventin {event} në kalendarin {calendar}",
- "You created event {event} in calendar {calendar}" : "Ju krijuat eventin {event} në kalendarin {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} fshiu eventin {event} nga kalendari {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Ju fshit eventin {event} nga kalndari {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} përditsoi eventin {event} në kalndarin {calendar}",
- "You updated event {event} in calendar {calendar}" : "Ju përditësuat eventin {event} në kalndarin {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} u krijua todo{todo} në listën {calendar}",
- "You created todo {todo} in list {calendar}" : "Ju krijuat todo {todo} në listën {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} u fshi todo{ todo} nga lista{calendar}",
- "You deleted todo {todo} from list {calendar}" : "Ju fshit todo{todo} nga lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} u përditësua todo{todo} në listën{calendar}",
- "You updated todo {todo} in list {calendar}" : "Ju përditësuat përtëbërë {todo} në listën{calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} zgjidhi përtëbërë {todo} në listën {calendar}",
- "You solved todo {todo} in list {calendar}" : "Ju zgjidhët përtëbërë {todo} në listën {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} rihapi përtëbërë {todo} në listën {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Ju rihapët përtëbërë {todo} në listën {calendar}",
- "A <strong>calendar</strong> was modified" : "Një <strong>kalendar</strong> u modifikua",
- "A calendar <strong>event</strong> was modified" : "Një <strong>event</strong> në kalendar u modifikua",
- "A calendar <strong>todo</strong> was modified" : "Një kalendar <strong>todo<strong> u modifikua",
- "Contact birthdays" : "Ditëlindjet e kontakteve",
- "Where:" : "Ku:",
- "Description:" : "Përshkrimi:",
- "Invitation canceled" : "Ftesa u anullua",
- "Invitation updated" : "Ftesa u përditësua",
- "Location:" : "Vendndodhje:",
- "Link:" : "Link:",
- "Accept" : "Prano",
- "Decline" : "Refuzo",
- "More options …" : "Më shumë opsione ...",
- "Contacts" : "Kontaktet",
- "Tasks" : "Detyra",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativë",
- "Save" : "Ruaj",
- "Your attendance was updated successfully." : "Pjesëmarrja juaj u përditësua me sukses.",
- "Send invitations to attendees" : "Dërgo ftesa tek pjesëmarrësit",
- "Hello %s," : "Përshëndetje %s,",
- "When:" : "Kur:"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/sq.json b/apps/dav/l10n/sq.json
deleted file mode 100644
index 128ad854cac..00000000000
--- a/apps/dav/l10n/sq.json
+++ /dev/null
@@ -1,61 +0,0 @@
-{ "translations": {
- "Calendar" : "Kalendar",
- "Todos" : "Për tu bërë",
- "Personal" : "Personale",
- "{actor} created calendar {calendar}" : "{aktori} krijoi kalendarin {kalendarin}",
- "You created calendar {calendar}" : "Ju krijuat kalendarin {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} fshiu kalendarin {calendar}",
- "You deleted calendar {calendar}" : "Ju fshit kalendarin {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} përditësoi kalendarin {calendar}",
- "You updated calendar {calendar}" : "Ju përditësuat kalendarin {calendar}",
- "{actor} shared calendar {calendar} with you" : " {actor} ndau kalendarin {calendar} me ju",
- "You shared calendar {calendar} with {user}" : "Ju ndat kalendarin {calendar} me {user}",
- "{actor} shared calendar {calendar} with {user}" : "{actor} ndau kalendarin {calendar} me {user}",
- "{actor} unshared calendar {calendar} from you" : "{actor} ndaloj së ndari kalendarin {calendar} me ju",
- "You unshared calendar {calendar} from {user}" : "Ju ndaluat së ndari kalendarin {calendar} me {user}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} ndaloj së ndari kalendarin {calendar} me {user}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} ndaloj së ndari kalendarin {calendar} me veten",
- "You shared calendar {calendar} with group {group}" : "Ju ndat kalendarin {calendar} me grupin {group}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} ndau kalendarin {calendar} me grupin {group}",
- "You unshared calendar {calendar} from group {group}" : "Ju ndaluat së ndari kalendarin {calendar} me grupin {group}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} ndaloj së ndari kalendarin {calendar} me grupin {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} krijoj eventin {event} në kalendarin {calendar}",
- "You created event {event} in calendar {calendar}" : "Ju krijuat eventin {event} në kalendarin {calendar}",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} fshiu eventin {event} nga kalendari {calendar}",
- "You deleted event {event} from calendar {calendar}" : "Ju fshit eventin {event} nga kalndari {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} përditsoi eventin {event} në kalndarin {calendar}",
- "You updated event {event} in calendar {calendar}" : "Ju përditësuat eventin {event} në kalndarin {calendar}",
- "{actor} created todo {todo} in list {calendar}" : "{actor} u krijua todo{todo} në listën {calendar}",
- "You created todo {todo} in list {calendar}" : "Ju krijuat todo {todo} në listën {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} u fshi todo{ todo} nga lista{calendar}",
- "You deleted todo {todo} from list {calendar}" : "Ju fshit todo{todo} nga lista {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} u përditësua todo{todo} në listën{calendar}",
- "You updated todo {todo} in list {calendar}" : "Ju përditësuat përtëbërë {todo} në listën{calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} zgjidhi përtëbërë {todo} në listën {calendar}",
- "You solved todo {todo} in list {calendar}" : "Ju zgjidhët përtëbërë {todo} në listën {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} rihapi përtëbërë {todo} në listën {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Ju rihapët përtëbërë {todo} në listën {calendar}",
- "A <strong>calendar</strong> was modified" : "Një <strong>kalendar</strong> u modifikua",
- "A calendar <strong>event</strong> was modified" : "Një <strong>event</strong> në kalendar u modifikua",
- "A calendar <strong>todo</strong> was modified" : "Një kalendar <strong>todo<strong> u modifikua",
- "Contact birthdays" : "Ditëlindjet e kontakteve",
- "Where:" : "Ku:",
- "Description:" : "Përshkrimi:",
- "Invitation canceled" : "Ftesa u anullua",
- "Invitation updated" : "Ftesa u përditësua",
- "Location:" : "Vendndodhje:",
- "Link:" : "Link:",
- "Accept" : "Prano",
- "Decline" : "Refuzo",
- "More options …" : "Më shumë opsione ...",
- "Contacts" : "Kontaktet",
- "Tasks" : "Detyra",
- "WebDAV" : "WebDAV",
- "Tentative" : "Tentativë",
- "Save" : "Ruaj",
- "Your attendance was updated successfully." : "Pjesëmarrja juaj u përditësua me sukses.",
- "Send invitations to attendees" : "Dërgo ftesa tek pjesëmarrësit",
- "Hello %s," : "Përshëndetje %s,",
- "When:" : "Kur:"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/dav/l10n/sr.js b/apps/dav/l10n/sr.js
index 35eb35d6f67..6ac07d5ffb3 100644
--- a/apps/dav/l10n/sr.js
+++ b/apps/dav/l10n/sr.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Календар",
- "Todos" : "Подсетници",
+ "Tasks" : "Задаци",
"Personal" : "Лично",
"{actor} created calendar {calendar}" : "{actor} направи календар {calendar}",
"You created calendar {calendar}" : "Направили сте календар {calendar}",
@@ -10,6 +10,8 @@ OC.L10N.register(
"You deleted calendar {calendar}" : "Обрисали сте календар {calendar}",
"{actor} updated calendar {calendar}" : "{actor} ажурира календар {calendar}",
"You updated calendar {calendar}" : "Ажурирали сте календар {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} је обновио календар {calendar}",
+ "You restored calendar {calendar}" : "Обновили сте календар {calendar}",
"You shared calendar {calendar} as public link" : "Поделили сте календар {calendar} као јавну везу",
"You removed public link for calendar {calendar}" : "Уклонили сте јавну везу за календар {calendar}",
"{actor} shared calendar {calendar} with you" : "{actor} подели календар {calendar} са вама",
@@ -23,33 +25,41 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} подели календар {calendar} са групом {group}",
"You unshared calendar {calendar} from group {group}" : "Укинули сте дељење календара {calendar} са групом {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} укину дељење календара {calendar} са групом {group}",
+ "Untitled event" : "Неименовани догађај",
"{actor} created event {event} in calendar {calendar}" : "{actor} је направио догађај {event} у календару {calendar}",
"You created event {event} in calendar {calendar}" : "Креирали сте догађај {event} у календару {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} је обрисао догађај {event} из календара {calendar}",
"You deleted event {event} from calendar {calendar}" : "Обрисали сте догађај {event} из календара {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} је ажурирао догађај {event} у календару {calendar}",
"You updated event {event} in calendar {calendar}" : "Ажурирали сте догађај {event} у календару {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} је преместио догађај {event} из календара {sourceCalendar} у календар {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Преместили сте догађај {event} из календара {sourceCalendar} у календар {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} је обновио догађај {event} календара {calendar}",
+ "You restored event {event} of calendar {calendar}" : "Обновили сте догађај {event} календара {calendar}",
"Busy" : "Заузет/а",
- "{actor} created todo {todo} in list {calendar}" : "{actor} је направио подсетник {todo} у листи {calendar}",
- "You created todo {todo} in list {calendar}" : "Креирали сте подсетник {todo} у листи {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} је обрисао подсетник {todo} из листе {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Обрисали сте подсетник {todo} из листе {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} је ажурирао подсетник {todo} у листи {calendar}",
- "You updated todo {todo} in list {calendar}" : "Ажурирали сте подсетник {todo} у листи {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} је обрисао подсетник {todo} из листе {calendar}",
- "You solved todo {todo} in list {calendar}" : "Маркирали сте подсетник {todo} као готов у листи {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} је поново отворио подсетник {todo} у листи {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Поново сте отворили подсетник {todo}  у листи {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} је креирао обавезу {todo} у листи {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Креирали сте обавезу {todo} у листи {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} је обрисао обавезу {todo} из листе {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "обрисали сте обавезу {todo} из листе {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} је ажурирао обавезу {todo} у листи {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Ажурирали сте обавезу {todo} у листи {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} је извршио обавезу {todo} у листи {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Извршили сте обавезу {todo} у листи {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} је поново отворио обавезу {todo} у листи {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Поново сте отворили обавезу {todo} у листи {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} је преместио обавезу {todo} из листе {sourceCalendar} у листу {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Преместили сте обавезу {todo} из листе {sourceCalendar} у листу {targetCalendar}",
+ "Calendar, contacts and tasks" : "Календар, контакти и задаци",
"A <strong>calendar</strong> was modified" : "<strong>Календар</strong> је измењен",
"A calendar <strong>event</strong> was modified" : "<strong>Догађај</strong> из календара је измењен",
- "A calendar <strong>todo</strong> was modified" : "<strong>Подсетник</strong> из календара је измењен",
+ "A calendar <strong>to-do</strong> was modified" : "Календар <strong>обавеза</strong> је измењен",
"Contact birthdays" : "Рођендани контаката",
"Death of %s" : " %s смрт",
+ "Untitled calendar" : "Неименовани календар",
"Calendar:" : "Календар:",
"Date:" : "Датум:",
"Where:" : "Место:",
"Description:" : "Опис:",
- "Untitled event" : "Неименовани догађај",
"_%n year_::_%n years_" : ["%n година","%n године","%n година"],
"_%n month_::_%n months_" : ["%n месец","%n месеца","%n месеци"],
"_%n day_::_%n days_" : ["%n дан","%n дана","%n дана"],
@@ -62,41 +72,251 @@ OC.L10N.register(
"Description: %s" : "Опис: %s",
"Where: %s" : "Место: %s",
"%1$s via %2$s" : "%1$s преко %2$s",
- "Invitation canceled" : "Позивница отказана",
- "Invitation updated" : "Позивница ажурирана",
- "Invitation" : "Позивница",
+ "In the past on %1$s for the entire day" : "У прошлости %1$s током целог дана",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["За минут дана %1$s током целог дана","За %n минута дана %1$s током целог дана","За %n минут дана %1$s током целог дана"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["За %n сат у %1$s током целог дана","За %n сата у %1$s током целог дана","За %n сата у %1$s током целог дана"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["За %n дан у %1$s током целог дана","За %n дана у %1$s током целог дана","За %n дана у %1$s током целог дана"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["За %n недељу у %1$s током целог дана","За %n недеље у %1$s током целог дана","За %n недеља у %1$s током целог дана"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["За %n месец у %1$s током целог дана","За %n месеца у %1$s током целог дана","За %n месеци у %1$s током целог дана"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["За %n годину у %1$s током целог дана","За %n године у %1$s током целог дана","За %n година у %1$s током целог дана"],
+ "In the past on %1$s between %2$s - %3$s" : "У прошлости, у %1$s између %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["За %n минут, у %1$s између %2$s - %3$s","За %n минута, у %1$s између %2$s - %3$s","За %n минута, у %1$s између %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["За %n сат, у %1$s између %2$s - %3$s","За %n сата, у %1$s између %2$s - %3$s","За %n сати, у %1$s између %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["За %n дан, у %1$s између %2$s - %3$s","За %n дана, у %1$s између %2$s - %3$s","За %n дана, у %1$s између %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["За %n недељу, у %1$s између %2$s - %3$s","За %n недеље, у %1$s између %2$s - %3$s","За %n недеља, у %1$s између %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["За %n месец, у %1$s између %2$s - %3$s","За %n месеца, у %1$s између %2$s - %3$s","За %n месеци, у %1$s између %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["За %n годину, у %1$s између %2$s - %3$s","За %n године, у %1$s између %2$s - %3$s","За %n година, у %1$s између %2$s - %3$s"],
+ "Could not generate when statement" : "Не може да се генерише одредба када",
+ "Every Day for the entire day" : "Сваки дан током целог дана",
+ "Every Day for the entire day until %1$s" : "Сваки дан током целог дана, све до %1$s",
+ "Every Day between %1$s - %2$s" : "Сваки дан од %1$s до %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Сваки дан од %1$s до %2$s све до %3$s",
+ "Every %1$d Days for the entire day" : "Сваких %1$d дана током целог дана",
+ "Every %1$d Days for the entire day until %2$s" : "Сваких %1$d дана током целог дана све до %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Сваких %1$d дана од %2$s до %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Сваких %1$d дана од %2$s до %3$s све до %4$s",
+ "Could not generate event recurrence statement" : "Не може да се генерише одредба понављања",
+ "Every Week on %1$s for the entire day" : "%1$s сваке недеље током целог дана",
+ "Every Week on %1$s for the entire day until %2$s" : "%1$s сваке недеље током целог дана све до %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "%1$s сваке недеље од %2$s до %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "%1$s сваке недеље од %2$s до %3$s све до %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "%2$s сваких %1$d недеља током целог дана",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "%2$s сваких %1$d недеља током целог дана све до %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "%2$s сваких %1$d недеља од %3$s до %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "%2$s сваких %1$d недеља од %3$s до %4$s све до %5$s",
+ "Every Month on the %1$s for the entire day" : "%1$s сваког месеца током целог дана",
+ "Every Month on the %1$s for the entire day until %2$s" : "%1$s сваког месеца током целог дана све до %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "%1$s сваког месеца од %2$s до %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "%1$s сваког месеца од %2$s до %3$s све до %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "%2$s сваких %1$d месеци током целог дана",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "%2$s сваких %1$d месеци током целог дана све до %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "%2$s сваких %1$d месеци од %3$s до %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "%2$s сваких %1$d месеци од %3$s до %4$s све до %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "%1$s сваке године дана %2$s током целог дана",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "%1$s сваке године, дана %2$s током целог дана све до %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "%1$s сваке године, дана %2$s од %3$s до %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "%1$s сваке године, дана %2$s од %3$s до %4$s све до %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "%2$s сваких %1$d година, дана %3$s током целог дана",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "%2$s сваких %1$d година, дана %3$s током целог дана све до %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "%2$s сваких %1$d година, дана %3$s од %4$s до %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "%2$s сваких %1$d година, дана %3$s од %4$s до %5$s све до %6$s",
+ "On specific dates for the entire day until %1$s" : "Одређених дана током целог дана, све до %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "Одређених дана од %1$s до %2$s све до %3$s",
+ "In the past on %1$s" : "У прошлости, у %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["За %n минут, у %1$s","За %n минута, у %1$s","За %n минута, у %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["За %n сат, у %1$s","За %n сата, у %1$s","За %n сати, у %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["За %n дан, у %1$s","За %n дана, у %1$s","За %n дана, у %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["За %n недељу, у %1$s","За %n недеље, у %1$s","За %n недеља, у %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["За %n месец, у %1$s","За %n месеца, у %1$s","За %n месеци, у %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["За %n годину, у %1$s","За %n године, у %1$s","За %n година, у %1$s"],
+ "In the past on %1$s then on %2$s" : "У прошлости, у %1$s па онда у %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["За %n минут, у %1$s па онда у %2$s","За %n минута, у %1$s па онда у %2$s","За %n минута, у %1$s па онда у %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["За %n сат, у %1$s па онда у %2$s","За %n сата, у %1$s па онда у %2$s","За %n сати, у %1$s па онда у %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["За %n дан, у %1$s па онда у %2$s","За %n дана, у %1$s па онда у %2$s","За %n дана, у %1$s па онда у %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["За %n недељу, у %1$s па онда у %2$s","За %n недеље, у %1$s па онда у %2$s","За %n недеља, у %1$s па онда у %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["За %n месец, у %1$s па онда у %2$s","За %n месеца, у %1$s па онда у %2$s","За %n месеци, у %1$s па онда у %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["За %n годину, у %1$s па онда у %2$s","За %n године, у %1$s па онда у %2$s","За %n година, у %1$s па онда у %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "У прошлости, у %1$s па онда у %2$s и %3$s",
+ "_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_" : ["За %n минут, у %1$s па онда у %2$s и %3$s","За %n минута, у %1$s па онда у %2$s и %3$s","За %n минута, у %1$s па онда у %2$s и %3$s"],
+ "_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_" : ["За %n сат, у %1$s па онда у %2$s и %3$s","За %n сата, у %1$s па онда у %2$s и %3$s","За %n сати, у %1$s па онда у %2$s и %3$s"],
+ "_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_" : ["За %n дан, у %1$s па онда у %2$s и %3$s","За %n дана, у %1$s па онда у %2$s и %3$s","За %n дана, у %1$s па онда у %2$s и %3$s"],
+ "_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_" : ["За %n недељу, у %1$s па онда у %2$s и %3$s","За %n недеље, у %1$s па онда у %2$s и %3$s","За %n недеља, у %1$s па онда у %2$s и %3$s"],
+ "_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_" : ["За %n месец, у %1$s па онда у %2$s и %3$s","За %n месеца, у %1$s па онда у %2$s и %3$s","За %n месеци, у %1$s па онда у %2$s и %3$s"],
+ "_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_" : ["За %n годину, у %1$s па онда у %2$s и %3$s","За %n године, у %1$s па онда у %2$s и %3$s","За %n година, у %1$s па онда у %2$s и %3$s"],
+ "Could not generate next recurrence statement" : "Није могла да се генерише одредба следћег појављивања",
+ "Cancelled: %1$s" : "Отказано: %1$s",
+ "\"%1$s\" has been canceled" : "„%1$s” је отказано",
+ "Re: %1$s" : "Одг: %1$s",
+ "%1$s has accepted your invitation" : "%1$s је прихватио вашу позивницу",
+ "%1$s has tentatively accepted your invitation" : "%1$s је условно прихватио вашу позивницу",
+ "%1$s has declined your invitation" : "%1$s је одбио вашу позивницу",
+ "%1$s has responded to your invitation" : "%1$s је одговорио на вашу позивницу",
+ "Invitation updated: %1$s" : "Позивница је ажурирана: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s је ажурирао догађај „%2$s”",
+ "Invitation: %1$s" : "Позивница: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s жели да вас позове на „%2$s",
+ "Organizer:" : "Организатор:",
+ "Attendees:" : "Присутни:",
"Title:" : "Наслов:",
- "Time:" : "Време:",
+ "When:" : "Време:",
"Location:" : "Локација:",
"Link:" : "Веза:",
- "Organizer:" : "Организатор:",
- "Attendees:" : "Присутни:",
+ "Occurring:" : "Појављивање:",
"Accept" : "Прихвати",
"Decline" : "Одбиј",
"More options …" : "Још опција…",
"More options at %s" : "Још опција на %s",
+ "Monday" : "Понедељак",
+ "Tuesday" : "Уторак",
+ "Wednesday" : "Среда",
+ "Thursday" : "Четвртак",
+ "Friday" : "Петак",
+ "Saturday" : "Субота",
+ "Sunday" : "Недеља",
+ "January" : "Јануар",
+ "February" : "Фебруар",
+ "March" : "Март",
+ "April" : "Април",
+ "May" : "Мај",
+ "June" : "Јун",
+ "July" : "Јул",
+ "August" : "Август",
+ "September" : "Септембар",
+ "October" : "Октобар",
+ "November" : "Новембар",
+ "December" : "Децембар",
+ "First" : "Прва",
+ "Second" : "Друго",
+ "Third" : "Треће",
+ "Fourth" : "Четврто",
+ "Fifth" : "Пето",
+ "Last" : "Последња",
+ "Second Last" : "Претпоследње",
+ "Third Last" : "Треће од краја",
+ "Fourth Last" : "Четврто од краја",
+ "Fifth Last" : "Пето од краја",
"Contacts" : "Контакти",
+ "{actor} created address book {addressbook}" : "{actor} је креирао адресар {addressbook}",
+ "You created address book {addressbook}" : "Креирали сте адресар {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} је обрисао адресар {addressbook}",
+ "You deleted address book {addressbook}" : "Обрисали сте адресар {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} је ажурирао адресар {addressbook}",
+ "You updated address book {addressbook}" : "Ажурирали сте адресар {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} је са вама поделио адресар {addressbook}",
+ "You shared address book {addressbook} with {user}" : "Поделили сте адресар {addressbook} са {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} је поделио адресар {addressbook} са {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} је уклонио дељење адресара {addressbook} са вама",
+ "You unshared address book {addressbook} from {user}" : "Уклонили сте дељење адресара {addressbook} са {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} је уклонио дељење адресара {addressbook} са {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} више не дели адресар {addressbook} са собом",
+ "You shared address book {addressbook} with group {group}" : "Поделили сте адресар {addressbook} са групом {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} је поделио адресар {addressbook} са групом {group}",
+ "You unshared address book {addressbook} from group {group}" : "Уклонили сте дељење адресара {addressbook} са групом {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} је уклонио дељење адресара {addressbook} са групом {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} је креирао контакт {card} у адресару {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Креирали сте контакт {card} у адресару {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} је обрисао контакт {card} из адресара {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Обрисали сте контакт {card} из адресара {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} је ажурирао контакт {card} у адресару {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Ажурирали сте контакт {card} у адресару {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Измењен је <strong>контакт</strong> или <strong>адресар</strong>",
+ "Accounts" : "Налози",
+ "System address book which holds all accounts" : "Системски адресар у којем се налазе сви налози",
+ "File is not updatable: %1$s" : "Фајл не може да се ажурира: %1$s",
+ "Failed to get storage for file" : "Није успело добијање складишта за фајл",
+ "Could not write to final file, canceled by hook" : "Не може да се упише у крајњи фајл, отказала је кука",
+ "Could not write file contents" : "Не може да се упише садржај фајла",
+ "_%n byte_::_%n bytes_" : ["%n бајт","%n бајта","%n бајтова"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Грешка приликом копирања фајла на циљну локацију (копирано: %1$s, очекивана величина фајла: %2$s)",
+ "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." : "Очекивала се величина фајла %1$s, али је (од Nextcloud клијента) прочитано и уписано (у Nextcloud складиште) %2$s. Или може бити мрежни проблем на страни која шаље, или проблем код уписа у складиште на серверу.",
+ "Could not rename part file to final file, canceled by hook" : "Делимични фајл не може да се преименује у коначни фајл, отказала је кука",
+ "Could not rename part file to final file" : "Делимични фајл не може да се преименује у коначни фајл",
+ "Failed to check file size: %1$s" : "Није успела провера величине фајла: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "Фајл не може да се отвори: %1$s, изгледа да фајл постоји",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Фајл не може да се отвори: %1$s, изгледа да фајл не постоји",
+ "Encryption not ready: %1$s" : "Шифрирање није спремно: %1$s",
+ "Failed to open file: %1$s" : "Фајл не може да се отвори: %1$s",
+ "Failed to unlink: %1$s" : "Није успело уклањање линка: %1$s",
+ "Failed to write file contents: %1$s" : "Није успело уписивање садржаја фајла: %1$s",
+ "File not found: %1$s" : "Фајл не може да се пронађе: %1$s",
+ "Invalid target path" : "Неисправна путања циља",
+ "System is in maintenance mode." : "Систем је у режиму одржавања.",
"Upgrade needed" : "Потребна надградња",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "%s мора да буде подешен да користи HTTPS да бисте користи CalDAV и CardDAV са iOS/macOS-ом.",
"Configures a CalDAV account" : "Подешава CalDAV налог",
"Configures a CardDAV account" : "Подешава CardDAV налог",
"Events" : "Догађаји",
- "Tasks" : "Задаци",
"Untitled task" : "Неименовани задатак",
"Completed on %s" : "Завршено %s",
"Due on %s by %s" : "Рок је %s од стране %s",
"Due on %s" : "Рок је %s",
- "WebDAV" : "ВебДАВ",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Добро дошли у Nextcloud Календар!\n\nОво је догађај за пример - истражите флексибилност планирања са Nextcloud Календаром тако што ћете направите било какве измене!\n\nСа Nextcloud Календаром, можете да:\n- Креирате, уређујете и управљате догађајима без муке.\n- Креирате више календара и делите их са члановима тима, пријатељима или породицом.\n- Проверавате доступност и осталима приказујете време у које сте заузети.\n- Лако интегришете са осталим апликацијама и уређајима помоћу CalDAV.\n- Прилагодите своје искуство: правите распоред догађаја који се понављају, подешавате обавештења и остале поставке.",
+ "Example event - open me!" : "Догађај за пример - отвори ме!",
+ "System Address Book" : "Системски адресар",
+ "The system address book contains contact information for all users in your instance." : "Системски адресар садржи информације о свим корисницима на вашој инстанци.",
+ "Enable System Address Book" : "Укључи системски адресар",
+ "DAV system address book" : "DAV системски адресар",
+ "No outstanding DAV system address book sync." : "Не постоји ниједна синхронизација DAV системског адресара која треба да се обави.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV синхронизација системског адресара се још увек није покренула јер ваша инстанца има више од 1000 корисника или јер је дошло до грешке. Молимо вас да га ручно покренете командом „occ dav:sync-system-addressbook”.",
"WebDAV endpoint" : "WebDAV крајња тачка",
- "to" : "за",
- "Monday" : "Понедељак",
- "Tuesday" : "Уторак",
- "Wednesday" : "Среда",
- "Thursday" : "Четвртак",
- "Friday" : "Петак",
- "Saturday" : "Субота",
- "Sunday" : "Недеља",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Није могло да се провери да ли ваш веб сервер исправно подешен тако да се омогући синхронизација фајлова преко WebDAV. Молимо вас да проверите ручно.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Сервер није правилно подешен за синхронизацију фајлова. Изгледа да је ВебДАВ сучеље покварено.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Ваш веб сервер исправно подешен тако да се омогући синхронизација фајлова преко WebDAV.",
+ "Migrated calendar (%1$s)" : "Мигрирани календар (%1$s)",
+ "Calendars including events, details and attendees" : "Календари који укључују догађаје, детаље и учеснике",
+ "Contacts and groups" : "Контакти и групе",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "Одсутност је сачувана",
+ "Failed to save your absence settings" : "Није успело чување ваших поставки одсутности",
+ "Absence cleared" : "Одсутност је обрисана",
+ "Failed to clear your absence settings" : "Није успело брисање ваших поставки одсутности",
+ "First day" : "Први дан",
+ "Last day (inclusive)" : "Последњи дан (укључујући и њега)",
+ "Out of office replacement (optional)" : "Замена за ван канцеларије (није обавезно)",
+ "Name of the replacement" : "Име замене",
+ "No results." : "Нема резултата.",
+ "Start typing." : "Почните да куцате",
+ "Short absence status" : "Кратак статус одсуства",
+ "Long absence Message" : "Дугачак статус одсуства",
"Save" : "Сачувај",
+ "Disable absence" : "Искључи одсутност",
+ "Failed to load availability" : "Доступност није могла да се учита",
+ "Saved availability" : "Доступност је сачувана",
+ "Failed to save availability" : "Није успело снимање доступности ",
+ "Time zone:" : "Временска зона:",
+ "to" : "за",
+ "Delete slot" : "Обриши прорез",
+ "No working hours set" : "Нису подешени радни сати",
+ "Add slot" : "Додај термин",
+ "Weekdays" : "Дани у недељи",
+ "Pick a start time for {dayName}" : "Изаберите време почетка за {dayName}",
+ "Pick a end time for {dayName}" : "Изаберите време завршетка за {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Аутоматски поставља статус кориниска на „Не узнемиравај” како би се ван доступности пригушила сва обавештења.",
+ "Cancel" : "Откажи",
+ "Import" : "Увоз",
+ "Error while saving settings" : "Грешка приликом чувања подешавања",
+ "Contact reset successfully" : "Контакт је успешно ресетован",
+ "Error while resetting contact" : "Грешка приликом ресетовања контакта",
+ "Contact imported successfully" : "Контакт је успешно увезен",
+ "Error while importing contact" : "Грешка током увоза контакта",
+ "Import contact" : "Увези контакт",
+ "Reset to default" : "Врати на подразумевано",
+ "Import contacts" : "Увези контакте",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Увоз новог .vcf фајла ће да обрише постојећи подразумевани контакт и замениће га са новим. Желите ли да наставите?",
+ "Failed to save example event creation setting" : "Није успело чување подешавања креирања догађаја за пример",
+ "Failed to upload the example event" : "Није успело отпремање догађаја за пример",
+ "Custom example event was saved successfully" : "Произвољни догађај за пример је успешно сачуван",
+ "Failed to delete the custom example event" : "Произвољни догађај за пример није могао да се обрише",
+ "Custom example event was deleted successfully" : "Произвољни догађај за пример је успешно обрисан",
+ "Import calendar event" : "Увоз догађаја календара",
+ "Uploading a new event will overwrite the existing one." : "Отпремање новог догађаја ће да препише постојећи.",
+ "Upload event" : "Отпреми догађај",
+ "Availability" : "Доступност",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Ако подесите своје радне сате, када буду заказивали састанак, други људи ће видети када сте ван канцеларије.",
+ "Absence" : "Одсутност",
+ "Configure your next absence period." : "Конфигуришите свој наредни период одсутности.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Такође инсталирајте {calendarappstoreopen}Календар апликацију{linkclose}, или {calendardocopen}повежите Ваш рачунар & мобилни за синхронизацију ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Постарајте се да правилно подесите {emailopen}сервер е-поште{linkclose}.",
"Calendar server" : "Календар сервера",
"Send invitations to attendees" : "Пошаљи позивницу учесницима",
"Automatically generate a birthday calendar" : "Аутоматски изгенериши календар рођендана",
@@ -104,15 +324,15 @@ OC.L10N.register(
"Hence they will not be available immediately after enabling but will show up after some time." : "Зато можда неће бити видљиви баш одмах по укључивању, али ће се појавити после неког времена.",
"Send notifications for events" : "Шаљи обавештења о догађајима",
"Notifications are sent via background jobs, so these must occur often enough." : "Обавештења се шаљу кроз послове у позадини, па би требало да су постављени да се често извршавају.",
+ "Send reminder notifications to calendar sharees as well" : "Пошаљи подсетнике и корисницима којима је календар подељен",
+ "Reminders are always sent to organizers and attendees." : "Подсетници се увек шаљу организаторима и учесницима.",
"Enable notifications for events via push" : "Укључи обавештења за догађаје преко гурања догађаја",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Такође инсталирајте {calendarappstoreopen}Календар апликацију{linkclose}, или {calendardocopen}повежите Ваш рачунар & мобилни за синхронизацију ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Постарајте се да правилно подесите {emailopen}сервер е-поште{linkclose}.",
+ "Example content" : "Садржај за пример",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Садржај за пример служи да покаже Nextcloud могућности. Подразумевани садржај се испоручује уз Nextcloud и може да се замени произвољним.",
"There was an error updating your attendance status." : "Десила се грешка приликом ажурирања статуса Вашег присуства.",
"Please contact the organizer directly." : "Контактирајте директно организатора.",
"Are you accepting the invitation?" : "Да ли прихватате позивницу?",
"Tentative" : "Условна потврда",
- "Comment" : "Коментар",
- "Your attendance was updated successfully." : "Ваше присуство је успешно ажурирано.",
- "Calendar and tasks" : "Календар и задаци"
+ "Your attendance was updated successfully." : "Ваше присуство је успешно ажурирано."
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);");
diff --git a/apps/dav/l10n/sr.json b/apps/dav/l10n/sr.json
index c4ab6bafb32..ddea6a44542 100644
--- a/apps/dav/l10n/sr.json
+++ b/apps/dav/l10n/sr.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Календар",
- "Todos" : "Подсетници",
+ "Tasks" : "Задаци",
"Personal" : "Лично",
"{actor} created calendar {calendar}" : "{actor} направи календар {calendar}",
"You created calendar {calendar}" : "Направили сте календар {calendar}",
@@ -8,6 +8,8 @@
"You deleted calendar {calendar}" : "Обрисали сте календар {calendar}",
"{actor} updated calendar {calendar}" : "{actor} ажурира календар {calendar}",
"You updated calendar {calendar}" : "Ажурирали сте календар {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} је обновио календар {calendar}",
+ "You restored calendar {calendar}" : "Обновили сте календар {calendar}",
"You shared calendar {calendar} as public link" : "Поделили сте календар {calendar} као јавну везу",
"You removed public link for calendar {calendar}" : "Уклонили сте јавну везу за календар {calendar}",
"{actor} shared calendar {calendar} with you" : "{actor} подели календар {calendar} са вама",
@@ -21,33 +23,41 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} подели календар {calendar} са групом {group}",
"You unshared calendar {calendar} from group {group}" : "Укинули сте дељење календара {calendar} са групом {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} укину дељење календара {calendar} са групом {group}",
+ "Untitled event" : "Неименовани догађај",
"{actor} created event {event} in calendar {calendar}" : "{actor} је направио догађај {event} у календару {calendar}",
"You created event {event} in calendar {calendar}" : "Креирали сте догађај {event} у календару {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} је обрисао догађај {event} из календара {calendar}",
"You deleted event {event} from calendar {calendar}" : "Обрисали сте догађај {event} из календара {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} је ажурирао догађај {event} у календару {calendar}",
"You updated event {event} in calendar {calendar}" : "Ажурирали сте догађај {event} у календару {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} је преместио догађај {event} из календара {sourceCalendar} у календар {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Преместили сте догађај {event} из календара {sourceCalendar} у календар {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} је обновио догађај {event} календара {calendar}",
+ "You restored event {event} of calendar {calendar}" : "Обновили сте догађај {event} календара {calendar}",
"Busy" : "Заузет/а",
- "{actor} created todo {todo} in list {calendar}" : "{actor} је направио подсетник {todo} у листи {calendar}",
- "You created todo {todo} in list {calendar}" : "Креирали сте подсетник {todo} у листи {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} је обрисао подсетник {todo} из листе {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Обрисали сте подсетник {todo} из листе {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} је ажурирао подсетник {todo} у листи {calendar}",
- "You updated todo {todo} in list {calendar}" : "Ажурирали сте подсетник {todo} у листи {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} је обрисао подсетник {todo} из листе {calendar}",
- "You solved todo {todo} in list {calendar}" : "Маркирали сте подсетник {todo} као готов у листи {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} је поново отворио подсетник {todo} у листи {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Поново сте отворили подсетник {todo}  у листи {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} је креирао обавезу {todo} у листи {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Креирали сте обавезу {todo} у листи {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} је обрисао обавезу {todo} из листе {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "обрисали сте обавезу {todo} из листе {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} је ажурирао обавезу {todo} у листи {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Ажурирали сте обавезу {todo} у листи {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} је извршио обавезу {todo} у листи {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Извршили сте обавезу {todo} у листи {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} је поново отворио обавезу {todo} у листи {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Поново сте отворили обавезу {todo} у листи {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} је преместио обавезу {todo} из листе {sourceCalendar} у листу {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Преместили сте обавезу {todo} из листе {sourceCalendar} у листу {targetCalendar}",
+ "Calendar, contacts and tasks" : "Календар, контакти и задаци",
"A <strong>calendar</strong> was modified" : "<strong>Календар</strong> је измењен",
"A calendar <strong>event</strong> was modified" : "<strong>Догађај</strong> из календара је измењен",
- "A calendar <strong>todo</strong> was modified" : "<strong>Подсетник</strong> из календара је измењен",
+ "A calendar <strong>to-do</strong> was modified" : "Календар <strong>обавеза</strong> је измењен",
"Contact birthdays" : "Рођендани контаката",
"Death of %s" : " %s смрт",
+ "Untitled calendar" : "Неименовани календар",
"Calendar:" : "Календар:",
"Date:" : "Датум:",
"Where:" : "Место:",
"Description:" : "Опис:",
- "Untitled event" : "Неименовани догађај",
"_%n year_::_%n years_" : ["%n година","%n године","%n година"],
"_%n month_::_%n months_" : ["%n месец","%n месеца","%n месеци"],
"_%n day_::_%n days_" : ["%n дан","%n дана","%n дана"],
@@ -60,41 +70,251 @@
"Description: %s" : "Опис: %s",
"Where: %s" : "Место: %s",
"%1$s via %2$s" : "%1$s преко %2$s",
- "Invitation canceled" : "Позивница отказана",
- "Invitation updated" : "Позивница ажурирана",
- "Invitation" : "Позивница",
+ "In the past on %1$s for the entire day" : "У прошлости %1$s током целог дана",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["За минут дана %1$s током целог дана","За %n минута дана %1$s током целог дана","За %n минут дана %1$s током целог дана"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["За %n сат у %1$s током целог дана","За %n сата у %1$s током целог дана","За %n сата у %1$s током целог дана"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["За %n дан у %1$s током целог дана","За %n дана у %1$s током целог дана","За %n дана у %1$s током целог дана"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["За %n недељу у %1$s током целог дана","За %n недеље у %1$s током целог дана","За %n недеља у %1$s током целог дана"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["За %n месец у %1$s током целог дана","За %n месеца у %1$s током целог дана","За %n месеци у %1$s током целог дана"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["За %n годину у %1$s током целог дана","За %n године у %1$s током целог дана","За %n година у %1$s током целог дана"],
+ "In the past on %1$s between %2$s - %3$s" : "У прошлости, у %1$s између %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["За %n минут, у %1$s између %2$s - %3$s","За %n минута, у %1$s између %2$s - %3$s","За %n минута, у %1$s између %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["За %n сат, у %1$s између %2$s - %3$s","За %n сата, у %1$s између %2$s - %3$s","За %n сати, у %1$s између %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["За %n дан, у %1$s између %2$s - %3$s","За %n дана, у %1$s између %2$s - %3$s","За %n дана, у %1$s између %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["За %n недељу, у %1$s између %2$s - %3$s","За %n недеље, у %1$s између %2$s - %3$s","За %n недеља, у %1$s између %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["За %n месец, у %1$s између %2$s - %3$s","За %n месеца, у %1$s између %2$s - %3$s","За %n месеци, у %1$s између %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["За %n годину, у %1$s између %2$s - %3$s","За %n године, у %1$s између %2$s - %3$s","За %n година, у %1$s између %2$s - %3$s"],
+ "Could not generate when statement" : "Не може да се генерише одредба када",
+ "Every Day for the entire day" : "Сваки дан током целог дана",
+ "Every Day for the entire day until %1$s" : "Сваки дан током целог дана, све до %1$s",
+ "Every Day between %1$s - %2$s" : "Сваки дан од %1$s до %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Сваки дан од %1$s до %2$s све до %3$s",
+ "Every %1$d Days for the entire day" : "Сваких %1$d дана током целог дана",
+ "Every %1$d Days for the entire day until %2$s" : "Сваких %1$d дана током целог дана све до %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Сваких %1$d дана од %2$s до %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Сваких %1$d дана од %2$s до %3$s све до %4$s",
+ "Could not generate event recurrence statement" : "Не може да се генерише одредба понављања",
+ "Every Week on %1$s for the entire day" : "%1$s сваке недеље током целог дана",
+ "Every Week on %1$s for the entire day until %2$s" : "%1$s сваке недеље током целог дана све до %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "%1$s сваке недеље од %2$s до %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "%1$s сваке недеље од %2$s до %3$s све до %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "%2$s сваких %1$d недеља током целог дана",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "%2$s сваких %1$d недеља током целог дана све до %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "%2$s сваких %1$d недеља од %3$s до %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "%2$s сваких %1$d недеља од %3$s до %4$s све до %5$s",
+ "Every Month on the %1$s for the entire day" : "%1$s сваког месеца током целог дана",
+ "Every Month on the %1$s for the entire day until %2$s" : "%1$s сваког месеца током целог дана све до %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "%1$s сваког месеца од %2$s до %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "%1$s сваког месеца од %2$s до %3$s све до %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "%2$s сваких %1$d месеци током целог дана",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "%2$s сваких %1$d месеци током целог дана све до %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "%2$s сваких %1$d месеци од %3$s до %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "%2$s сваких %1$d месеци од %3$s до %4$s све до %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "%1$s сваке године дана %2$s током целог дана",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "%1$s сваке године, дана %2$s током целог дана све до %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "%1$s сваке године, дана %2$s од %3$s до %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "%1$s сваке године, дана %2$s од %3$s до %4$s све до %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "%2$s сваких %1$d година, дана %3$s током целог дана",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "%2$s сваких %1$d година, дана %3$s током целог дана све до %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "%2$s сваких %1$d година, дана %3$s од %4$s до %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "%2$s сваких %1$d година, дана %3$s од %4$s до %5$s све до %6$s",
+ "On specific dates for the entire day until %1$s" : "Одређених дана током целог дана, све до %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "Одређених дана од %1$s до %2$s све до %3$s",
+ "In the past on %1$s" : "У прошлости, у %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["За %n минут, у %1$s","За %n минута, у %1$s","За %n минута, у %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["За %n сат, у %1$s","За %n сата, у %1$s","За %n сати, у %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["За %n дан, у %1$s","За %n дана, у %1$s","За %n дана, у %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["За %n недељу, у %1$s","За %n недеље, у %1$s","За %n недеља, у %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["За %n месец, у %1$s","За %n месеца, у %1$s","За %n месеци, у %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["За %n годину, у %1$s","За %n године, у %1$s","За %n година, у %1$s"],
+ "In the past on %1$s then on %2$s" : "У прошлости, у %1$s па онда у %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["За %n минут, у %1$s па онда у %2$s","За %n минута, у %1$s па онда у %2$s","За %n минута, у %1$s па онда у %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["За %n сат, у %1$s па онда у %2$s","За %n сата, у %1$s па онда у %2$s","За %n сати, у %1$s па онда у %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["За %n дан, у %1$s па онда у %2$s","За %n дана, у %1$s па онда у %2$s","За %n дана, у %1$s па онда у %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["За %n недељу, у %1$s па онда у %2$s","За %n недеље, у %1$s па онда у %2$s","За %n недеља, у %1$s па онда у %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["За %n месец, у %1$s па онда у %2$s","За %n месеца, у %1$s па онда у %2$s","За %n месеци, у %1$s па онда у %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["За %n годину, у %1$s па онда у %2$s","За %n године, у %1$s па онда у %2$s","За %n година, у %1$s па онда у %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "У прошлости, у %1$s па онда у %2$s и %3$s",
+ "_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_" : ["За %n минут, у %1$s па онда у %2$s и %3$s","За %n минута, у %1$s па онда у %2$s и %3$s","За %n минута, у %1$s па онда у %2$s и %3$s"],
+ "_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_" : ["За %n сат, у %1$s па онда у %2$s и %3$s","За %n сата, у %1$s па онда у %2$s и %3$s","За %n сати, у %1$s па онда у %2$s и %3$s"],
+ "_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_" : ["За %n дан, у %1$s па онда у %2$s и %3$s","За %n дана, у %1$s па онда у %2$s и %3$s","За %n дана, у %1$s па онда у %2$s и %3$s"],
+ "_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_" : ["За %n недељу, у %1$s па онда у %2$s и %3$s","За %n недеље, у %1$s па онда у %2$s и %3$s","За %n недеља, у %1$s па онда у %2$s и %3$s"],
+ "_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_" : ["За %n месец, у %1$s па онда у %2$s и %3$s","За %n месеца, у %1$s па онда у %2$s и %3$s","За %n месеци, у %1$s па онда у %2$s и %3$s"],
+ "_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_" : ["За %n годину, у %1$s па онда у %2$s и %3$s","За %n године, у %1$s па онда у %2$s и %3$s","За %n година, у %1$s па онда у %2$s и %3$s"],
+ "Could not generate next recurrence statement" : "Није могла да се генерише одредба следћег појављивања",
+ "Cancelled: %1$s" : "Отказано: %1$s",
+ "\"%1$s\" has been canceled" : "„%1$s” је отказано",
+ "Re: %1$s" : "Одг: %1$s",
+ "%1$s has accepted your invitation" : "%1$s је прихватио вашу позивницу",
+ "%1$s has tentatively accepted your invitation" : "%1$s је условно прихватио вашу позивницу",
+ "%1$s has declined your invitation" : "%1$s је одбио вашу позивницу",
+ "%1$s has responded to your invitation" : "%1$s је одговорио на вашу позивницу",
+ "Invitation updated: %1$s" : "Позивница је ажурирана: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s је ажурирао догађај „%2$s”",
+ "Invitation: %1$s" : "Позивница: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s жели да вас позове на „%2$s",
+ "Organizer:" : "Организатор:",
+ "Attendees:" : "Присутни:",
"Title:" : "Наслов:",
- "Time:" : "Време:",
+ "When:" : "Време:",
"Location:" : "Локација:",
"Link:" : "Веза:",
- "Organizer:" : "Организатор:",
- "Attendees:" : "Присутни:",
+ "Occurring:" : "Појављивање:",
"Accept" : "Прихвати",
"Decline" : "Одбиј",
"More options …" : "Још опција…",
"More options at %s" : "Још опција на %s",
+ "Monday" : "Понедељак",
+ "Tuesday" : "Уторак",
+ "Wednesday" : "Среда",
+ "Thursday" : "Четвртак",
+ "Friday" : "Петак",
+ "Saturday" : "Субота",
+ "Sunday" : "Недеља",
+ "January" : "Јануар",
+ "February" : "Фебруар",
+ "March" : "Март",
+ "April" : "Април",
+ "May" : "Мај",
+ "June" : "Јун",
+ "July" : "Јул",
+ "August" : "Август",
+ "September" : "Септембар",
+ "October" : "Октобар",
+ "November" : "Новембар",
+ "December" : "Децембар",
+ "First" : "Прва",
+ "Second" : "Друго",
+ "Third" : "Треће",
+ "Fourth" : "Четврто",
+ "Fifth" : "Пето",
+ "Last" : "Последња",
+ "Second Last" : "Претпоследње",
+ "Third Last" : "Треће од краја",
+ "Fourth Last" : "Четврто од краја",
+ "Fifth Last" : "Пето од краја",
"Contacts" : "Контакти",
+ "{actor} created address book {addressbook}" : "{actor} је креирао адресар {addressbook}",
+ "You created address book {addressbook}" : "Креирали сте адресар {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} је обрисао адресар {addressbook}",
+ "You deleted address book {addressbook}" : "Обрисали сте адресар {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} је ажурирао адресар {addressbook}",
+ "You updated address book {addressbook}" : "Ажурирали сте адресар {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} је са вама поделио адресар {addressbook}",
+ "You shared address book {addressbook} with {user}" : "Поделили сте адресар {addressbook} са {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} је поделио адресар {addressbook} са {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} је уклонио дељење адресара {addressbook} са вама",
+ "You unshared address book {addressbook} from {user}" : "Уклонили сте дељење адресара {addressbook} са {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} је уклонио дељење адресара {addressbook} са {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} више не дели адресар {addressbook} са собом",
+ "You shared address book {addressbook} with group {group}" : "Поделили сте адресар {addressbook} са групом {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} је поделио адресар {addressbook} са групом {group}",
+ "You unshared address book {addressbook} from group {group}" : "Уклонили сте дељење адресара {addressbook} са групом {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} је уклонио дељење адресара {addressbook} са групом {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} је креирао контакт {card} у адресару {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Креирали сте контакт {card} у адресару {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} је обрисао контакт {card} из адресара {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Обрисали сте контакт {card} из адресара {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} је ажурирао контакт {card} у адресару {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Ажурирали сте контакт {card} у адресару {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Измењен је <strong>контакт</strong> или <strong>адресар</strong>",
+ "Accounts" : "Налози",
+ "System address book which holds all accounts" : "Системски адресар у којем се налазе сви налози",
+ "File is not updatable: %1$s" : "Фајл не може да се ажурира: %1$s",
+ "Failed to get storage for file" : "Није успело добијање складишта за фајл",
+ "Could not write to final file, canceled by hook" : "Не може да се упише у крајњи фајл, отказала је кука",
+ "Could not write file contents" : "Не може да се упише садржај фајла",
+ "_%n byte_::_%n bytes_" : ["%n бајт","%n бајта","%n бајтова"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Грешка приликом копирања фајла на циљну локацију (копирано: %1$s, очекивана величина фајла: %2$s)",
+ "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." : "Очекивала се величина фајла %1$s, али је (од Nextcloud клијента) прочитано и уписано (у Nextcloud складиште) %2$s. Или може бити мрежни проблем на страни која шаље, или проблем код уписа у складиште на серверу.",
+ "Could not rename part file to final file, canceled by hook" : "Делимични фајл не може да се преименује у коначни фајл, отказала је кука",
+ "Could not rename part file to final file" : "Делимични фајл не може да се преименује у коначни фајл",
+ "Failed to check file size: %1$s" : "Није успела провера величине фајла: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "Фајл не може да се отвори: %1$s, изгледа да фајл постоји",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Фајл не може да се отвори: %1$s, изгледа да фајл не постоји",
+ "Encryption not ready: %1$s" : "Шифрирање није спремно: %1$s",
+ "Failed to open file: %1$s" : "Фајл не може да се отвори: %1$s",
+ "Failed to unlink: %1$s" : "Није успело уклањање линка: %1$s",
+ "Failed to write file contents: %1$s" : "Није успело уписивање садржаја фајла: %1$s",
+ "File not found: %1$s" : "Фајл не може да се пронађе: %1$s",
+ "Invalid target path" : "Неисправна путања циља",
+ "System is in maintenance mode." : "Систем је у режиму одржавања.",
"Upgrade needed" : "Потребна надградња",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "%s мора да буде подешен да користи HTTPS да бисте користи CalDAV и CardDAV са iOS/macOS-ом.",
"Configures a CalDAV account" : "Подешава CalDAV налог",
"Configures a CardDAV account" : "Подешава CardDAV налог",
"Events" : "Догађаји",
- "Tasks" : "Задаци",
"Untitled task" : "Неименовани задатак",
"Completed on %s" : "Завршено %s",
"Due on %s by %s" : "Рок је %s од стране %s",
"Due on %s" : "Рок је %s",
- "WebDAV" : "ВебДАВ",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Добро дошли у Nextcloud Календар!\n\nОво је догађај за пример - истражите флексибилност планирања са Nextcloud Календаром тако што ћете направите било какве измене!\n\nСа Nextcloud Календаром, можете да:\n- Креирате, уређујете и управљате догађајима без муке.\n- Креирате више календара и делите их са члановима тима, пријатељима или породицом.\n- Проверавате доступност и осталима приказујете време у које сте заузети.\n- Лако интегришете са осталим апликацијама и уређајима помоћу CalDAV.\n- Прилагодите своје искуство: правите распоред догађаја који се понављају, подешавате обавештења и остале поставке.",
+ "Example event - open me!" : "Догађај за пример - отвори ме!",
+ "System Address Book" : "Системски адресар",
+ "The system address book contains contact information for all users in your instance." : "Системски адресар садржи информације о свим корисницима на вашој инстанци.",
+ "Enable System Address Book" : "Укључи системски адресар",
+ "DAV system address book" : "DAV системски адресар",
+ "No outstanding DAV system address book sync." : "Не постоји ниједна синхронизација DAV системског адресара која треба да се обави.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV синхронизација системског адресара се још увек није покренула јер ваша инстанца има више од 1000 корисника или јер је дошло до грешке. Молимо вас да га ручно покренете командом „occ dav:sync-system-addressbook”.",
"WebDAV endpoint" : "WebDAV крајња тачка",
- "to" : "за",
- "Monday" : "Понедељак",
- "Tuesday" : "Уторак",
- "Wednesday" : "Среда",
- "Thursday" : "Четвртак",
- "Friday" : "Петак",
- "Saturday" : "Субота",
- "Sunday" : "Недеља",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Није могло да се провери да ли ваш веб сервер исправно подешен тако да се омогући синхронизација фајлова преко WebDAV. Молимо вас да проверите ручно.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Сервер није правилно подешен за синхронизацију фајлова. Изгледа да је ВебДАВ сучеље покварено.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Ваш веб сервер исправно подешен тако да се омогући синхронизација фајлова преко WebDAV.",
+ "Migrated calendar (%1$s)" : "Мигрирани календар (%1$s)",
+ "Calendars including events, details and attendees" : "Календари који укључују догађаје, детаље и учеснике",
+ "Contacts and groups" : "Контакти и групе",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "Одсутност је сачувана",
+ "Failed to save your absence settings" : "Није успело чување ваших поставки одсутности",
+ "Absence cleared" : "Одсутност је обрисана",
+ "Failed to clear your absence settings" : "Није успело брисање ваших поставки одсутности",
+ "First day" : "Први дан",
+ "Last day (inclusive)" : "Последњи дан (укључујући и њега)",
+ "Out of office replacement (optional)" : "Замена за ван канцеларије (није обавезно)",
+ "Name of the replacement" : "Име замене",
+ "No results." : "Нема резултата.",
+ "Start typing." : "Почните да куцате",
+ "Short absence status" : "Кратак статус одсуства",
+ "Long absence Message" : "Дугачак статус одсуства",
"Save" : "Сачувај",
+ "Disable absence" : "Искључи одсутност",
+ "Failed to load availability" : "Доступност није могла да се учита",
+ "Saved availability" : "Доступност је сачувана",
+ "Failed to save availability" : "Није успело снимање доступности ",
+ "Time zone:" : "Временска зона:",
+ "to" : "за",
+ "Delete slot" : "Обриши прорез",
+ "No working hours set" : "Нису подешени радни сати",
+ "Add slot" : "Додај термин",
+ "Weekdays" : "Дани у недељи",
+ "Pick a start time for {dayName}" : "Изаберите време почетка за {dayName}",
+ "Pick a end time for {dayName}" : "Изаберите време завршетка за {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Аутоматски поставља статус кориниска на „Не узнемиравај” како би се ван доступности пригушила сва обавештења.",
+ "Cancel" : "Откажи",
+ "Import" : "Увоз",
+ "Error while saving settings" : "Грешка приликом чувања подешавања",
+ "Contact reset successfully" : "Контакт је успешно ресетован",
+ "Error while resetting contact" : "Грешка приликом ресетовања контакта",
+ "Contact imported successfully" : "Контакт је успешно увезен",
+ "Error while importing contact" : "Грешка током увоза контакта",
+ "Import contact" : "Увези контакт",
+ "Reset to default" : "Врати на подразумевано",
+ "Import contacts" : "Увези контакте",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Увоз новог .vcf фајла ће да обрише постојећи подразумевани контакт и замениће га са новим. Желите ли да наставите?",
+ "Failed to save example event creation setting" : "Није успело чување подешавања креирања догађаја за пример",
+ "Failed to upload the example event" : "Није успело отпремање догађаја за пример",
+ "Custom example event was saved successfully" : "Произвољни догађај за пример је успешно сачуван",
+ "Failed to delete the custom example event" : "Произвољни догађај за пример није могао да се обрише",
+ "Custom example event was deleted successfully" : "Произвољни догађај за пример је успешно обрисан",
+ "Import calendar event" : "Увоз догађаја календара",
+ "Uploading a new event will overwrite the existing one." : "Отпремање новог догађаја ће да препише постојећи.",
+ "Upload event" : "Отпреми догађај",
+ "Availability" : "Доступност",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Ако подесите своје радне сате, када буду заказивали састанак, други људи ће видети када сте ван канцеларије.",
+ "Absence" : "Одсутност",
+ "Configure your next absence period." : "Конфигуришите свој наредни период одсутности.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Такође инсталирајте {calendarappstoreopen}Календар апликацију{linkclose}, или {calendardocopen}повежите Ваш рачунар & мобилни за синхронизацију ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Постарајте се да правилно подесите {emailopen}сервер е-поште{linkclose}.",
"Calendar server" : "Календар сервера",
"Send invitations to attendees" : "Пошаљи позивницу учесницима",
"Automatically generate a birthday calendar" : "Аутоматски изгенериши календар рођендана",
@@ -102,15 +322,15 @@
"Hence they will not be available immediately after enabling but will show up after some time." : "Зато можда неће бити видљиви баш одмах по укључивању, али ће се појавити после неког времена.",
"Send notifications for events" : "Шаљи обавештења о догађајима",
"Notifications are sent via background jobs, so these must occur often enough." : "Обавештења се шаљу кроз послове у позадини, па би требало да су постављени да се често извршавају.",
+ "Send reminder notifications to calendar sharees as well" : "Пошаљи подсетнике и корисницима којима је календар подељен",
+ "Reminders are always sent to organizers and attendees." : "Подсетници се увек шаљу организаторима и учесницима.",
"Enable notifications for events via push" : "Укључи обавештења за догађаје преко гурања догађаја",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Такође инсталирајте {calendarappstoreopen}Календар апликацију{linkclose}, или {calendardocopen}повежите Ваш рачунар & мобилни за синхронизацију ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Постарајте се да правилно подесите {emailopen}сервер е-поште{linkclose}.",
+ "Example content" : "Садржај за пример",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Садржај за пример служи да покаже Nextcloud могућности. Подразумевани садржај се испоручује уз Nextcloud и може да се замени произвољним.",
"There was an error updating your attendance status." : "Десила се грешка приликом ажурирања статуса Вашег присуства.",
"Please contact the organizer directly." : "Контактирајте директно организатора.",
"Are you accepting the invitation?" : "Да ли прихватате позивницу?",
"Tentative" : "Условна потврда",
- "Comment" : "Коментар",
- "Your attendance was updated successfully." : "Ваше присуство је успешно ажурирано.",
- "Calendar and tasks" : "Календар и задаци"
+ "Your attendance was updated successfully." : "Ваше присуство је успешно ажурирано."
},"pluralForm" :"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
} \ No newline at end of file
diff --git a/apps/dav/l10n/sv.js b/apps/dav/l10n/sv.js
index 43a2af0ca08..62597fdf138 100644
--- a/apps/dav/l10n/sv.js
+++ b/apps/dav/l10n/sv.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Kalender",
- "Todos" : "Uppgifter",
+ "Tasks" : "Uppgifter",
"Personal" : "Privat",
"{actor} created calendar {calendar}" : "{actor} skapade kalender {calendar}",
"You created calendar {calendar}" : "Du skapade kalender {calendar}",
@@ -25,36 +25,41 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} delade kalender {calendar} med grupp {group}",
"You unshared calendar {calendar} from group {group}" : "Du slutade dela kalender {calendar} med gruppen {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} slutade dela kalender {calendar} med gruppen {group}",
+ "Untitled event" : "Namnlös händelse",
"{actor} created event {event} in calendar {calendar}" : "{actor} skapade händelse {event} i kalender {calendar}",
"You created event {event} in calendar {calendar}" : "Du skapade händelse {event} i kalender {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} tog bort händelse {event} från kalender {calendar}",
"You deleted event {event} from calendar {calendar}" : "Du tog bort händelse {event} från kalender {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} uppdaterade händelse {event} i kalender {calendar}",
"You updated event {event} in calendar {calendar}" : "Du uppdaterade händelse {event} i kalender {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} flyttade händelse {event} från kalender {sourceCalendar} till kalender {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Du flyttade händelse {event} från kalender {sourceCalendar} till kalender {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} återställde händelsen {event} i kalendern {calendar}",
"You restored event {event} of calendar {calendar}" : "Du återställde händelsen {event} i kalendern {calendar}",
"Busy" : "Upptagen",
- "{actor} created todo {todo} in list {calendar}" : "{actor} skapade uppgift {todo} i listan {calendar}",
- "You created todo {todo} in list {calendar}" : "Du skapade uppgift {todo} i listan {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} tog bort uppgift {todo} från listan {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Du tog bort uppgift {todo} från listan {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} uppdaterade uppgift {todo} i listan {calendar}",
- "You updated todo {todo} in list {calendar}" : "Du uppdaterade uppgift {todo} i listan {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} löste uppgift {todo} i listan {calendar}",
- "You solved todo {todo} in list {calendar}" : "Du löste uppgift {todo} i listan {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} återupptog uppgift {todo} i listan {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Du återupptog uppgift {todo} i listan {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} skapade uppgift {todo} i listan {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Du skapade uppgift {todo} i listan {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} tog bort uppgift {todo} från listan {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Du tog bort uppgift {todo} från listan {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} uppdaterade uppgift {todo} i listan {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Du uppdaterade uppgift {todo} i listan {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} löste uppgift {todo} i listan {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Du löste uppgift {todo} i listan {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} återupptog uppgift {todo} i listan {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Du återupptog uppgift {todo} i listan {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} flyttade uppgift {todo} från lista {sourceCalendar} till lista {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Du flyttade uppgift {todo} från lista {sourceCalendar} till lista {targetCalendar}",
"Calendar, contacts and tasks" : "Kalender, kontakter och uppgifter",
- "A <strong>calendar</strong> was modified" : "En <strong>kalender</strong> modifierades",
- "A calendar <strong>event</strong> was modified" : "En kalender-<strong>händelse</strong> modifierades",
- "A calendar <strong>todo</strong> was modified" : "En kalender <strong>uppgift</strong> modifierades",
+ "A <strong>calendar</strong> was modified" : "En <strong>kalender</strong> ändrades",
+ "A calendar <strong>event</strong> was modified" : "En kalender-<strong>händelse</strong> ändrades",
+ "A calendar <strong>to-do</strong> was modified" : "En kalender <strong>uppgift</strong> ändrades",
"Contact birthdays" : "Födelsedagar",
- "Death of %s" : "Död av %s",
+ "Death of %s" : "%ss död ",
+ "Untitled calendar" : "Namnlös kalender",
"Calendar:" : "Kalender:",
"Date:" : "Datum:",
"Where:" : "Var:",
"Description:" : "Beskrivning:",
- "Untitled event" : "Namnlös händelse",
"_%n year_::_%n years_" : ["%n år","%n år"],
"_%n month_::_%n months_" : ["%n månad","%n månader"],
"_%n day_::_%n days_" : ["%n dag","%n dagar"],
@@ -67,22 +72,129 @@ OC.L10N.register(
"Description: %s" : "Beskrivning: %s",
"Where: %s" : "Var: %s",
"%1$s via %2$s" : "%1$s via %2$s",
+ "In the past on %1$s for the entire day" : "Tidigare den %1$s hela dagen",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Om en minut den %1$s hela dagen","Om %n minuter den %1$s hela dagen"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Om en timme den %1$s hela dagen","Om %n timmar den %1$s hela dagen"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Om en dag den %1$s hela dagen","Om %n dagar den %1$s hela dagen"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Om en vecka den %1$s hela dagen","Om %n veckor den %1$s hela dagen"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Om en månad den %1$s hela dagen","Om %n månader den %1$s hela dagen"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Om en år den %1$s hela dagen","Om %n år den %1$s hela dagen"],
+ "In the past on %1$s between %2$s - %3$s" : "Tidigare den %1$s mellan %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Om en minut den %1$s mellan %2$s - %3$s","Om %n minuter den %1$s mellan %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Om en timme den %1$s mellan %2$s - %3$s","Om %n timmar den %1$s mellan %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Om en dag den %1$s mellan %2$s - %3$s","Om %n dagar den %1$s mellan %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Om en vecka den %1$s mellan %2$s - %3$s","Om %n veckor den %1$s mellan %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Om en månad den %1$s mellan %2$s - %3$s","Om %n månader den %1$s mellan %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Om ett år den %1$s mellan %2$s - %3$s","Om %n år den %1$s mellan %2$s - %3$s"],
+ "Could not generate when statement" : "Kunde inte generera ett när-villkor",
+ "Every Day for the entire day" : "Varje dag hela dagen",
+ "Every Day for the entire day until %1$s" : "Varje dag hela dagen tills den %1$s",
+ "Every Day between %1$s - %2$s" : "Varje dag mellan %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Varje dag mellan %1$s - %2$s tills den %3$s",
+ "Every %1$d Days for the entire day" : "Varje %1$d dagar, hela dagen",
+ "Every %1$d Days for the entire day until %2$s" : "Varje %1$d dagar hela dagen tills den %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Varje %1$d dagar mellan %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Varje %1$d dagar mellan %2$s - %3$s tills den %4$s",
+ "Could not generate event recurrence statement" : "Kunde inte generera händelseupprepning",
+ "Every Week on %1$s for the entire day" : "Varje vecka på %1$s, hela dagen",
+ "Every Week on %1$s for the entire day until %2$s" : "Varje vecka på %1$s hela dagen tills den %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Varje vecka på %1$s mellan %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Varje vecka på %1$s mellan %2$s - %3$s till den %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Varje %1$d vecka på %2$s hela dagen",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Varje %1$d vecka på %2$s för hela dagen till %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Varje %1$d vecka på %2$s mellan %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Varje %1$d vecka på %2$s mellan %3$s - %4$s till %5$s",
+ "Every Month on the %1$s for the entire day" : "Varje månad på den %1$s hela dagen",
+ "Every Month on the %1$s for the entire day until %2$s" : "Varje månad på den %1$s hela dagen till %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Varje månad på den %1$s mellan %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Varje månad på den %1$s mellan %2$s - %3$s till %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Varje %1$d månad på den %2$s hela dagen",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Varje %1$d månad på den %2$s hela dagen till %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Varje %1$d månad på den %2$s mellan %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Varje %1$d månad på den %2$s mellan %3$s - %4$s till %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Varje år %1$s på den %2$s hela dagen",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Varje år i %1$s på den %2$s hela dagen till %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Varje år i %1$s på den %2$s mellan %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Varje år i %1$s på den %2$s mellan %3$s - %4$s till %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Vart %1$d år i %2$s på den %3$s hela dagen",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Vart %1$d år i %2$s på den %3$s hela dagen till %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Vart %1$d år i %2$s på den %3$s mellan %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Vart %1$d år i %2$s på den %3$s mellan %4$s - %5$s till %6$s",
+ "On specific dates for the entire day until %1$s" : "På specifika datum hela dagen fram till kl %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "På specifika datum mellan %1$s - %2$s tills %3$s",
+ "In the past on %1$s" : "Tidigare den %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Om en minut den %1$s","Om %n minuter den %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Om en timme den %1$s","Om %n timmar den %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Om en dag den %1$s","Om %n dagar den %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Om en vecka den %1$s","Om %n veckor den %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Om en månad den %1$s","Om %n månader den %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Om ett år den %1$s","Om %n år den %1$s"],
+ "In the past on %1$s then on %2$s" : "Tidigare den %1$s och sedan den %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Om en minu den %1$s och sedan den %2$s","Om %n minuter den %1$s och sedan den %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Om en timme den %1$s och sedan den %2$s","Om %n timmar den %1$s och sedan den %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Om en dag den %1$s och sedan den %2$s","Om %n dagar den %1$s och sedan den %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Om en vecka den %1$s och sedan den %2$s","Om %n veckor den %1$s och sedan den %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Om en månad den %1$s och sedan den %2$s","Om %n månader den %1$s och sedan den %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Om ett år den %1$s och sedan den %2$s","Om %n år den %1$s och sedan den %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "Tidigare den %1$s sedan den %2$s och %3$s",
+ "_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_" : ["Om en minut den %1$s sedan den %2$s och %3$s","Om %n minuter den %1$s sedan den %2$s och %3$s"],
+ "_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_" : ["Om en timme den %1$s sedan den %2$s och %3$s","Om %n timmar den %1$s sedan den %2$s och %3$s"],
+ "_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_" : ["Om en dag den %1$s sedan den %2$s och %3$s","Om %n dagar den %1$s sedan den %2$s och %3$s"],
+ "_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_" : ["Om en vecka den %1$s sedan den %2$s och %3$s","Om %n veckor den %1$s sedan den %2$s och %3$s"],
+ "_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_" : ["Om en månad den %1$s sedan den %2$s och %3$s","Om %n månader den %1$s sedan den %2$s och %3$s"],
+ "_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_" : ["Om ett år den %1$s sedan den %2$s och %3$s","Om %n år den %1$s sedan den %2$s och %3$s"],
+ "Could not generate next recurrence statement" : "Kunde inte generera nästa händelseupprepning",
"Cancelled: %1$s" : "Avbruten: %1$s",
- "Invitation canceled" : "Inbjudan avbruten",
+ "\"%1$s\" has been canceled" : "\"%1$s\" har avbrutits",
"Re: %1$s" : "Sv: %1$s",
- "Invitation updated" : "Inbjudan uppdaterad",
+ "%1$s has accepted your invitation" : "%1$s har accepterat din inbjudan",
+ "%1$s has tentatively accepted your invitation" : "%1$s har preliminärt accepterat din inbjudan",
+ "%1$s has declined your invitation" : "%1$s har tackat nej till din inbjudan",
+ "%1$s has responded to your invitation" : "%1$s har svarat på din inbjudan",
+ "Invitation updated: %1$s" : "Inbjudan uppdaterad: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s uppdaterade händelse \"%2$s\"",
"Invitation: %1$s" : "Inbjudan: %1$s",
- "Invitation" : "Inbjudan",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s har bjudit in dig till \"%2$s\"",
+ "Organizer:" : "Arrangör:",
+ "Attendees:" : "Deltagare:",
"Title:" : "Titel:",
- "Time:" : "Tid:",
+ "When:" : "När",
"Location:" : "Ort:",
"Link:" : "Länk:",
- "Organizer:" : "Arrangör:",
- "Attendees:" : "Deltagare:",
+ "Occurring:" : "Förekommer:",
"Accept" : "Acceptera",
"Decline" : "Avböj",
"More options …" : "Fler alternativ ...",
"More options at %s" : "Fler alternativ på %s",
+ "Monday" : "Måndag",
+ "Tuesday" : "Tisdag",
+ "Wednesday" : "Onsdag",
+ "Thursday" : "Torsdag",
+ "Friday" : "Fredag",
+ "Saturday" : "Lördag",
+ "Sunday" : "Söndag",
+ "January" : "Januari",
+ "February" : "Februari",
+ "March" : "Mars",
+ "April" : "April",
+ "May" : "Maj",
+ "June" : "Juni",
+ "July" : "Juli",
+ "August" : "Augusti",
+ "September" : "September",
+ "October" : "Oktober",
+ "November" : "November",
+ "December" : "December",
+ "First" : "Första",
+ "Second" : "Andra",
+ "Third" : "Tredje",
+ "Fourth" : "Fjärde",
+ "Fifth" : "Femte",
+ "Last" : "Sista",
+ "Second Last" : "Näst sist",
+ "Third Last" : "Tredje sist",
+ "Fourth Last" : "Fjärde sist",
+ "Fifth Last" : "Femte sista",
"Contacts" : "Kontakter",
"{actor} created address book {addressbook}" : "{actor} skapade adressboken {addressbook}",
"You created address book {addressbook}" : "Du skapade adressboken {addressbook}",
@@ -104,54 +216,122 @@ OC.L10N.register(
"{actor} created contact {card} in address book {addressbook}" : "{actor} skapade kontakten {card} i adressboken {addressbook}",
"You created contact {card} in address book {addressbook}" : "Du skapade kontakten {card} i adressboken {addressbook}",
"{actor} deleted contact {card} from address book {addressbook}" : "{actor} tog bort kontakten {card} från adressboken {addressbook}",
- "You deleted contact {card} from address book {addressbook}" : "Du tog bort kontakten {©ard} från adressboken {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Du tog bort kontakten {card} från adressboken {addressbook}",
"{actor} updated contact {card} in address book {addressbook}" : "{actor} uppdaterade kontakten {card} i adressboken {addressbook}",
"You updated contact {card} in address book {addressbook}" : "Du uppdaterade kontakten {card} i adressboken {addressbook}",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "En <strong>kontakt</strong> eller <strong>adressbok</strong> ändrades",
+ "Accounts" : "Konton",
+ "System address book which holds all accounts" : "Systemadressbok som innehåller alla konton",
+ "File is not updatable: %1$s" : "Fil kan inte uppdateras: %1$s",
+ "Failed to get storage for file" : "Kunde inte få lagringsutrymme för filen",
+ "Could not write to final file, canceled by hook" : "Kunde ej skriva till den slutgiltiga filen, avbröts av en kopplad åtgärd",
+ "Could not write file contents" : "Kunde inte skriva filens innehåll",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Fel vid kopiering av fil till målplats (kopierade: %1$s, förväntad filstorlek: %2$s)",
+ "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." : "Förväntad filstorlek på %1$s men läste (från Nextcloud-klienten) och skrev (till Nextcloud-lagringen) %2$s. Kan antingen vara ett nätverksproblem på sändnings-sidan eller problem med att skriva till lagringen på server-sidan.",
+ "Could not rename part file to final file, canceled by hook" : "Kunde inte byta namn på filfragment till slutgiltigt filnamn, avbröts av en kopplad åtgärd.",
+ "Could not rename part file to final file" : "Kunde inte ändra namn från temporära filen till slutliga filen",
+ "Failed to check file size: %1$s" : "Kunde inte kontrollera filstorleken: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "Kunde inte öppna filen: %1$s, filen verkar inte finnas",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Kunde inte öppna filen: %1$s, filen verkar inte finnas",
+ "Encryption not ready: %1$s" : "Kryptering ej redo: %1$s",
+ "Failed to open file: %1$s" : "Kunde inte öppna fil: %1$s",
+ "Failed to unlink: %1$s" : "Kunde inte ta bort: %1$s",
+ "Failed to write file contents: %1$s" : "Kunde inte skriva filinnehåll: %1$s",
+ "File not found: %1$s" : "Filen hittades inte: %1$s",
+ "Invalid target path" : "Ogiltig målsökväg",
"System is in maintenance mode." : "Systemet är i underhållsläge.",
"Upgrade needed" : "Uppdatering nödvändig",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Din %s måste konfigureras att använda HTTPS för CalDAV och CardDAV med iOS/macOS.",
"Configures a CalDAV account" : "Konfigurerar ett CalDAV-konto",
"Configures a CardDAV account" : "Konfigurerar ett CardDAV-konto",
"Events" : "Händelser",
- "Tasks" : "Uppgifter",
"Untitled task" : "Namnlös uppgift",
"Completed on %s" : "Slutförd %s",
"Due on %s by %s" : "Slutar den %s vid %s",
"Due on %s" : "Slutar den %s",
+ "Example event - open me!" : "Exempelhändelse – öppna mig!",
+ "System Address Book" : "Systemadressbok",
+ "The system address book contains contact information for all users in your instance." : "Systemadressboken innehåller kontaktinformation för alla användare i din instans.",
+ "Enable System Address Book" : "Aktivera systemadressboken",
+ "DAV system address book" : "DAV-systemets adressbok",
+ "No outstanding DAV system address book sync." : "Ingen utestående synkronisering för DAV-systemets adressbok.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV-systemets adressbokssynkronisering har inte körts ännu eftersom din instans har fler än 1000 användare eller för att ett fel uppstod. Kör det manuellt genom att anropa \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "WebDAV endpoint",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Det gick inte att kontrollera att din webbserver är korrekt inställd för att tillåta filsynkronisering över WebDAV. Kontrollera manuellt.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Din webbserver är ännu inte korrekt inställd för att tillåta filsynkronisering, eftersom WebDAV-gränssnittet verkar vara trasigt.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Din webbserver är korrekt inställd för att tillåta filsynkronisering över WebDAV.",
+ "Migrated calendar (%1$s)" : "Migrerade kalender (%1$s)",
+ "Calendars including events, details and attendees" : "Kalendrar inklusive händelser, detaljer och deltagare",
"Contacts and groups" : "Kontakter och grupper",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV endpoint",
- "Availability" : "Tillgänglighet",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Om du konfigurerar dina arbetstimmar kan andra användare se när du inte är på jobbet när de bokar ett möte.",
+ "Absence saved" : "Frånvaro sparad",
+ "Failed to save your absence settings" : "Kunde inte spara dina frånvaroinställningar",
+ "Absence cleared" : "Frånvaro rensad",
+ "Failed to clear your absence settings" : "Kunde inte rensa dina frånvaroinställningar",
+ "First day" : "Första dagen",
+ "Last day (inclusive)" : "Sista dagen (inklusive)",
+ "Out of office replacement (optional)" : "Ersättare utanför kontoret (valfritt)",
+ "Name of the replacement" : "Namn på ersättaren",
+ "No results." : "Inga resultat.",
+ "Start typing." : "Börja skriva.",
+ "Short absence status" : "Meddelande om kort frånvaro",
+ "Long absence Message" : "Meddelande om långvarig frånvaro",
+ "Save" : "Spara",
+ "Disable absence" : "Inaktivera frånvaro",
+ "Failed to load availability" : "Misslyckades med att ladda tidsluckor",
+ "Saved availability" : "Sparade tidslucka",
+ "Failed to save availability" : "Misslyckades med att spara tidslucka",
"Time zone:" : "Tidszon:",
"to" : "till",
+ "Delete slot" : "Radera lucka",
"No working hours set" : "Inga arbetstimmar satta",
- "Monday" : "Måndag",
- "Tuesday" : "Tisdag",
- "Wednesday" : "Onsdag",
- "Thursday" : "Torsdag",
- "Friday" : "Fredag",
- "Saturday" : "Lördag",
- "Sunday" : "Söndag",
- "Save" : "Spara",
+ "Add slot" : "Lägg till lucka",
+ "Weekdays" : "Vardagar",
+ "Pick a start time for {dayName}" : "Välj en starttid för {dayName}",
+ "Pick a end time for {dayName}" : "Välj en sluttid för {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Sätt automatiskt användarstatus till \"Stör ej\" utanför tillgängliga tider för att tysta alla notifikationer.",
+ "Cancel" : "Avbryt",
+ "Import" : "Importera",
+ "Error while saving settings" : "Fel vid sparande av inställningar",
+ "Contact reset successfully" : "Kontakten har återställts",
+ "Error while resetting contact" : "Fel vid återställning av kontakt",
+ "Contact imported successfully" : "Kontakten har importerats",
+ "Error while importing contact" : "Fel vid import av kontakt",
+ "Import contact" : "Importera kontakt",
+ "Reset to default" : "Återställ till grundinställningar",
+ "Import contacts" : "Importera kontakter",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Att importera en ny .vcf-fil kommer att radera den befintliga standardkontakten och ersätta den med den nya. Vill du fortsätta?",
+ "Failed to save example event creation setting" : "Kunde inte spara exemplet för inställning av händelseskapande",
+ "Failed to upload the example event" : "Kunde inte ladda upp exempelhändelsen",
+ "Custom example event was saved successfully" : "Anpassad exempelhändelse sparades",
+ "Failed to delete the custom example event" : "Kunde inte ta bort den anpassade exempelhändelsen",
+ "Custom example event was deleted successfully" : "Anpassad exempelhändelse har raderats",
+ "Import calendar event" : "Importera kalenderhändelse",
+ "Uploading a new event will overwrite the existing one." : "Om du laddar upp en ny händelse kommer den att skriva över den befintliga.",
+ "Upload event" : "Ladda upp händelse",
+ "Availability" : "Din tillgänglighet",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Om du konfigurerar dina arbetstider kommer andra att se när du är frånvarande när de bokar ett möte.",
+ "Absence" : "Frånvaro",
+ "Configure your next absence period." : "Konfigurera din nästa frånvaroperiod.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installera även {calendarappstoreopen}Kalender-app{linkclose}, eller {calendardocopen}anslut din dator & mobil för synkronisering ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Verifiera och säkerställ inställningar för {emailopen}e-postserver{linkclose}.",
"Calendar server" : "Kalenderserver",
"Send invitations to attendees" : "Skicka inbjudan till deltagare",
"Automatically generate a birthday calendar" : "Generera en födelsedagskalender automatiskt",
"Birthday calendars will be generated by a background job." : "Födelsedagskalender kommer skapas som ett bakgrundsjobb.",
"Hence they will not be available immediately after enabling but will show up after some time." : "Därför kommer de inte vara tillgängliga direkt efter aktivering men kommer dyka upp efter en tid.",
- "Send notifications for events" : "Skicka aviseringar för händelser",
- "Notifications are sent via background jobs, so these must occur often enough." : "Aviseringar skickas genom bakgrundsjobb, så dessa måste ske tillräckligt ofta.",
- "Enable notifications for events via push" : "Aktivera aviseringar för händelser via push",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installera även {calendarappstoreopen}Kalender-app{linkclose}, eller {calendardocopen}anslut din dator & mobil för synkronisering ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Verifiera och säkerställ inställningar för {emailopen}e-postserver{linkclose}.",
+ "Send notifications for events" : "Skicka notiser för händelser",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Notiser skickas genom bakgrundsjobb, så dessa måste ske tillräckligt ofta.",
+ "Send reminder notifications to calendar sharees as well" : "Skicka även påminnelser till kalenderdeltagare",
+ "Reminders are always sent to organizers and attendees." : "Påminnelser skickas alltid till arrangörer och deltagare.",
+ "Enable notifications for events via push" : "Aktivera notiser för händelser via push",
+ "Example content" : "Exempelinnehåll",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Exempelinnehåll används för att visa upp funktionerna i Nextcloud. Standardinnehåll levereras med Nextcloud och kan ersättas med anpassat innehåll.",
"There was an error updating your attendance status." : "Ett fel uppstod vid uppdatering av din närvarostatus.",
"Please contact the organizer directly." : "Vänligen kontakta arrangören direkt.",
"Are you accepting the invitation?" : "Acceptera inbjudan?",
"Tentative" : "Preliminärt",
- "Number of guests" : "Antal gäster",
- "Comment" : "Kommentar",
- "Your attendance was updated successfully." : "Dina närvaro uppdaterades.",
- "Calendar and tasks" : "Kalender och uppgifter"
+ "Your attendance was updated successfully." : "Dina närvaro uppdaterades."
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/sv.json b/apps/dav/l10n/sv.json
index a79a9e98a0d..5346062715e 100644
--- a/apps/dav/l10n/sv.json
+++ b/apps/dav/l10n/sv.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Kalender",
- "Todos" : "Uppgifter",
+ "Tasks" : "Uppgifter",
"Personal" : "Privat",
"{actor} created calendar {calendar}" : "{actor} skapade kalender {calendar}",
"You created calendar {calendar}" : "Du skapade kalender {calendar}",
@@ -23,36 +23,41 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} delade kalender {calendar} med grupp {group}",
"You unshared calendar {calendar} from group {group}" : "Du slutade dela kalender {calendar} med gruppen {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} slutade dela kalender {calendar} med gruppen {group}",
+ "Untitled event" : "Namnlös händelse",
"{actor} created event {event} in calendar {calendar}" : "{actor} skapade händelse {event} i kalender {calendar}",
"You created event {event} in calendar {calendar}" : "Du skapade händelse {event} i kalender {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} tog bort händelse {event} från kalender {calendar}",
"You deleted event {event} from calendar {calendar}" : "Du tog bort händelse {event} från kalender {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} uppdaterade händelse {event} i kalender {calendar}",
"You updated event {event} in calendar {calendar}" : "Du uppdaterade händelse {event} i kalender {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} flyttade händelse {event} från kalender {sourceCalendar} till kalender {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Du flyttade händelse {event} från kalender {sourceCalendar} till kalender {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} återställde händelsen {event} i kalendern {calendar}",
"You restored event {event} of calendar {calendar}" : "Du återställde händelsen {event} i kalendern {calendar}",
"Busy" : "Upptagen",
- "{actor} created todo {todo} in list {calendar}" : "{actor} skapade uppgift {todo} i listan {calendar}",
- "You created todo {todo} in list {calendar}" : "Du skapade uppgift {todo} i listan {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} tog bort uppgift {todo} från listan {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Du tog bort uppgift {todo} från listan {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} uppdaterade uppgift {todo} i listan {calendar}",
- "You updated todo {todo} in list {calendar}" : "Du uppdaterade uppgift {todo} i listan {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} löste uppgift {todo} i listan {calendar}",
- "You solved todo {todo} in list {calendar}" : "Du löste uppgift {todo} i listan {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} återupptog uppgift {todo} i listan {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Du återupptog uppgift {todo} i listan {calendar}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} skapade uppgift {todo} i listan {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Du skapade uppgift {todo} i listan {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} tog bort uppgift {todo} från listan {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Du tog bort uppgift {todo} från listan {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} uppdaterade uppgift {todo} i listan {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Du uppdaterade uppgift {todo} i listan {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} löste uppgift {todo} i listan {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Du löste uppgift {todo} i listan {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} återupptog uppgift {todo} i listan {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Du återupptog uppgift {todo} i listan {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} flyttade uppgift {todo} från lista {sourceCalendar} till lista {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Du flyttade uppgift {todo} från lista {sourceCalendar} till lista {targetCalendar}",
"Calendar, contacts and tasks" : "Kalender, kontakter och uppgifter",
- "A <strong>calendar</strong> was modified" : "En <strong>kalender</strong> modifierades",
- "A calendar <strong>event</strong> was modified" : "En kalender-<strong>händelse</strong> modifierades",
- "A calendar <strong>todo</strong> was modified" : "En kalender <strong>uppgift</strong> modifierades",
+ "A <strong>calendar</strong> was modified" : "En <strong>kalender</strong> ändrades",
+ "A calendar <strong>event</strong> was modified" : "En kalender-<strong>händelse</strong> ändrades",
+ "A calendar <strong>to-do</strong> was modified" : "En kalender <strong>uppgift</strong> ändrades",
"Contact birthdays" : "Födelsedagar",
- "Death of %s" : "Död av %s",
+ "Death of %s" : "%ss död ",
+ "Untitled calendar" : "Namnlös kalender",
"Calendar:" : "Kalender:",
"Date:" : "Datum:",
"Where:" : "Var:",
"Description:" : "Beskrivning:",
- "Untitled event" : "Namnlös händelse",
"_%n year_::_%n years_" : ["%n år","%n år"],
"_%n month_::_%n months_" : ["%n månad","%n månader"],
"_%n day_::_%n days_" : ["%n dag","%n dagar"],
@@ -65,22 +70,129 @@
"Description: %s" : "Beskrivning: %s",
"Where: %s" : "Var: %s",
"%1$s via %2$s" : "%1$s via %2$s",
+ "In the past on %1$s for the entire day" : "Tidigare den %1$s hela dagen",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Om en minut den %1$s hela dagen","Om %n minuter den %1$s hela dagen"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Om en timme den %1$s hela dagen","Om %n timmar den %1$s hela dagen"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Om en dag den %1$s hela dagen","Om %n dagar den %1$s hela dagen"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Om en vecka den %1$s hela dagen","Om %n veckor den %1$s hela dagen"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Om en månad den %1$s hela dagen","Om %n månader den %1$s hela dagen"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Om en år den %1$s hela dagen","Om %n år den %1$s hela dagen"],
+ "In the past on %1$s between %2$s - %3$s" : "Tidigare den %1$s mellan %2$s - %3$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Om en minut den %1$s mellan %2$s - %3$s","Om %n minuter den %1$s mellan %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Om en timme den %1$s mellan %2$s - %3$s","Om %n timmar den %1$s mellan %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Om en dag den %1$s mellan %2$s - %3$s","Om %n dagar den %1$s mellan %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Om en vecka den %1$s mellan %2$s - %3$s","Om %n veckor den %1$s mellan %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Om en månad den %1$s mellan %2$s - %3$s","Om %n månader den %1$s mellan %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Om ett år den %1$s mellan %2$s - %3$s","Om %n år den %1$s mellan %2$s - %3$s"],
+ "Could not generate when statement" : "Kunde inte generera ett när-villkor",
+ "Every Day for the entire day" : "Varje dag hela dagen",
+ "Every Day for the entire day until %1$s" : "Varje dag hela dagen tills den %1$s",
+ "Every Day between %1$s - %2$s" : "Varje dag mellan %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Varje dag mellan %1$s - %2$s tills den %3$s",
+ "Every %1$d Days for the entire day" : "Varje %1$d dagar, hela dagen",
+ "Every %1$d Days for the entire day until %2$s" : "Varje %1$d dagar hela dagen tills den %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Varje %1$d dagar mellan %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Varje %1$d dagar mellan %2$s - %3$s tills den %4$s",
+ "Could not generate event recurrence statement" : "Kunde inte generera händelseupprepning",
+ "Every Week on %1$s for the entire day" : "Varje vecka på %1$s, hela dagen",
+ "Every Week on %1$s for the entire day until %2$s" : "Varje vecka på %1$s hela dagen tills den %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Varje vecka på %1$s mellan %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Varje vecka på %1$s mellan %2$s - %3$s till den %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Varje %1$d vecka på %2$s hela dagen",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Varje %1$d vecka på %2$s för hela dagen till %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Varje %1$d vecka på %2$s mellan %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Varje %1$d vecka på %2$s mellan %3$s - %4$s till %5$s",
+ "Every Month on the %1$s for the entire day" : "Varje månad på den %1$s hela dagen",
+ "Every Month on the %1$s for the entire day until %2$s" : "Varje månad på den %1$s hela dagen till %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Varje månad på den %1$s mellan %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Varje månad på den %1$s mellan %2$s - %3$s till %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Varje %1$d månad på den %2$s hela dagen",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Varje %1$d månad på den %2$s hela dagen till %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Varje %1$d månad på den %2$s mellan %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Varje %1$d månad på den %2$s mellan %3$s - %4$s till %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Varje år %1$s på den %2$s hela dagen",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Varje år i %1$s på den %2$s hela dagen till %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Varje år i %1$s på den %2$s mellan %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Varje år i %1$s på den %2$s mellan %3$s - %4$s till %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Vart %1$d år i %2$s på den %3$s hela dagen",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Vart %1$d år i %2$s på den %3$s hela dagen till %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Vart %1$d år i %2$s på den %3$s mellan %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Vart %1$d år i %2$s på den %3$s mellan %4$s - %5$s till %6$s",
+ "On specific dates for the entire day until %1$s" : "På specifika datum hela dagen fram till kl %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "På specifika datum mellan %1$s - %2$s tills %3$s",
+ "In the past on %1$s" : "Tidigare den %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Om en minut den %1$s","Om %n minuter den %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Om en timme den %1$s","Om %n timmar den %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["Om en dag den %1$s","Om %n dagar den %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Om en vecka den %1$s","Om %n veckor den %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Om en månad den %1$s","Om %n månader den %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Om ett år den %1$s","Om %n år den %1$s"],
+ "In the past on %1$s then on %2$s" : "Tidigare den %1$s och sedan den %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Om en minu den %1$s och sedan den %2$s","Om %n minuter den %1$s och sedan den %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Om en timme den %1$s och sedan den %2$s","Om %n timmar den %1$s och sedan den %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Om en dag den %1$s och sedan den %2$s","Om %n dagar den %1$s och sedan den %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Om en vecka den %1$s och sedan den %2$s","Om %n veckor den %1$s och sedan den %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Om en månad den %1$s och sedan den %2$s","Om %n månader den %1$s och sedan den %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Om ett år den %1$s och sedan den %2$s","Om %n år den %1$s och sedan den %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "Tidigare den %1$s sedan den %2$s och %3$s",
+ "_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_" : ["Om en minut den %1$s sedan den %2$s och %3$s","Om %n minuter den %1$s sedan den %2$s och %3$s"],
+ "_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_" : ["Om en timme den %1$s sedan den %2$s och %3$s","Om %n timmar den %1$s sedan den %2$s och %3$s"],
+ "_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_" : ["Om en dag den %1$s sedan den %2$s och %3$s","Om %n dagar den %1$s sedan den %2$s och %3$s"],
+ "_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_" : ["Om en vecka den %1$s sedan den %2$s och %3$s","Om %n veckor den %1$s sedan den %2$s och %3$s"],
+ "_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_" : ["Om en månad den %1$s sedan den %2$s och %3$s","Om %n månader den %1$s sedan den %2$s och %3$s"],
+ "_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_" : ["Om ett år den %1$s sedan den %2$s och %3$s","Om %n år den %1$s sedan den %2$s och %3$s"],
+ "Could not generate next recurrence statement" : "Kunde inte generera nästa händelseupprepning",
"Cancelled: %1$s" : "Avbruten: %1$s",
- "Invitation canceled" : "Inbjudan avbruten",
+ "\"%1$s\" has been canceled" : "\"%1$s\" har avbrutits",
"Re: %1$s" : "Sv: %1$s",
- "Invitation updated" : "Inbjudan uppdaterad",
+ "%1$s has accepted your invitation" : "%1$s har accepterat din inbjudan",
+ "%1$s has tentatively accepted your invitation" : "%1$s har preliminärt accepterat din inbjudan",
+ "%1$s has declined your invitation" : "%1$s har tackat nej till din inbjudan",
+ "%1$s has responded to your invitation" : "%1$s har svarat på din inbjudan",
+ "Invitation updated: %1$s" : "Inbjudan uppdaterad: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s uppdaterade händelse \"%2$s\"",
"Invitation: %1$s" : "Inbjudan: %1$s",
- "Invitation" : "Inbjudan",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s har bjudit in dig till \"%2$s\"",
+ "Organizer:" : "Arrangör:",
+ "Attendees:" : "Deltagare:",
"Title:" : "Titel:",
- "Time:" : "Tid:",
+ "When:" : "När",
"Location:" : "Ort:",
"Link:" : "Länk:",
- "Organizer:" : "Arrangör:",
- "Attendees:" : "Deltagare:",
+ "Occurring:" : "Förekommer:",
"Accept" : "Acceptera",
"Decline" : "Avböj",
"More options …" : "Fler alternativ ...",
"More options at %s" : "Fler alternativ på %s",
+ "Monday" : "Måndag",
+ "Tuesday" : "Tisdag",
+ "Wednesday" : "Onsdag",
+ "Thursday" : "Torsdag",
+ "Friday" : "Fredag",
+ "Saturday" : "Lördag",
+ "Sunday" : "Söndag",
+ "January" : "Januari",
+ "February" : "Februari",
+ "March" : "Mars",
+ "April" : "April",
+ "May" : "Maj",
+ "June" : "Juni",
+ "July" : "Juli",
+ "August" : "Augusti",
+ "September" : "September",
+ "October" : "Oktober",
+ "November" : "November",
+ "December" : "December",
+ "First" : "Första",
+ "Second" : "Andra",
+ "Third" : "Tredje",
+ "Fourth" : "Fjärde",
+ "Fifth" : "Femte",
+ "Last" : "Sista",
+ "Second Last" : "Näst sist",
+ "Third Last" : "Tredje sist",
+ "Fourth Last" : "Fjärde sist",
+ "Fifth Last" : "Femte sista",
"Contacts" : "Kontakter",
"{actor} created address book {addressbook}" : "{actor} skapade adressboken {addressbook}",
"You created address book {addressbook}" : "Du skapade adressboken {addressbook}",
@@ -102,54 +214,122 @@
"{actor} created contact {card} in address book {addressbook}" : "{actor} skapade kontakten {card} i adressboken {addressbook}",
"You created contact {card} in address book {addressbook}" : "Du skapade kontakten {card} i adressboken {addressbook}",
"{actor} deleted contact {card} from address book {addressbook}" : "{actor} tog bort kontakten {card} från adressboken {addressbook}",
- "You deleted contact {card} from address book {addressbook}" : "Du tog bort kontakten {©ard} från adressboken {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Du tog bort kontakten {card} från adressboken {addressbook}",
"{actor} updated contact {card} in address book {addressbook}" : "{actor} uppdaterade kontakten {card} i adressboken {addressbook}",
"You updated contact {card} in address book {addressbook}" : "Du uppdaterade kontakten {card} i adressboken {addressbook}",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "En <strong>kontakt</strong> eller <strong>adressbok</strong> ändrades",
+ "Accounts" : "Konton",
+ "System address book which holds all accounts" : "Systemadressbok som innehåller alla konton",
+ "File is not updatable: %1$s" : "Fil kan inte uppdateras: %1$s",
+ "Failed to get storage for file" : "Kunde inte få lagringsutrymme för filen",
+ "Could not write to final file, canceled by hook" : "Kunde ej skriva till den slutgiltiga filen, avbröts av en kopplad åtgärd",
+ "Could not write file contents" : "Kunde inte skriva filens innehåll",
+ "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Fel vid kopiering av fil till målplats (kopierade: %1$s, förväntad filstorlek: %2$s)",
+ "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." : "Förväntad filstorlek på %1$s men läste (från Nextcloud-klienten) och skrev (till Nextcloud-lagringen) %2$s. Kan antingen vara ett nätverksproblem på sändnings-sidan eller problem med att skriva till lagringen på server-sidan.",
+ "Could not rename part file to final file, canceled by hook" : "Kunde inte byta namn på filfragment till slutgiltigt filnamn, avbröts av en kopplad åtgärd.",
+ "Could not rename part file to final file" : "Kunde inte ändra namn från temporära filen till slutliga filen",
+ "Failed to check file size: %1$s" : "Kunde inte kontrollera filstorleken: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "Kunde inte öppna filen: %1$s, filen verkar inte finnas",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Kunde inte öppna filen: %1$s, filen verkar inte finnas",
+ "Encryption not ready: %1$s" : "Kryptering ej redo: %1$s",
+ "Failed to open file: %1$s" : "Kunde inte öppna fil: %1$s",
+ "Failed to unlink: %1$s" : "Kunde inte ta bort: %1$s",
+ "Failed to write file contents: %1$s" : "Kunde inte skriva filinnehåll: %1$s",
+ "File not found: %1$s" : "Filen hittades inte: %1$s",
+ "Invalid target path" : "Ogiltig målsökväg",
"System is in maintenance mode." : "Systemet är i underhållsläge.",
"Upgrade needed" : "Uppdatering nödvändig",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Din %s måste konfigureras att använda HTTPS för CalDAV och CardDAV med iOS/macOS.",
"Configures a CalDAV account" : "Konfigurerar ett CalDAV-konto",
"Configures a CardDAV account" : "Konfigurerar ett CardDAV-konto",
"Events" : "Händelser",
- "Tasks" : "Uppgifter",
"Untitled task" : "Namnlös uppgift",
"Completed on %s" : "Slutförd %s",
"Due on %s by %s" : "Slutar den %s vid %s",
"Due on %s" : "Slutar den %s",
+ "Example event - open me!" : "Exempelhändelse – öppna mig!",
+ "System Address Book" : "Systemadressbok",
+ "The system address book contains contact information for all users in your instance." : "Systemadressboken innehåller kontaktinformation för alla användare i din instans.",
+ "Enable System Address Book" : "Aktivera systemadressboken",
+ "DAV system address book" : "DAV-systemets adressbok",
+ "No outstanding DAV system address book sync." : "Ingen utestående synkronisering för DAV-systemets adressbok.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV-systemets adressbokssynkronisering har inte körts ännu eftersom din instans har fler än 1000 användare eller för att ett fel uppstod. Kör det manuellt genom att anropa \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "WebDAV endpoint",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Det gick inte att kontrollera att din webbserver är korrekt inställd för att tillåta filsynkronisering över WebDAV. Kontrollera manuellt.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Din webbserver är ännu inte korrekt inställd för att tillåta filsynkronisering, eftersom WebDAV-gränssnittet verkar vara trasigt.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Din webbserver är korrekt inställd för att tillåta filsynkronisering över WebDAV.",
+ "Migrated calendar (%1$s)" : "Migrerade kalender (%1$s)",
+ "Calendars including events, details and attendees" : "Kalendrar inklusive händelser, detaljer och deltagare",
"Contacts and groups" : "Kontakter och grupper",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV endpoint",
- "Availability" : "Tillgänglighet",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Om du konfigurerar dina arbetstimmar kan andra användare se när du inte är på jobbet när de bokar ett möte.",
+ "Absence saved" : "Frånvaro sparad",
+ "Failed to save your absence settings" : "Kunde inte spara dina frånvaroinställningar",
+ "Absence cleared" : "Frånvaro rensad",
+ "Failed to clear your absence settings" : "Kunde inte rensa dina frånvaroinställningar",
+ "First day" : "Första dagen",
+ "Last day (inclusive)" : "Sista dagen (inklusive)",
+ "Out of office replacement (optional)" : "Ersättare utanför kontoret (valfritt)",
+ "Name of the replacement" : "Namn på ersättaren",
+ "No results." : "Inga resultat.",
+ "Start typing." : "Börja skriva.",
+ "Short absence status" : "Meddelande om kort frånvaro",
+ "Long absence Message" : "Meddelande om långvarig frånvaro",
+ "Save" : "Spara",
+ "Disable absence" : "Inaktivera frånvaro",
+ "Failed to load availability" : "Misslyckades med att ladda tidsluckor",
+ "Saved availability" : "Sparade tidslucka",
+ "Failed to save availability" : "Misslyckades med att spara tidslucka",
"Time zone:" : "Tidszon:",
"to" : "till",
+ "Delete slot" : "Radera lucka",
"No working hours set" : "Inga arbetstimmar satta",
- "Monday" : "Måndag",
- "Tuesday" : "Tisdag",
- "Wednesday" : "Onsdag",
- "Thursday" : "Torsdag",
- "Friday" : "Fredag",
- "Saturday" : "Lördag",
- "Sunday" : "Söndag",
- "Save" : "Spara",
+ "Add slot" : "Lägg till lucka",
+ "Weekdays" : "Vardagar",
+ "Pick a start time for {dayName}" : "Välj en starttid för {dayName}",
+ "Pick a end time for {dayName}" : "Välj en sluttid för {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Sätt automatiskt användarstatus till \"Stör ej\" utanför tillgängliga tider för att tysta alla notifikationer.",
+ "Cancel" : "Avbryt",
+ "Import" : "Importera",
+ "Error while saving settings" : "Fel vid sparande av inställningar",
+ "Contact reset successfully" : "Kontakten har återställts",
+ "Error while resetting contact" : "Fel vid återställning av kontakt",
+ "Contact imported successfully" : "Kontakten har importerats",
+ "Error while importing contact" : "Fel vid import av kontakt",
+ "Import contact" : "Importera kontakt",
+ "Reset to default" : "Återställ till grundinställningar",
+ "Import contacts" : "Importera kontakter",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Att importera en ny .vcf-fil kommer att radera den befintliga standardkontakten och ersätta den med den nya. Vill du fortsätta?",
+ "Failed to save example event creation setting" : "Kunde inte spara exemplet för inställning av händelseskapande",
+ "Failed to upload the example event" : "Kunde inte ladda upp exempelhändelsen",
+ "Custom example event was saved successfully" : "Anpassad exempelhändelse sparades",
+ "Failed to delete the custom example event" : "Kunde inte ta bort den anpassade exempelhändelsen",
+ "Custom example event was deleted successfully" : "Anpassad exempelhändelse har raderats",
+ "Import calendar event" : "Importera kalenderhändelse",
+ "Uploading a new event will overwrite the existing one." : "Om du laddar upp en ny händelse kommer den att skriva över den befintliga.",
+ "Upload event" : "Ladda upp händelse",
+ "Availability" : "Din tillgänglighet",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Om du konfigurerar dina arbetstider kommer andra att se när du är frånvarande när de bokar ett möte.",
+ "Absence" : "Frånvaro",
+ "Configure your next absence period." : "Konfigurera din nästa frånvaroperiod.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installera även {calendarappstoreopen}Kalender-app{linkclose}, eller {calendardocopen}anslut din dator & mobil för synkronisering ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Verifiera och säkerställ inställningar för {emailopen}e-postserver{linkclose}.",
"Calendar server" : "Kalenderserver",
"Send invitations to attendees" : "Skicka inbjudan till deltagare",
"Automatically generate a birthday calendar" : "Generera en födelsedagskalender automatiskt",
"Birthday calendars will be generated by a background job." : "Födelsedagskalender kommer skapas som ett bakgrundsjobb.",
"Hence they will not be available immediately after enabling but will show up after some time." : "Därför kommer de inte vara tillgängliga direkt efter aktivering men kommer dyka upp efter en tid.",
- "Send notifications for events" : "Skicka aviseringar för händelser",
- "Notifications are sent via background jobs, so these must occur often enough." : "Aviseringar skickas genom bakgrundsjobb, så dessa måste ske tillräckligt ofta.",
- "Enable notifications for events via push" : "Aktivera aviseringar för händelser via push",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installera även {calendarappstoreopen}Kalender-app{linkclose}, eller {calendardocopen}anslut din dator & mobil för synkronisering ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Verifiera och säkerställ inställningar för {emailopen}e-postserver{linkclose}.",
+ "Send notifications for events" : "Skicka notiser för händelser",
+ "Notifications are sent via background jobs, so these must occur often enough." : "Notiser skickas genom bakgrundsjobb, så dessa måste ske tillräckligt ofta.",
+ "Send reminder notifications to calendar sharees as well" : "Skicka även påminnelser till kalenderdeltagare",
+ "Reminders are always sent to organizers and attendees." : "Påminnelser skickas alltid till arrangörer och deltagare.",
+ "Enable notifications for events via push" : "Aktivera notiser för händelser via push",
+ "Example content" : "Exempelinnehåll",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Exempelinnehåll används för att visa upp funktionerna i Nextcloud. Standardinnehåll levereras med Nextcloud och kan ersättas med anpassat innehåll.",
"There was an error updating your attendance status." : "Ett fel uppstod vid uppdatering av din närvarostatus.",
"Please contact the organizer directly." : "Vänligen kontakta arrangören direkt.",
"Are you accepting the invitation?" : "Acceptera inbjudan?",
"Tentative" : "Preliminärt",
- "Number of guests" : "Antal gäster",
- "Comment" : "Kommentar",
- "Your attendance was updated successfully." : "Dina närvaro uppdaterades.",
- "Calendar and tasks" : "Kalender och uppgifter"
+ "Your attendance was updated successfully." : "Dina närvaro uppdaterades."
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/dav/l10n/tr.js b/apps/dav/l10n/tr.js
index aa21694e64e..9d1ed13b4f9 100644
--- a/apps/dav/l10n/tr.js
+++ b/apps/dav/l10n/tr.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Takvim",
- "Todos" : "Yapılacak işler",
+ "Tasks" : "Görevler",
"Personal" : "Kişisel",
"{actor} created calendar {calendar}" : "{actor}, {calendar} takvimini ekledi",
"You created calendar {calendar}" : "{calendar} takvimini eklediniz",
@@ -25,36 +25,41 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor}, {calendar} takvimini {group} grubu ile paylaştı",
"You unshared calendar {calendar} from group {group}" : "{calendar} takviminin {group} grubu ile paylaşımını kaldırdınız",
"{actor} unshared calendar {calendar} from group {group}" : "{actor}, {calendar} takviminin {group} grubu ile paylaşımını kaldırdı",
+ "Untitled event" : "Adlandırılmamış etkinlik",
"{actor} created event {event} in calendar {calendar}" : "{actor}, {calendar} takvimine {event} etkinliğini ekledi",
"You created event {event} in calendar {calendar}" : "{calendar} takvimine {event} etkinliğini eklediniz",
"{actor} deleted event {event} from calendar {calendar}" : "{actor}, {calendar} takviminden {event} etkinliğini sildi",
"You deleted event {event} from calendar {calendar}" : "{calendar} takviminden {event} etkinliğini sildiniz",
"{actor} updated event {event} in calendar {calendar}" : "{actor}, {calendar} takvimindeki {event} etkinliğini güncelledi",
"You updated event {event} in calendar {calendar}" : "{calendar} takvimindeki {event} etkinliğini güncellediniz",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor}, {event} etkinliğini {sourceCalendar} takviminden {targetCalendar} takvimine taşıdı",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{event} etkinliğini {sourceCalendar} takviminden {targetCalendar} takvimine taşıdınız",
"{actor} restored event {event} of calendar {calendar}" : "{actor}, {calendar} takvimindeki {event} etkinliğini geri yükledi",
"You restored event {event} of calendar {calendar}" : "{calendar} takvimindeki {event} etkinliğini geri yüklediniz",
"Busy" : "Meşgul",
- "{actor} created todo {todo} in list {calendar}" : "{actor}, {calendar} takvimi listesine {todo} yapılacak işini ekledi",
- "You created todo {todo} in list {calendar}" : "{calendar} takvimi listesine {todo} yapılacak işini eklediniz",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor}, {calendar} takvimi listesinden {todo} yapılacak işini sildi",
- "You deleted todo {todo} from list {calendar}" : "{calendar} takvimi listesinden {todo} yapılacak işini sildiniz",
- "{actor} updated todo {todo} in list {calendar}" : "{actor}, {calendar} takvimi listesindeki {todo} yapılacak işini güncelledi",
- "You updated todo {todo} in list {calendar}" : "{calendar} takvimi listesindeki {todo} yapılacak işini güncellediniz",
- "{actor} solved todo {todo} in list {calendar}" : "{actor}, {calendar} takvimi listesindeki {todo} yapılacak işini tamamladı",
- "You solved todo {todo} in list {calendar}" : "{calendar} takvimi listesindeki {todo} yapılacak işini tamamladınız",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor}, {calendar} takvimi listesindeki {todo} yapılacak işini yeniden başlattı",
- "You reopened todo {todo} in list {calendar}" : "{calendar} takvimi listesindeki {todo} yapılacak işini yeniden başlattınız",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor}, {calendar} takvimi listesine {todo} yapılacak işini ekledi",
+ "You created to-do {todo} in list {calendar}" : "{calendar} takvimi listesine {todo} yapılacak işini eklediniz",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor}, {calendar} takvimi listesinden {todo} yapılacak işini sildi",
+ "You deleted to-do {todo} from list {calendar}" : "{calendar} takvimi listesinden {todo} yapılacak işini sildiniz",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor}, {calendar} takvimi listesindeki {todo} yapılacak işini güncelledi",
+ "You updated to-do {todo} in list {calendar}" : "{calendar} takvimi listesindeki {todo} yapılacak işini güncellediniz",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor}, {calendar} takvimi listesindeki {todo} yapılacak işini tamamladı",
+ "You solved to-do {todo} in list {calendar}" : "{calendar} takvimi listesindeki {todo} yapılacak işini tamamladınız",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor}, {calendar} takvimi listesindeki {todo} yapılacak işini yeniden başlattı",
+ "You reopened to-do {todo} in list {calendar}" : "{calendar} takvimi listesindeki {todo} yapılacak işini yeniden başlattınız",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor}, {todo} yapılacak işini {sourceCalendar} listesinden {targetCalendar} listesine taşıdı",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{todo} yapılacak işini {sourceCalendar} listesinden {targetCalendar} listesine taşıdınız",
"Calendar, contacts and tasks" : "Takvim, kişiler ve görevler",
"A <strong>calendar</strong> was modified" : "Bir <strong>takvim</strong> düzenlendi",
"A calendar <strong>event</strong> was modified" : "Bir takvim <strong>etkinliği</strong> düzenlendi",
- "A calendar <strong>todo</strong> was modified" : "Bir takvim <strong>yapılacak işi</strong> düzenlendi",
+ "A calendar <strong>to-do</strong> was modified" : "Bir takvim <strong>yapılacak işi</strong> düzenlendi",
"Contact birthdays" : "Kişi doğum günleri",
"Death of %s" : "%s ölümü",
+ "Untitled calendar" : "Adlandırılmamış takvim",
"Calendar:" : "Takvim:",
"Date:" : "Tarih:",
"Where:" : "Yer:",
"Description:" : "Açıklama:",
- "Untitled event" : "Adsız etkinlik",
"_%n year_::_%n years_" : ["%n yıl","%n yıl"],
"_%n month_::_%n months_" : ["%n ay","%n ay"],
"_%n day_::_%n days_" : ["%n gün","%n gün"],
@@ -67,22 +72,129 @@ OC.L10N.register(
"Description: %s" : "Açıklama: %s",
"Where: %s" : "Şurada: %s",
"%1$s via %2$s" : "%1$s, %2$s aracılığıyla",
+ "In the past on %1$s for the entire day" : "Tüm gün boyunca %1$s zamanında geçmişte",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Tüm gün boyunca %1$s zamanında bir dakika içinde","Tüm gün boyunca %1$s zamanında %n dakika içinde"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Tüm gün boyunca %1$s zamanında bir saat içinde","Tüm gün boyunca %1$s zamanında %n saat içinde"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Tüm gün boyunca %1$s zamanında bir gün içinde","Tüm gün boyunca %1$s zamanında %n gün içinde"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Tüm gün boyunca %1$s zamanında bir hafta içinde","Tüm gün boyunca %1$s zamanında %n hafta içinde"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Tüm gün boyunca %1$s zamanında bir ay içinde","Tüm gün boyunca %1$s zamanında %n ay içinde"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Tüm gün boyunca %1$s zamanında bir yıl içinde","Tüm gün boyunca %1$s zamanında %n yıl içinde"],
+ "In the past on %1$s between %2$s - %3$s" : "Geçmişte %1$s zamanında %2$s ile %3$s arasında",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Bir dakika içinde %1$s zamanında %2$s ile %3$s arasında","%n dakika içinde %1$s zamanında %2$s ile %3$s arasında"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Bir saat içinde %1$s zamanında %2$s ile %3$s arasında","%n saat içinde %1$s zamanında %2$s ile %3$s arasında"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Bir gün içinde %1$s zamanında %2$s ile %3$s arasında","%n gün içinde %1$s zamanında %2$s ile %3$s arasında"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Bir hafta içinde %1$s zamanında %2$s ile %3$s arasında","%n hafta içinde %1$s zamanında %2$s ile %3$s arasında"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Bir ay içinde %1$s zamanında %2$s ile %3$s arasında","%n ay içinde %1$s zamanında %2$s ile %3$s arasında"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Bir yıl içinde %1$s zamanında %2$s ile %3$s arasında","%n yıl içinde %1$s zamanında %2$s ile %3$s arasında"],
+ "Could not generate when statement" : "Zaman ifadesi oluşturulamadı",
+ "Every Day for the entire day" : "Her gün tüm gün boyunca",
+ "Every Day for the entire day until %1$s" : "%1$s zamanına kadar her gün tüm gün boyunca",
+ "Every Day between %1$s - %2$s" : "Her gün %1$s ile %2$s arasında",
+ "Every Day between %1$s - %2$s until %3$s" : "%3$s zamanına kadar %1$s ile %2$s arasında her gün",
+ "Every %1$d Days for the entire day" : "Her %1$d günde tüm gün boyunca",
+ "Every %1$d Days for the entire day until %2$s" : "%2$s zamanına kadar her %1$d günde tüm gün boyunca",
+ "Every %1$d Days between %2$s - %3$s" : "Her %1$d günde %2$s ile %3$s arasında",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "%4$s zamanına kadar her %1$d günde %2$s ile %3$s",
+ "Could not generate event recurrence statement" : "Etkinlik yinelenme ifadesi oluşturulamadı",
+ "Every Week on %1$s for the entire day" : "Her hafta %1$s zamanında tüm gün boyunca",
+ "Every Week on %1$s for the entire day until %2$s" : "Her hafta %1$s zamanında tüm gün boyunca %2$s zamanına kadar",
+ "Every Week on %1$s between %2$s - %3$s" : "Her hafta %1$s zamanında %2$s ile %3$s arasında",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Her hafta %1$s zamanında %2$s ile %3$s arasında %4$s zamanına kadar",
+ "Every %1$d Weeks on %2$s for the entire day" : "Her %1$d haftada bir %2$s zamanında tüm gün boyunca",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Her %1$d haftada bir %2$s zamanında tüm gün boyunca %3$s zamanına kadar",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Her %1$d haftada bir %2$s zamanında tüm gün boyunca %3$s ile %4$s arasında",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Her %1$d haftada bir %2$s zamanında %3$s ile %4$s arasında %5$s zamanına kadar",
+ "Every Month on the %1$s for the entire day" : "Her ay %1$s zamanında tüm gün boyunca",
+ "Every Month on the %1$s for the entire day until %2$s" : "Her ay %1$s zamanında tüm gün boyunca %2$s zamanına kadar",
+ "Every Month on the %1$s between %2$s - %3$s" : "Her ay %1$s zamanında %2$s ile %3$s arasında",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Her ay %1$s zamanında %2$s ile %3$s arasında %4$s zamanına kadar",
+ "Every %1$d Months on the %2$s for the entire day" : "Her %1$d ayda bir %2$s zamanında tüm gün boyunca",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Her %1$d ayda bir %2$s zamanında tüm gün boyunca %3$s zamanına kadar",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Her %1$d ayda bir %2$s zamanında tüm gün boyunca %3$s ile %4$s arasında",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Her %1$d ayda bir %2$s zamanında %3$s ile %4$s arasında %5$s zamanına kadar",
+ "Every Year in %1$s on the %2$s for the entire day" : "Her yıl %1$s içinde %2$s zamanında tüm gün boyunca",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Her yıl %1$s içinde %2$s zamanında tüm gün boyunca %3$s zamanına kadar",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Her yıl %1$s içinde %2$s zamanında tüm gün boyunca %3$s ile %4$s arasında",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Her yıl %1$s içinde %2$s zamanında %3$s ile %4$s arasında %5$s zamanına kadar",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Her %1$d yılda bir %2$s içinde %3$s zamanında tüm gün boyunca",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Her %1$d yılda bir %2$s içinde %3$s zamanında tüm gün boyunca %4$s zamanına kadar",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Her %1$d yılda bir %2$s içinde %3$s zamanında tüm gün boyunca %4$s ile %5$s arasında",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Her %1$d yılda bir %2$s içinde %3$s zamanında %4$s ile %5$s arasında %6$s zamanına kadar",
+ "On specific dates for the entire day until %1$s" : "Belirli tarihlerde tüm gün boyunca %1$s zamanına kadar",
+ "On specific dates between %1$s - %2$s until %3$s" : "Belirli tarihlerde %1$s ile %2$s arasında %3$s zamanına kadar",
+ "In the past on %1$s" : "%1$s zamanında geçmişte",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["%1$s zamanında bir dakika içinde","%1$s zamanında %n dakika içinde"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["%1$s zamanında bir saat içinde","%1$s zamanında %n saat içinde"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["%1$s zamanında bir gün içinde","%1$s zamanında %n gün içinde"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["%1$s zamanında bir hafta içinde","%1$s zamanında %n hafta içinde"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["%1$s zamanında bir ay içinde","%1$s zamanında %n ay içinde"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["%1$s zamanında bir yıl içinde","%1$s zamanında %n yıl içinde"],
+ "In the past on %1$s then on %2$s" : "Geçmişte %1$s zamanında ardından %2$s zamanında",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Bir dakika içinde %1$s zamanında ardından %2$s zamanında","%n dakika içinde %1$s zamanında ardından %2$s zamanında"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Bir saat içinde %1$s zamanında ardından %2$s zamanında","%n saat içinde %1$s zamanında ardından %2$s zamanında"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Bir gün içinde %1$s zamanında ardından %2$s zamanında","%n gün içinde %1$s zamanında ardından %2$s zamanında"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Bir hafta içinde %1$s zamanında ardından %2$s zamanında","%n hafta içinde %1$s zamanında ardından %2$s zamanında"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Bir ay içinde %1$s zamanında ardından %2$s zamanında","%n ay içinde %1$s zamanında ardından %2$s zamanında"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Bir yıl içinde %1$s zamanında ardından %2$s zamanında","%n yıl içinde %1$s zamanında ardından %2$s zamanında"],
+ "In the past on %1$s then on %2$s and %3$s" : "Geçmişte %1$s zamanında ardından %2$s ve %3$s zamanında",
+ "_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_" : ["Bir dakika içinde %1$s zamanında ardından %2$s ve %3$s zamanında","%n dakika içinde %1$s zamanında ardından %2$s ve %3$s zamanında"],
+ "_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_" : ["Bir saat içinde %1$s zamanında ardından %2$s ve %3$s zamanında","%n saat içinde %1$s zamanında ardından %2$s ve %3$s zamanında"],
+ "_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_" : ["Bir gün içinde %1$s zamanında ardından %2$s ve %3$s zamanında","%n gün içinde %1$s zamanında ardından %2$s ve %3$s zamanında"],
+ "_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_" : ["Bir hafta içinde %1$s zamanında ardından %2$s ve %3$s zamanında","%n hafta içinde %1$s zamanında ardından %2$s ve %3$s zamanında"],
+ "_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_" : ["Bir ay içinde %1$s zamanında ardından %2$s ve %3$s zamanında","%n ay içinde %1$s zamanında ardından %2$s ve %3$s zamanında"],
+ "_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_" : ["Bir yıl içinde %1$s zamanında ardından %2$s ve %3$s zamanında","%n yıl içinde %1$s zamanında ardından %2$s ve %3$s zamanında"],
+ "Could not generate next recurrence statement" : "Sonraki yinelenme ifadesi oluşturulamadı",
"Cancelled: %1$s" : "İptal edildi: %1$s",
- "Invitation canceled" : "Çağrı iptal edildi",
+ "\"%1$s\" has been canceled" : "\"%1$s\" iptal edildi",
"Re: %1$s" : "Ynt: %1$s",
- "Invitation updated" : "Çağrı güncellendi",
- "Invitation: %1$s" : "Çağrı: %1$s",
- "Invitation" : "Çağrı",
+ "%1$s has accepted your invitation" : "%1$s davetinizi kabul etti",
+ "%1$s has tentatively accepted your invitation" : "%1$s davetinizi belirsiz olarak kabul etti",
+ "%1$s has declined your invitation" : "%1$s davetinizi reddetti.",
+ "%1$s has responded to your invitation" : "%1$s davetinizi yanıtladı",
+ "Invitation updated: %1$s" : "Davet güncellendi: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s, \"%2$s\" etkinliğini güncelledi",
+ "Invitation: %1$s" : "Davet: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s, size \"%2$s\" için davet gönderdi",
+ "Organizer:" : "Düzenleyen:",
+ "Attendees:" : "Katılımcılar:",
"Title:" : "Başlık:",
- "Time:" : "Zaman:",
+ "When:" : "Zaman:",
"Location:" : "Konum:",
"Link:" : "Bağlantı:",
- "Organizer:" : "Düzenleyen:",
- "Attendees:" : "Katılımcılar:",
+ "Occurring:" : "Başlangıç:",
"Accept" : "Kabul et",
"Decline" : "Reddet",
"More options …" : "Diğer seçenekler …",
"More options at %s" : "%s üzerindeki diğer seçenekler",
+ "Monday" : "Pazartesi",
+ "Tuesday" : "Salı",
+ "Wednesday" : "Çarşamba",
+ "Thursday" : "Perşembe",
+ "Friday" : "Cuma",
+ "Saturday" : "Cumartesi",
+ "Sunday" : "Pazar",
+ "January" : "Ocak",
+ "February" : "Şubat",
+ "March" : "Mart",
+ "April" : "Nisan",
+ "May" : "Mayıs",
+ "June" : "Haziran",
+ "July" : "Temmuz",
+ "August" : "Ağustos",
+ "September" : "Eylül",
+ "October" : "Ekim",
+ "November" : "Kasım",
+ "December" : "Aralık",
+ "First" : "İlk",
+ "Second" : "İkinci",
+ "Third" : "Üçüncü",
+ "Fourth" : "Dördüncü",
+ "Fifth" : "Beşinci",
+ "Last" : "Son",
+ "Second Last" : "İkinci son",
+ "Third Last" : "Üçüncü son",
+ "Fourth Last" : "Dördüncü son",
+ "Fifth Last" : "Beşinci son",
"Contacts" : "Kişiler",
"{actor} created address book {addressbook}" : "{actor}, {addressbook} adres defterini ekledi",
"You created address book {addressbook}" : "{addressbook} adres defterini eklediniz",
@@ -107,8 +219,11 @@ OC.L10N.register(
"You deleted contact {card} from address book {addressbook}" : "{addressbook} adres defterinden {card} kişi kartını sildiniz",
"{actor} updated contact {card} in address book {addressbook}" : "{actor}, {addressbook} adres defterindeki {card} kişi kartını güncelledi",
"You updated contact {card} in address book {addressbook}" : "{addressbook} adres defterindeki {card} kişi kartını güncellediniz",
- "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Bir <strong>kişi</strong> ya da <strong>adres defteri</strong> değiştirildi",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Bir <strong>kişi</strong> ya da <strong>adres defteri</strong> değiştirildiğinde",
+ "Accounts" : "Hesaplar",
+ "System address book which holds all accounts" : "Tüm hesapların bulunduğu sistem adres defteri",
"File is not updatable: %1$s" : "Dosya güncellenebilir değil: %1$s",
+ "Failed to get storage for file" : "Dosyanın depolaması alınamadı",
"Could not write to final file, canceled by hook" : "Sonuç dosyasına yazılamadı, bağlantı tarafından iptal edildi",
"Could not write file contents" : "Dosya içerikleri yazılamadı",
"_%n byte_::_%n bytes_" : ["%n bayt","%n bayt"],
@@ -117,64 +232,107 @@ OC.L10N.register(
"Could not rename part file to final file, canceled by hook" : "Parça dosyası sonuç dosyası olarak yeniden adlandırılamadı, bağlantı tarafından iptal edildi",
"Could not rename part file to final file" : "Parça dosyası sonuç dosyası olarak yeniden adlandırılamadı",
"Failed to check file size: %1$s" : "Dosya boyutu denetlenemedi: %1$s",
- "Could not open file" : "Dosya açılamadı",
+ "Could not open file: %1$s, file does seem to exist" : "Dosya açılamadı: %1$s, dosya var gibi görünüyor",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Dosya açılamadı: %1$s, dosya var gibi görünmüyor",
"Encryption not ready: %1$s" : "Şifreleme hazır değil: %1$s",
"Failed to open file: %1$s" : "Dosya açılamadı: %1$s",
"Failed to unlink: %1$s" : "Bağlantı kaldırılamadı: %1$s",
- "Invalid chunk name" : "Parça adı geçersiz",
- "Could not rename part file assembled from chunks" : "Parçalardan oluşturulan parça dosyası yeniden adlandırılamadı ",
"Failed to write file contents: %1$s" : "Dosya içerikleri yazılamadı: %1$s",
"File not found: %1$s" : "Dosya bulunamadı: %1$s",
+ "Invalid target path" : "Hedef yol geçersiz",
"System is in maintenance mode." : "Sistem bakım kipinde.",
"Upgrade needed" : "Yükseltme gerekiyor",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "iOS/macOS üzerinde CalDAV ve CardDAV kullanabilmek için %s HTTPS kullanacak şekilde yapılandırılmalıdır.",
"Configures a CalDAV account" : "Bir CalDAV hesabı yapılandırır",
"Configures a CardDAV account" : "Bir CardDAV hesabı yapılandırır",
"Events" : "Etkinlikler",
- "Tasks" : "Görevler",
- "Untitled task" : "Adsız görev",
+ "Untitled task" : "Adlandırılmamış görev",
"Completed on %s" : "%s tarihinde tamamlandı",
"Due on %s by %s" : "%s tarihine kadar %s tarafından",
"Due on %s" : "%s tarihine kadar",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Nextcloud Takvim uygulamasına hoş geldiniz!\n\nBu bir örnek etkinliktir. İstediğiniz düzenlemeleri yaparak Nextcloud Takvim ile planlamanın esnekliğini keşfedin!\n\nNextcloud Takvim ile şunları yapabilirsiniz:\n- Etkinlikleri kolayca oluşturabilir, düzenleyebilir ve yönetebilirsiniz.\n- Birden fazla takvim oluşturabilir ve bunları takım arkadaşlarınız, arkadaşlarınız veya ailenizle paylaşabilirsiniz.\n- Uygunluğunuzu kontrol edebilir ve yoğun zamanlarınızı başkalarına gösterebilirsiniz.\n- CalDAV aracılığıyla uygulamaları ve aygıtları sorunsuz bir şekilde bütünleştirebilirsiniz.\n- Deneyiminizi özelleştirebilirsiniz: Yinelenen etkinlikler planlayabilir, bildirimleri ve diğer ayarları ayarlayabilirsiniz.",
+ "Example event - open me!" : "Örnek etkinlik. Beni aç!",
+ "System Address Book" : "Sistem adres defteri",
+ "The system address book contains contact information for all users in your instance." : "Sistem adres defterinde, kopyanızdaki tüm kullanıcıların iletişim bilgileri bulunur.",
+ "Enable System Address Book" : "Sistem adres defteri kullanılsın",
+ "DAV system address book" : "DAV sistem adres defteri",
+ "No outstanding DAV system address book sync." : "Bekleyen bir DAV sistemi adres defteri eşitlemesi yok.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Kopyanızda 1000 üzerinde kullanıcı olduğundan ya da bir sorun çıktığından DAV sistemi adres defteri eşitlemesi henüz yapılmamış. Lütfen \"occ dav:sync-system-addressbook\" komutunu yürüterek el ile eşitleyin.",
+ "WebDAV endpoint" : "WebDAV bağlantı noktası",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Site sunucunuzun WebDAV üzerinden dosya eşitlemesi için doğru şekilde ayarlanıp ayarlanmadığı denetlenemedi. Lütfen el ile denetleyin.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Site sunucunuz dosya eşitlemesi için doğru şekilde ayarlanmamış. WebDAV arabirimi sorunlu görünüyor.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Site sunucunuz WebDAV üzerinden dosya eşitlemesi için doğru şekilde ayarlanmış.",
"Migrated calendar (%1$s)" : "Aktarılmış takvim (%1$s)",
"Calendars including events, details and attendees" : "Etkinlikler, bilgiler ve katılımcılar ile takvimler",
"Contacts and groups" : "Kişiler ve gruplar",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV bağlantı noktası",
- "Availability" : "Kullanılabilirlik",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Çalışma saatlerinizi ayarlarsanız, diğer kullanıcılar bir toplantı ayarladıklarında ofis dışında olduğunuzu görürler.",
+ "Absence saved" : "Bulunmama kaydedildi",
+ "Failed to save your absence settings" : "Bulunmama ayarlarınız kaydedilemedi",
+ "Absence cleared" : "Bulunmama temizlendi",
+ "Failed to clear your absence settings" : "Bulunmama ayarlarınız temizlenemedi",
+ "First day" : "İlk gün",
+ "Last day (inclusive)" : "Son gün (dahil)",
+ "Out of office replacement (optional)" : "Ofis dışındayken yerine bakacak kişi (isteğe bağlı)",
+ "Name of the replacement" : "Yerine bakacak kişi adı",
+ "No results." : "Herhangi bir sonuç bulunamadı.",
+ "Start typing." : "Yazmaya başlayın.",
+ "Short absence status" : "Kısa bulunmama durumu",
+ "Long absence Message" : "Uzun bulunmama iletisi",
+ "Save" : "Kaydet",
+ "Disable absence" : "Bulunmamayı kapat",
+ "Failed to load availability" : "Uygunluk yüklenemedi",
+ "Saved availability" : "Uygunluk kaydedildi",
+ "Failed to save availability" : "Uygunluk kaydedilemedi",
"Time zone:" : "Saat dilimi:",
"to" : "ile",
"Delete slot" : "Aralığı sil",
"No working hours set" : "Çalışma saatleri ayarlanmamış",
"Add slot" : "Aralık ekle",
- "Monday" : "Pazartesi",
- "Tuesday" : "Salı",
- "Wednesday" : "Çarşamba",
- "Thursday" : "Perşembe",
- "Friday" : "Cuma",
- "Saturday" : "Cumartesi",
- "Sunday" : "Pazar",
- "Save" : "Kaydet",
+ "Weekdays" : "Hafta içi günleri",
+ "Pick a start time for {dayName}" : "{dayName} için başlangıç zamanını seçin",
+ "Pick a end time for {dayName}" : "{dayName} için bitiş zamanını seçin",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Tüm bildirimleri sessize almak için, uygunluk durumu dışında kullanıcı durumu otomatik olarak \"Rahatsız etmeyin\" olarak ayarlanır.",
+ "Cancel" : "İptal",
+ "Import" : "İçe aktar",
+ "Error while saving settings" : "Ayarlar kaydedilirken sorun çıktı",
+ "Contact reset successfully" : "Kişi sıfırlandı",
+ "Error while resetting contact" : "Kişi sıfırlanırken sorun çıktı",
+ "Contact imported successfully" : "Kişi içe aktarıldı",
+ "Error while importing contact" : "Kişi içe aktarılırken sorun çıktı",
+ "Import contact" : "Kişiyi içe aktar",
+ "Reset to default" : "Varsayılanlara dön",
+ "Import contacts" : "Kişileri içe aktar",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Yeni bir .vcf dosyasını içe aktarmak, var olan varsayılan kişiyi siler ve yenisiyle değiştirir. İlerlemek istiyor musunuz?",
+ "Failed to save example event creation setting" : "Örnek etkinlik oluşturma ayarı kaydedilemedi",
+ "Failed to upload the example event" : "Örnek etkinlik yüklenemedi",
+ "Custom example event was saved successfully" : "Özel örnek etkinlik kaydedildi",
+ "Failed to delete the custom example event" : "Özel örnek etkinlik silinemedi",
+ "Custom example event was deleted successfully" : "Özel örnek etkinlik silindi",
+ "Import calendar event" : "Takvim etkinliğini içe aktar",
+ "Uploading a new event will overwrite the existing one." : "Yeni bir etkinlik yüklendiğinde eskisinin üzerine yazılır.",
+ "Upload event" : "Etkinlik yükle",
+ "Availability" : "Uygunluk",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Çalışma saatlerinizi ayarlarsanız, diğer kişiler bir toplantı ayarladıklarında ofis dışında olduğunuzu görürler.",
+ "Absence" : "Bulunmama",
+ "Configure your next absence period." : "Sonraki bulunmama aralığınızı yapılandırın.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Ayrıca {calendarappstoreopen}Takvim Uygulamasını{linkclose} kurun ya da {calendardocopen}bilgisayarınızı ya da taşınabilir aygıtınızı eşitlemek üzere bağlayın ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Lütfen {emailopen}e-posta sunucusunu{linkclose} doğru ayarladığınızdan emin olun.",
"Calendar server" : "Takvim sunucusu",
- "Send invitations to attendees" : "Katılımcılara çağrıları gönder",
+ "Send invitations to attendees" : "Katılımcılara davet gönder",
"Automatically generate a birthday calendar" : "Doğum günü takvimi otomatik oluşturulsun",
- "Birthday calendars will be generated by a background job." : "Bu seçenek etkinleştirildiğinde, doğum günü takvimi arka plan görevi olarak oluşturulur.",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Etkinleştirildikten hemen sonra görüntülenmez, bir süre sonra görüntülenir.",
+ "Birthday calendars will be generated by a background job." : "Açıldığında, arka plan görevi olarak doğum günü takvimi oluşturulur.",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "Açıldıktan hemen sonra değil bir süre sonra görüntülenir.",
"Send notifications for events" : "Etkinlik bildirimleri gönderilsin",
"Notifications are sent via background jobs, so these must occur often enough." : "Bildirimler arka plan işlemleri tarafından gönderilir. Bu nedenle sıklık değeri uygun şekilde ayarlanmalıdır.",
"Send reminder notifications to calendar sharees as well" : "Takvim paylaşımlarına da hatırlatma bildirimleri gönderilsin",
- "Reminders are always sent to organizers and attendees." : "Bu seçenek etkinleştirildiğinde, hatırlatıcılar her zaman düzenleyici ve katılımcılara gönderilir.",
- "Enable notifications for events via push" : "Anında etkinlik bildirimleri kullanılsın",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Ayrıca {calendarappstoreopen}Takvim Uygulamasını{linkclose} kurun ya da {calendardocopen}bilgisayarınızı ya da taşınabilir aygıtınızı eşitlemek üzere bağlayın ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Lütfen {emailopen}e-posta sunucusunu{linkclose} doğru ayarladığınızdan emin olun.",
+ "Reminders are always sent to organizers and attendees." : "Açıldığında, anımsatıcılar düzenleyici ve katılımcılara her zaman gönderilir.",
+ "Enable notifications for events via push" : "Anında etkinlik bildirimlerini aç",
+ "Example content" : "Örnek içerik",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Örnek içerik, Nextcloud özelliklerinin sunulmasını sağlar. Varsayılan içerik Nextcloud ile birlikte gelir ve özel içerikle değiştirilebilir.",
"There was an error updating your attendance status." : "Katılım durumunuz güncellenirken bir sorun çıktı.",
"Please contact the organizer directly." : "Lütfen düzenleyici ile doğrudan görüşün.",
- "Are you accepting the invitation?" : "Çağrıyı kabul ediyor musunuz?",
+ "Are you accepting the invitation?" : "Daveti kabul ediyor musunuz?",
"Tentative" : "Kesin değil",
- "Number of guests" : "Konuk sayısı",
- "Comment" : "Yorum",
- "Your attendance was updated successfully." : "Katılımınız güncellendi.",
- "Calendar and tasks" : "Takvim ve görevler"
+ "Your attendance was updated successfully." : "Katılımınız güncellendi."
},
"nplurals=2; plural=(n > 1);");
diff --git a/apps/dav/l10n/tr.json b/apps/dav/l10n/tr.json
index b80217bfa8d..31d00ec53eb 100644
--- a/apps/dav/l10n/tr.json
+++ b/apps/dav/l10n/tr.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "Takvim",
- "Todos" : "Yapılacak işler",
+ "Tasks" : "Görevler",
"Personal" : "Kişisel",
"{actor} created calendar {calendar}" : "{actor}, {calendar} takvimini ekledi",
"You created calendar {calendar}" : "{calendar} takvimini eklediniz",
@@ -23,36 +23,41 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor}, {calendar} takvimini {group} grubu ile paylaştı",
"You unshared calendar {calendar} from group {group}" : "{calendar} takviminin {group} grubu ile paylaşımını kaldırdınız",
"{actor} unshared calendar {calendar} from group {group}" : "{actor}, {calendar} takviminin {group} grubu ile paylaşımını kaldırdı",
+ "Untitled event" : "Adlandırılmamış etkinlik",
"{actor} created event {event} in calendar {calendar}" : "{actor}, {calendar} takvimine {event} etkinliğini ekledi",
"You created event {event} in calendar {calendar}" : "{calendar} takvimine {event} etkinliğini eklediniz",
"{actor} deleted event {event} from calendar {calendar}" : "{actor}, {calendar} takviminden {event} etkinliğini sildi",
"You deleted event {event} from calendar {calendar}" : "{calendar} takviminden {event} etkinliğini sildiniz",
"{actor} updated event {event} in calendar {calendar}" : "{actor}, {calendar} takvimindeki {event} etkinliğini güncelledi",
"You updated event {event} in calendar {calendar}" : "{calendar} takvimindeki {event} etkinliğini güncellediniz",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor}, {event} etkinliğini {sourceCalendar} takviminden {targetCalendar} takvimine taşıdı",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{event} etkinliğini {sourceCalendar} takviminden {targetCalendar} takvimine taşıdınız",
"{actor} restored event {event} of calendar {calendar}" : "{actor}, {calendar} takvimindeki {event} etkinliğini geri yükledi",
"You restored event {event} of calendar {calendar}" : "{calendar} takvimindeki {event} etkinliğini geri yüklediniz",
"Busy" : "Meşgul",
- "{actor} created todo {todo} in list {calendar}" : "{actor}, {calendar} takvimi listesine {todo} yapılacak işini ekledi",
- "You created todo {todo} in list {calendar}" : "{calendar} takvimi listesine {todo} yapılacak işini eklediniz",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor}, {calendar} takvimi listesinden {todo} yapılacak işini sildi",
- "You deleted todo {todo} from list {calendar}" : "{calendar} takvimi listesinden {todo} yapılacak işini sildiniz",
- "{actor} updated todo {todo} in list {calendar}" : "{actor}, {calendar} takvimi listesindeki {todo} yapılacak işini güncelledi",
- "You updated todo {todo} in list {calendar}" : "{calendar} takvimi listesindeki {todo} yapılacak işini güncellediniz",
- "{actor} solved todo {todo} in list {calendar}" : "{actor}, {calendar} takvimi listesindeki {todo} yapılacak işini tamamladı",
- "You solved todo {todo} in list {calendar}" : "{calendar} takvimi listesindeki {todo} yapılacak işini tamamladınız",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor}, {calendar} takvimi listesindeki {todo} yapılacak işini yeniden başlattı",
- "You reopened todo {todo} in list {calendar}" : "{calendar} takvimi listesindeki {todo} yapılacak işini yeniden başlattınız",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor}, {calendar} takvimi listesine {todo} yapılacak işini ekledi",
+ "You created to-do {todo} in list {calendar}" : "{calendar} takvimi listesine {todo} yapılacak işini eklediniz",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor}, {calendar} takvimi listesinden {todo} yapılacak işini sildi",
+ "You deleted to-do {todo} from list {calendar}" : "{calendar} takvimi listesinden {todo} yapılacak işini sildiniz",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor}, {calendar} takvimi listesindeki {todo} yapılacak işini güncelledi",
+ "You updated to-do {todo} in list {calendar}" : "{calendar} takvimi listesindeki {todo} yapılacak işini güncellediniz",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor}, {calendar} takvimi listesindeki {todo} yapılacak işini tamamladı",
+ "You solved to-do {todo} in list {calendar}" : "{calendar} takvimi listesindeki {todo} yapılacak işini tamamladınız",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor}, {calendar} takvimi listesindeki {todo} yapılacak işini yeniden başlattı",
+ "You reopened to-do {todo} in list {calendar}" : "{calendar} takvimi listesindeki {todo} yapılacak işini yeniden başlattınız",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor}, {todo} yapılacak işini {sourceCalendar} listesinden {targetCalendar} listesine taşıdı",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{todo} yapılacak işini {sourceCalendar} listesinden {targetCalendar} listesine taşıdınız",
"Calendar, contacts and tasks" : "Takvim, kişiler ve görevler",
"A <strong>calendar</strong> was modified" : "Bir <strong>takvim</strong> düzenlendi",
"A calendar <strong>event</strong> was modified" : "Bir takvim <strong>etkinliği</strong> düzenlendi",
- "A calendar <strong>todo</strong> was modified" : "Bir takvim <strong>yapılacak işi</strong> düzenlendi",
+ "A calendar <strong>to-do</strong> was modified" : "Bir takvim <strong>yapılacak işi</strong> düzenlendi",
"Contact birthdays" : "Kişi doğum günleri",
"Death of %s" : "%s ölümü",
+ "Untitled calendar" : "Adlandırılmamış takvim",
"Calendar:" : "Takvim:",
"Date:" : "Tarih:",
"Where:" : "Yer:",
"Description:" : "Açıklama:",
- "Untitled event" : "Adsız etkinlik",
"_%n year_::_%n years_" : ["%n yıl","%n yıl"],
"_%n month_::_%n months_" : ["%n ay","%n ay"],
"_%n day_::_%n days_" : ["%n gün","%n gün"],
@@ -65,22 +70,129 @@
"Description: %s" : "Açıklama: %s",
"Where: %s" : "Şurada: %s",
"%1$s via %2$s" : "%1$s, %2$s aracılığıyla",
+ "In the past on %1$s for the entire day" : "Tüm gün boyunca %1$s zamanında geçmişte",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["Tüm gün boyunca %1$s zamanında bir dakika içinde","Tüm gün boyunca %1$s zamanında %n dakika içinde"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["Tüm gün boyunca %1$s zamanında bir saat içinde","Tüm gün boyunca %1$s zamanında %n saat içinde"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["Tüm gün boyunca %1$s zamanında bir gün içinde","Tüm gün boyunca %1$s zamanında %n gün içinde"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Tüm gün boyunca %1$s zamanında bir hafta içinde","Tüm gün boyunca %1$s zamanında %n hafta içinde"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Tüm gün boyunca %1$s zamanında bir ay içinde","Tüm gün boyunca %1$s zamanında %n ay içinde"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Tüm gün boyunca %1$s zamanında bir yıl içinde","Tüm gün boyunca %1$s zamanında %n yıl içinde"],
+ "In the past on %1$s between %2$s - %3$s" : "Geçmişte %1$s zamanında %2$s ile %3$s arasında",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Bir dakika içinde %1$s zamanında %2$s ile %3$s arasında","%n dakika içinde %1$s zamanında %2$s ile %3$s arasında"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Bir saat içinde %1$s zamanında %2$s ile %3$s arasında","%n saat içinde %1$s zamanında %2$s ile %3$s arasında"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Bir gün içinde %1$s zamanında %2$s ile %3$s arasında","%n gün içinde %1$s zamanında %2$s ile %3$s arasında"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Bir hafta içinde %1$s zamanında %2$s ile %3$s arasında","%n hafta içinde %1$s zamanında %2$s ile %3$s arasında"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Bir ay içinde %1$s zamanında %2$s ile %3$s arasında","%n ay içinde %1$s zamanında %2$s ile %3$s arasında"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["Bir yıl içinde %1$s zamanında %2$s ile %3$s arasında","%n yıl içinde %1$s zamanında %2$s ile %3$s arasında"],
+ "Could not generate when statement" : "Zaman ifadesi oluşturulamadı",
+ "Every Day for the entire day" : "Her gün tüm gün boyunca",
+ "Every Day for the entire day until %1$s" : "%1$s zamanına kadar her gün tüm gün boyunca",
+ "Every Day between %1$s - %2$s" : "Her gün %1$s ile %2$s arasında",
+ "Every Day between %1$s - %2$s until %3$s" : "%3$s zamanına kadar %1$s ile %2$s arasında her gün",
+ "Every %1$d Days for the entire day" : "Her %1$d günde tüm gün boyunca",
+ "Every %1$d Days for the entire day until %2$s" : "%2$s zamanına kadar her %1$d günde tüm gün boyunca",
+ "Every %1$d Days between %2$s - %3$s" : "Her %1$d günde %2$s ile %3$s arasında",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "%4$s zamanına kadar her %1$d günde %2$s ile %3$s",
+ "Could not generate event recurrence statement" : "Etkinlik yinelenme ifadesi oluşturulamadı",
+ "Every Week on %1$s for the entire day" : "Her hafta %1$s zamanında tüm gün boyunca",
+ "Every Week on %1$s for the entire day until %2$s" : "Her hafta %1$s zamanında tüm gün boyunca %2$s zamanına kadar",
+ "Every Week on %1$s between %2$s - %3$s" : "Her hafta %1$s zamanında %2$s ile %3$s arasında",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Her hafta %1$s zamanında %2$s ile %3$s arasında %4$s zamanına kadar",
+ "Every %1$d Weeks on %2$s for the entire day" : "Her %1$d haftada bir %2$s zamanında tüm gün boyunca",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Her %1$d haftada bir %2$s zamanında tüm gün boyunca %3$s zamanına kadar",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Her %1$d haftada bir %2$s zamanında tüm gün boyunca %3$s ile %4$s arasında",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Her %1$d haftada bir %2$s zamanında %3$s ile %4$s arasında %5$s zamanına kadar",
+ "Every Month on the %1$s for the entire day" : "Her ay %1$s zamanında tüm gün boyunca",
+ "Every Month on the %1$s for the entire day until %2$s" : "Her ay %1$s zamanında tüm gün boyunca %2$s zamanına kadar",
+ "Every Month on the %1$s between %2$s - %3$s" : "Her ay %1$s zamanında %2$s ile %3$s arasında",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Her ay %1$s zamanında %2$s ile %3$s arasında %4$s zamanına kadar",
+ "Every %1$d Months on the %2$s for the entire day" : "Her %1$d ayda bir %2$s zamanında tüm gün boyunca",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Her %1$d ayda bir %2$s zamanında tüm gün boyunca %3$s zamanına kadar",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Her %1$d ayda bir %2$s zamanında tüm gün boyunca %3$s ile %4$s arasında",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Her %1$d ayda bir %2$s zamanında %3$s ile %4$s arasında %5$s zamanına kadar",
+ "Every Year in %1$s on the %2$s for the entire day" : "Her yıl %1$s içinde %2$s zamanında tüm gün boyunca",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Her yıl %1$s içinde %2$s zamanında tüm gün boyunca %3$s zamanına kadar",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Her yıl %1$s içinde %2$s zamanında tüm gün boyunca %3$s ile %4$s arasında",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Her yıl %1$s içinde %2$s zamanında %3$s ile %4$s arasında %5$s zamanına kadar",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Her %1$d yılda bir %2$s içinde %3$s zamanında tüm gün boyunca",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Her %1$d yılda bir %2$s içinde %3$s zamanında tüm gün boyunca %4$s zamanına kadar",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Her %1$d yılda bir %2$s içinde %3$s zamanında tüm gün boyunca %4$s ile %5$s arasında",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Her %1$d yılda bir %2$s içinde %3$s zamanında %4$s ile %5$s arasında %6$s zamanına kadar",
+ "On specific dates for the entire day until %1$s" : "Belirli tarihlerde tüm gün boyunca %1$s zamanına kadar",
+ "On specific dates between %1$s - %2$s until %3$s" : "Belirli tarihlerde %1$s ile %2$s arasında %3$s zamanına kadar",
+ "In the past on %1$s" : "%1$s zamanında geçmişte",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["%1$s zamanında bir dakika içinde","%1$s zamanında %n dakika içinde"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["%1$s zamanında bir saat içinde","%1$s zamanında %n saat içinde"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["%1$s zamanında bir gün içinde","%1$s zamanında %n gün içinde"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["%1$s zamanında bir hafta içinde","%1$s zamanında %n hafta içinde"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["%1$s zamanında bir ay içinde","%1$s zamanında %n ay içinde"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["%1$s zamanında bir yıl içinde","%1$s zamanında %n yıl içinde"],
+ "In the past on %1$s then on %2$s" : "Geçmişte %1$s zamanında ardından %2$s zamanında",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Bir dakika içinde %1$s zamanında ardından %2$s zamanında","%n dakika içinde %1$s zamanında ardından %2$s zamanında"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Bir saat içinde %1$s zamanında ardından %2$s zamanında","%n saat içinde %1$s zamanında ardından %2$s zamanında"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Bir gün içinde %1$s zamanında ardından %2$s zamanında","%n gün içinde %1$s zamanında ardından %2$s zamanında"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Bir hafta içinde %1$s zamanında ardından %2$s zamanında","%n hafta içinde %1$s zamanında ardından %2$s zamanında"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Bir ay içinde %1$s zamanında ardından %2$s zamanında","%n ay içinde %1$s zamanında ardından %2$s zamanında"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Bir yıl içinde %1$s zamanında ardından %2$s zamanında","%n yıl içinde %1$s zamanında ardından %2$s zamanında"],
+ "In the past on %1$s then on %2$s and %3$s" : "Geçmişte %1$s zamanında ardından %2$s ve %3$s zamanında",
+ "_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_" : ["Bir dakika içinde %1$s zamanında ardından %2$s ve %3$s zamanında","%n dakika içinde %1$s zamanında ardından %2$s ve %3$s zamanında"],
+ "_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_" : ["Bir saat içinde %1$s zamanında ardından %2$s ve %3$s zamanında","%n saat içinde %1$s zamanında ardından %2$s ve %3$s zamanında"],
+ "_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_" : ["Bir gün içinde %1$s zamanında ardından %2$s ve %3$s zamanında","%n gün içinde %1$s zamanında ardından %2$s ve %3$s zamanında"],
+ "_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_" : ["Bir hafta içinde %1$s zamanında ardından %2$s ve %3$s zamanında","%n hafta içinde %1$s zamanında ardından %2$s ve %3$s zamanında"],
+ "_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_" : ["Bir ay içinde %1$s zamanında ardından %2$s ve %3$s zamanında","%n ay içinde %1$s zamanında ardından %2$s ve %3$s zamanında"],
+ "_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_" : ["Bir yıl içinde %1$s zamanında ardından %2$s ve %3$s zamanında","%n yıl içinde %1$s zamanında ardından %2$s ve %3$s zamanında"],
+ "Could not generate next recurrence statement" : "Sonraki yinelenme ifadesi oluşturulamadı",
"Cancelled: %1$s" : "İptal edildi: %1$s",
- "Invitation canceled" : "Çağrı iptal edildi",
+ "\"%1$s\" has been canceled" : "\"%1$s\" iptal edildi",
"Re: %1$s" : "Ynt: %1$s",
- "Invitation updated" : "Çağrı güncellendi",
- "Invitation: %1$s" : "Çağrı: %1$s",
- "Invitation" : "Çağrı",
+ "%1$s has accepted your invitation" : "%1$s davetinizi kabul etti",
+ "%1$s has tentatively accepted your invitation" : "%1$s davetinizi belirsiz olarak kabul etti",
+ "%1$s has declined your invitation" : "%1$s davetinizi reddetti.",
+ "%1$s has responded to your invitation" : "%1$s davetinizi yanıtladı",
+ "Invitation updated: %1$s" : "Davet güncellendi: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s, \"%2$s\" etkinliğini güncelledi",
+ "Invitation: %1$s" : "Davet: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s, size \"%2$s\" için davet gönderdi",
+ "Organizer:" : "Düzenleyen:",
+ "Attendees:" : "Katılımcılar:",
"Title:" : "Başlık:",
- "Time:" : "Zaman:",
+ "When:" : "Zaman:",
"Location:" : "Konum:",
"Link:" : "Bağlantı:",
- "Organizer:" : "Düzenleyen:",
- "Attendees:" : "Katılımcılar:",
+ "Occurring:" : "Başlangıç:",
"Accept" : "Kabul et",
"Decline" : "Reddet",
"More options …" : "Diğer seçenekler …",
"More options at %s" : "%s üzerindeki diğer seçenekler",
+ "Monday" : "Pazartesi",
+ "Tuesday" : "Salı",
+ "Wednesday" : "Çarşamba",
+ "Thursday" : "Perşembe",
+ "Friday" : "Cuma",
+ "Saturday" : "Cumartesi",
+ "Sunday" : "Pazar",
+ "January" : "Ocak",
+ "February" : "Şubat",
+ "March" : "Mart",
+ "April" : "Nisan",
+ "May" : "Mayıs",
+ "June" : "Haziran",
+ "July" : "Temmuz",
+ "August" : "Ağustos",
+ "September" : "Eylül",
+ "October" : "Ekim",
+ "November" : "Kasım",
+ "December" : "Aralık",
+ "First" : "İlk",
+ "Second" : "İkinci",
+ "Third" : "Üçüncü",
+ "Fourth" : "Dördüncü",
+ "Fifth" : "Beşinci",
+ "Last" : "Son",
+ "Second Last" : "İkinci son",
+ "Third Last" : "Üçüncü son",
+ "Fourth Last" : "Dördüncü son",
+ "Fifth Last" : "Beşinci son",
"Contacts" : "Kişiler",
"{actor} created address book {addressbook}" : "{actor}, {addressbook} adres defterini ekledi",
"You created address book {addressbook}" : "{addressbook} adres defterini eklediniz",
@@ -105,8 +217,11 @@
"You deleted contact {card} from address book {addressbook}" : "{addressbook} adres defterinden {card} kişi kartını sildiniz",
"{actor} updated contact {card} in address book {addressbook}" : "{actor}, {addressbook} adres defterindeki {card} kişi kartını güncelledi",
"You updated contact {card} in address book {addressbook}" : "{addressbook} adres defterindeki {card} kişi kartını güncellediniz",
- "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Bir <strong>kişi</strong> ya da <strong>adres defteri</strong> değiştirildi",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Bir <strong>kişi</strong> ya da <strong>adres defteri</strong> değiştirildiğinde",
+ "Accounts" : "Hesaplar",
+ "System address book which holds all accounts" : "Tüm hesapların bulunduğu sistem adres defteri",
"File is not updatable: %1$s" : "Dosya güncellenebilir değil: %1$s",
+ "Failed to get storage for file" : "Dosyanın depolaması alınamadı",
"Could not write to final file, canceled by hook" : "Sonuç dosyasına yazılamadı, bağlantı tarafından iptal edildi",
"Could not write file contents" : "Dosya içerikleri yazılamadı",
"_%n byte_::_%n bytes_" : ["%n bayt","%n bayt"],
@@ -115,64 +230,107 @@
"Could not rename part file to final file, canceled by hook" : "Parça dosyası sonuç dosyası olarak yeniden adlandırılamadı, bağlantı tarafından iptal edildi",
"Could not rename part file to final file" : "Parça dosyası sonuç dosyası olarak yeniden adlandırılamadı",
"Failed to check file size: %1$s" : "Dosya boyutu denetlenemedi: %1$s",
- "Could not open file" : "Dosya açılamadı",
+ "Could not open file: %1$s, file does seem to exist" : "Dosya açılamadı: %1$s, dosya var gibi görünüyor",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Dosya açılamadı: %1$s, dosya var gibi görünmüyor",
"Encryption not ready: %1$s" : "Şifreleme hazır değil: %1$s",
"Failed to open file: %1$s" : "Dosya açılamadı: %1$s",
"Failed to unlink: %1$s" : "Bağlantı kaldırılamadı: %1$s",
- "Invalid chunk name" : "Parça adı geçersiz",
- "Could not rename part file assembled from chunks" : "Parçalardan oluşturulan parça dosyası yeniden adlandırılamadı ",
"Failed to write file contents: %1$s" : "Dosya içerikleri yazılamadı: %1$s",
"File not found: %1$s" : "Dosya bulunamadı: %1$s",
+ "Invalid target path" : "Hedef yol geçersiz",
"System is in maintenance mode." : "Sistem bakım kipinde.",
"Upgrade needed" : "Yükseltme gerekiyor",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "iOS/macOS üzerinde CalDAV ve CardDAV kullanabilmek için %s HTTPS kullanacak şekilde yapılandırılmalıdır.",
"Configures a CalDAV account" : "Bir CalDAV hesabı yapılandırır",
"Configures a CardDAV account" : "Bir CardDAV hesabı yapılandırır",
"Events" : "Etkinlikler",
- "Tasks" : "Görevler",
- "Untitled task" : "Adsız görev",
+ "Untitled task" : "Adlandırılmamış görev",
"Completed on %s" : "%s tarihinde tamamlandı",
"Due on %s by %s" : "%s tarihine kadar %s tarafından",
"Due on %s" : "%s tarihine kadar",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Nextcloud Takvim uygulamasına hoş geldiniz!\n\nBu bir örnek etkinliktir. İstediğiniz düzenlemeleri yaparak Nextcloud Takvim ile planlamanın esnekliğini keşfedin!\n\nNextcloud Takvim ile şunları yapabilirsiniz:\n- Etkinlikleri kolayca oluşturabilir, düzenleyebilir ve yönetebilirsiniz.\n- Birden fazla takvim oluşturabilir ve bunları takım arkadaşlarınız, arkadaşlarınız veya ailenizle paylaşabilirsiniz.\n- Uygunluğunuzu kontrol edebilir ve yoğun zamanlarınızı başkalarına gösterebilirsiniz.\n- CalDAV aracılığıyla uygulamaları ve aygıtları sorunsuz bir şekilde bütünleştirebilirsiniz.\n- Deneyiminizi özelleştirebilirsiniz: Yinelenen etkinlikler planlayabilir, bildirimleri ve diğer ayarları ayarlayabilirsiniz.",
+ "Example event - open me!" : "Örnek etkinlik. Beni aç!",
+ "System Address Book" : "Sistem adres defteri",
+ "The system address book contains contact information for all users in your instance." : "Sistem adres defterinde, kopyanızdaki tüm kullanıcıların iletişim bilgileri bulunur.",
+ "Enable System Address Book" : "Sistem adres defteri kullanılsın",
+ "DAV system address book" : "DAV sistem adres defteri",
+ "No outstanding DAV system address book sync." : "Bekleyen bir DAV sistemi adres defteri eşitlemesi yok.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Kopyanızda 1000 üzerinde kullanıcı olduğundan ya da bir sorun çıktığından DAV sistemi adres defteri eşitlemesi henüz yapılmamış. Lütfen \"occ dav:sync-system-addressbook\" komutunu yürüterek el ile eşitleyin.",
+ "WebDAV endpoint" : "WebDAV bağlantı noktası",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Site sunucunuzun WebDAV üzerinden dosya eşitlemesi için doğru şekilde ayarlanıp ayarlanmadığı denetlenemedi. Lütfen el ile denetleyin.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Site sunucunuz dosya eşitlemesi için doğru şekilde ayarlanmamış. WebDAV arabirimi sorunlu görünüyor.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "Site sunucunuz WebDAV üzerinden dosya eşitlemesi için doğru şekilde ayarlanmış.",
"Migrated calendar (%1$s)" : "Aktarılmış takvim (%1$s)",
"Calendars including events, details and attendees" : "Etkinlikler, bilgiler ve katılımcılar ile takvimler",
"Contacts and groups" : "Kişiler ve gruplar",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV bağlantı noktası",
- "Availability" : "Kullanılabilirlik",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Çalışma saatlerinizi ayarlarsanız, diğer kullanıcılar bir toplantı ayarladıklarında ofis dışında olduğunuzu görürler.",
+ "Absence saved" : "Bulunmama kaydedildi",
+ "Failed to save your absence settings" : "Bulunmama ayarlarınız kaydedilemedi",
+ "Absence cleared" : "Bulunmama temizlendi",
+ "Failed to clear your absence settings" : "Bulunmama ayarlarınız temizlenemedi",
+ "First day" : "İlk gün",
+ "Last day (inclusive)" : "Son gün (dahil)",
+ "Out of office replacement (optional)" : "Ofis dışındayken yerine bakacak kişi (isteğe bağlı)",
+ "Name of the replacement" : "Yerine bakacak kişi adı",
+ "No results." : "Herhangi bir sonuç bulunamadı.",
+ "Start typing." : "Yazmaya başlayın.",
+ "Short absence status" : "Kısa bulunmama durumu",
+ "Long absence Message" : "Uzun bulunmama iletisi",
+ "Save" : "Kaydet",
+ "Disable absence" : "Bulunmamayı kapat",
+ "Failed to load availability" : "Uygunluk yüklenemedi",
+ "Saved availability" : "Uygunluk kaydedildi",
+ "Failed to save availability" : "Uygunluk kaydedilemedi",
"Time zone:" : "Saat dilimi:",
"to" : "ile",
"Delete slot" : "Aralığı sil",
"No working hours set" : "Çalışma saatleri ayarlanmamış",
"Add slot" : "Aralık ekle",
- "Monday" : "Pazartesi",
- "Tuesday" : "Salı",
- "Wednesday" : "Çarşamba",
- "Thursday" : "Perşembe",
- "Friday" : "Cuma",
- "Saturday" : "Cumartesi",
- "Sunday" : "Pazar",
- "Save" : "Kaydet",
+ "Weekdays" : "Hafta içi günleri",
+ "Pick a start time for {dayName}" : "{dayName} için başlangıç zamanını seçin",
+ "Pick a end time for {dayName}" : "{dayName} için bitiş zamanını seçin",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Tüm bildirimleri sessize almak için, uygunluk durumu dışında kullanıcı durumu otomatik olarak \"Rahatsız etmeyin\" olarak ayarlanır.",
+ "Cancel" : "İptal",
+ "Import" : "İçe aktar",
+ "Error while saving settings" : "Ayarlar kaydedilirken sorun çıktı",
+ "Contact reset successfully" : "Kişi sıfırlandı",
+ "Error while resetting contact" : "Kişi sıfırlanırken sorun çıktı",
+ "Contact imported successfully" : "Kişi içe aktarıldı",
+ "Error while importing contact" : "Kişi içe aktarılırken sorun çıktı",
+ "Import contact" : "Kişiyi içe aktar",
+ "Reset to default" : "Varsayılanlara dön",
+ "Import contacts" : "Kişileri içe aktar",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Yeni bir .vcf dosyasını içe aktarmak, var olan varsayılan kişiyi siler ve yenisiyle değiştirir. İlerlemek istiyor musunuz?",
+ "Failed to save example event creation setting" : "Örnek etkinlik oluşturma ayarı kaydedilemedi",
+ "Failed to upload the example event" : "Örnek etkinlik yüklenemedi",
+ "Custom example event was saved successfully" : "Özel örnek etkinlik kaydedildi",
+ "Failed to delete the custom example event" : "Özel örnek etkinlik silinemedi",
+ "Custom example event was deleted successfully" : "Özel örnek etkinlik silindi",
+ "Import calendar event" : "Takvim etkinliğini içe aktar",
+ "Uploading a new event will overwrite the existing one." : "Yeni bir etkinlik yüklendiğinde eskisinin üzerine yazılır.",
+ "Upload event" : "Etkinlik yükle",
+ "Availability" : "Uygunluk",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Çalışma saatlerinizi ayarlarsanız, diğer kişiler bir toplantı ayarladıklarında ofis dışında olduğunuzu görürler.",
+ "Absence" : "Bulunmama",
+ "Configure your next absence period." : "Sonraki bulunmama aralığınızı yapılandırın.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Ayrıca {calendarappstoreopen}Takvim Uygulamasını{linkclose} kurun ya da {calendardocopen}bilgisayarınızı ya da taşınabilir aygıtınızı eşitlemek üzere bağlayın ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Lütfen {emailopen}e-posta sunucusunu{linkclose} doğru ayarladığınızdan emin olun.",
"Calendar server" : "Takvim sunucusu",
- "Send invitations to attendees" : "Katılımcılara çağrıları gönder",
+ "Send invitations to attendees" : "Katılımcılara davet gönder",
"Automatically generate a birthday calendar" : "Doğum günü takvimi otomatik oluşturulsun",
- "Birthday calendars will be generated by a background job." : "Bu seçenek etkinleştirildiğinde, doğum günü takvimi arka plan görevi olarak oluşturulur.",
- "Hence they will not be available immediately after enabling but will show up after some time." : "Etkinleştirildikten hemen sonra görüntülenmez, bir süre sonra görüntülenir.",
+ "Birthday calendars will be generated by a background job." : "Açıldığında, arka plan görevi olarak doğum günü takvimi oluşturulur.",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "Açıldıktan hemen sonra değil bir süre sonra görüntülenir.",
"Send notifications for events" : "Etkinlik bildirimleri gönderilsin",
"Notifications are sent via background jobs, so these must occur often enough." : "Bildirimler arka plan işlemleri tarafından gönderilir. Bu nedenle sıklık değeri uygun şekilde ayarlanmalıdır.",
"Send reminder notifications to calendar sharees as well" : "Takvim paylaşımlarına da hatırlatma bildirimleri gönderilsin",
- "Reminders are always sent to organizers and attendees." : "Bu seçenek etkinleştirildiğinde, hatırlatıcılar her zaman düzenleyici ve katılımcılara gönderilir.",
- "Enable notifications for events via push" : "Anında etkinlik bildirimleri kullanılsın",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Ayrıca {calendarappstoreopen}Takvim Uygulamasını{linkclose} kurun ya da {calendardocopen}bilgisayarınızı ya da taşınabilir aygıtınızı eşitlemek üzere bağlayın ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Lütfen {emailopen}e-posta sunucusunu{linkclose} doğru ayarladığınızdan emin olun.",
+ "Reminders are always sent to organizers and attendees." : "Açıldığında, anımsatıcılar düzenleyici ve katılımcılara her zaman gönderilir.",
+ "Enable notifications for events via push" : "Anında etkinlik bildirimlerini aç",
+ "Example content" : "Örnek içerik",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Örnek içerik, Nextcloud özelliklerinin sunulmasını sağlar. Varsayılan içerik Nextcloud ile birlikte gelir ve özel içerikle değiştirilebilir.",
"There was an error updating your attendance status." : "Katılım durumunuz güncellenirken bir sorun çıktı.",
"Please contact the organizer directly." : "Lütfen düzenleyici ile doğrudan görüşün.",
- "Are you accepting the invitation?" : "Çağrıyı kabul ediyor musunuz?",
+ "Are you accepting the invitation?" : "Daveti kabul ediyor musunuz?",
"Tentative" : "Kesin değil",
- "Number of guests" : "Konuk sayısı",
- "Comment" : "Yorum",
- "Your attendance was updated successfully." : "Katılımınız güncellendi.",
- "Calendar and tasks" : "Takvim ve görevler"
+ "Your attendance was updated successfully." : "Katılımınız güncellendi."
},"pluralForm" :"nplurals=2; plural=(n > 1);"
} \ No newline at end of file
diff --git a/apps/dav/l10n/ug.js b/apps/dav/l10n/ug.js
new file mode 100644
index 00000000000..5915a185740
--- /dev/null
+++ b/apps/dav/l10n/ug.js
@@ -0,0 +1,268 @@
+OC.L10N.register(
+ "dav",
+ {
+ "Calendar" : "يىلنامە",
+ "Tasks" : "ۋەزىپەلەر",
+ "Personal" : "شەخسىي",
+ "{actor} created calendar {calendar}" : "{actor} قۇرغان كالېندار {calendar}",
+ "You created calendar {calendar}" : "كالېندار {calendar} قۇردىڭىز",
+ "{actor} deleted calendar {calendar}" : "{actor} ئۆچۈرۈلگەن كالېندار {calendar}",
+ "You deleted calendar {calendar}" : "كالېندار {calendar} ئۆچۈردىڭىز",
+ "{actor} updated calendar {calendar}" : "{actor} يېڭىلانغان كالېندار {calendar}",
+ "You updated calendar {calendar}" : "كالېندار {calendar} يېڭىلىدىڭىز",
+ "{actor} restored calendar {calendar}" : "{actor} ئەسلىگە كەلتۈرۈلگەن كالېندار {calendar}",
+ "You restored calendar {calendar}" : "كالېندار {calendar} ئەسلىگە كەلتۈردىڭىز",
+ "You shared calendar {calendar} as public link" : "سىز كالېندار {calendar} public نى ئاممىۋى ئۇلىنىش سۈپىتىدە ھەمبەھىرلىدىڭىز",
+ "You removed public link for calendar {calendar}" : "كالېندار {calendar} ئۈچۈن ئاممىۋى ئۇلىنىشنى ئۆچۈردىڭىز",
+ "{actor} shared calendar {calendar} with you" : "{actor} ئورتاقلاشقان كالېندار {calendar} سىز بىلەن",
+ "You shared calendar {calendar} with {user}" : "سىز كالېندار {calendar} نى {user} بىلەن ئورتاقلاشتىڭىز",
+ "{actor} shared calendar {calendar} with {user}" : "{actor} ئورتاقلاشقان كالېندار {calendar} {user} بىلەن",
+ "{actor} unshared calendar {calendar} from you" : "{actor} ئورتاقلاشمىغان كالېندار {calendar} سىزدىن",
+ "You unshared calendar {calendar} from {user}" : "سىز كالىندار {calendar} نى {user} دىن ھەمبەرلەنمىدىڭىز",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor} ئورتاقلاشمىغان كالېندار {calendar} {user}",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} ئورتاقلاشمىغان كالېندار {calendar} ئۆزىدىن",
+ "You shared calendar {calendar} with group {group}" : "كالېندار {calendar} group گۇرۇپپا {group} بىلەن ئورتاقلاشتىڭىز",
+ "{actor} shared calendar {calendar} with group {group}" : "{actor} گۇرۇپپا {calendar} بىلەن ئورتاقلاشقان كالېندار {group}",
+ "You unshared calendar {calendar} from group {group}" : "گۇرۇپپا {calendar} ئورتاقلاشمىغان كالېندار {group}",
+ "{actor} unshared calendar {calendar} from group {group}" : "{actor} بولسا كالىندار {calendar} نى گۇرۇپپا {group} بىلەن ھەمبەھىرلىمىدى",
+ "Untitled event" : "نامسىز ھادىسە",
+ "{actor} created event {event} in calendar {calendar}" : "{actor} كالېندار {event} پائالىيەت {calendar} قۇرغان}",
+ "You created event {event} in calendar {calendar}" : "كالېندار {event} پائالىيەت {calendar} created قۇردىڭىز",
+ "{actor} deleted event {event} from calendar {calendar}" : "{actor} ئۆچۈرۈلگەن ھادىسە {event} كالېنداردىن {calendar}",
+ "You deleted event {event} from calendar {calendar}" : "كالېندار {event} ھادىسە {calendar} ئۆچۈردىڭىز",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} بولسا كالىندار {calendar} دىكى پائالىيەت {event} نى يېڭىلىدى",
+ "You updated event {event} in calendar {calendar}" : "كالېندار {event} پائالىيەت {calendar} نى يېڭىلىدىڭىز",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} پائالىيەت {event} كالېنداردىن {sourceCalendar} دىن كالېندارغا {targetCalendar} غا يۆتكەلدى",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "سىز ھادىسە {event} كالىندارىدىن {sourceCalendar} دىن كالېندار {targetCalendar} غا يۆتكىدىڭىز",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} ئەسلىگە كەلتۈرۈلگەن ھادىسە {event} كالىندارى {calendar}",
+ "You restored event {event} of calendar {calendar}" : "كالېندار {event} پائالىيەت {calendar} نى ئەسلىگە كەلتۈردىڭىز",
+ "Busy" : "ئالدىراش",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} list تىزىملىك {calendar} {todo} ئىجاد قىلىش",
+ "You created to-do {todo} in list {calendar}" : "سىز تىزىملىك {calendar} {todo} نى قۇردىڭىز",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} list تىزىملىكتىن {calendar} to ئۆچۈرۈلدى {todo}",
+ "You deleted to-do {todo} from list {calendar}" : "تىزىملىكتىن {todo} to نى ئۆچۈردىڭىز {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} تىزىملىك {calendar} {todo} يېڭىلانغان",
+ "You updated to-do {todo} in list {calendar}" : "تىزىملىك {calendar} دىكى {todo} نى يېڭىلىدىڭىز",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} بولسا {calendar} تىزىملىكتىكى {todo} نى ھەل قىلدى",
+ "You solved to-do {todo} in list {calendar}" : "تىزىملىك {calendar} دىكى {todo} نى ھەل قىلدىڭىز",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} تىزىملىك {calendar} {todo} قايتا ئېچىلدى",
+ "You reopened to-do {todo} in list {calendar}" : "تىزىملىك {calendar} دىكى {todo} نى قايتا ئاچتىڭىز",
+ "Calendar, contacts and tasks" : "كالېندار ، ئالاقىلىشىش ۋە ۋەزىپە",
+ "A <strong>calendar</strong> was modified" : "<strong> كالېندار </ strong> ئۆزگەرتىلدى",
+ "A calendar <strong>event</strong> was modified" : "كالېندار <strong> ھادىسە </ strong> ئۆزگەرتىلدى",
+ "A calendar <strong>to-do</strong> was modified" : "كالېندار <strong> قىلىش </ strong> ئۆزگەرتىلدى",
+ "Contact birthdays" : "تۇغۇلغان كۈنى بىلەن ئالاقىلىشىڭ",
+ "Death of %s" : "% S نىڭ ئۆلۈمى",
+ "Untitled calendar" : "نامسىز كالېندار",
+ "Calendar:" : "كالېندار:",
+ "Date:" : "چېسلا:",
+ "Where:" : "قەيەردە:",
+ "Description:" : "چۈشەندۈرۈش:",
+ "%s (in %s)" : "% s (% s)",
+ "%s (%s ago)" : "% s (% s ago)",
+ "Calendar: %s" : "كالېندار:% s",
+ "Date: %s" : "چېسلا:% s",
+ "Description: %s" : "چۈشەندۈرۈش:% s",
+ "Where: %s" : "قەيەردە:% s",
+ "%1$s via %2$s" : "%1 $ s ئارقىلىق%2 $ s",
+ "Could not generate when statement" : "بايان قىلغاندا ھاسىل قىلالمىدى",
+ "Every Day for the entire day" : "ھەر بىر كۈن پۈتۈن بىر كۈن",
+ "Every Day for the entire day until %1$s" : "ھەر بىر كۈن پۈتۈن كۈن%1 $ s غىچە",
+ "Every Day between %1$s - %2$s" : "ھەر كۈنى%1 $ s -%2 $ s ئارىلىقىدا",
+ "Every Day between %1$s - %2$s until %3$s" : "ھەر كۈنى%1 $ s -%2 $ s ئارىلىقىدا%3 $ s غىچە",
+ "Every %1$d Days for the entire day" : "پۈتۈن%1 $ d كۈن",
+ "Every %1$d Days for the entire day until %2$s" : "ھەر%1 $ d پۈتۈن بىر كۈن ئىچىدە%2 $ s غىچە",
+ "Every %1$d Days between %2$s - %3$s" : "ھەر%1 $ d كۈنلەر%2 $ s -%3 $ s ئارىلىقىدا",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "ھەر%1 $ d كۈنلىرى%2 $ s -%3 $ s ئارىلىقىدا%4 $ s غىچە",
+ "Could not generate event recurrence statement" : "ھادىسە قايتا-قايتا بايانات ھاسىل قىلالمىدى",
+ "Every Week on %1$s for the entire day" : "ھەر ھەپتە پۈتۈن كۈن ئۈچۈن%1 $ s",
+ "Every Week on %1$s for the entire day until %2$s" : "ھەر ھەپتە%1 $ s پۈتۈن بىر كۈن ئىچىدە%2 $ s غىچە",
+ "Every Week on %1$s between %2$s - %3$s" : "ھەر ھەپتە%1 $ s دىكى%2 $ s -%3 $ s ئارىلىقىدا",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "ھەر ھەپتە%1 $ s دىكى%2 $ s -%3 $ s ئارىلىقىدا%4 $ s غىچە",
+ "Every %1$d Weeks on %2$s for the entire day" : "پۈتۈن%1 $ d ھەپتىلىك%2 $ s",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "ھەر%1 $ d ھەپتىلىك%2 $ s پۈتۈن بىر كۈن ئىچىدە%3 $ s غىچە",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "ھەر%1 $ d ھەپتىلىك%2 $ s دىكى%3 $ s -%4 $ s ئارىلىقىدا",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "ھەر%1 $ d ھەپتىلىكى%2 $ s دىكى%3 $ s -%4 $ s ئارىلىقىدا%5 $ s گىچە",
+ "Every Month on the %1$s for the entire day" : "ھەر ئايدا%1 $ s پۈتۈن بىر كۈن",
+ "Every Month on the %1$s for the entire day until %2$s" : "ھەر ئايدا%1 $ s پۈتۈن بىر كۈن ئىچىدە%2 $ s غىچە",
+ "Every Month on the %1$s between %2$s - %3$s" : "ھەر ئايدا%1 $ s دىكى%2 $ s -%3 $ s ئارىلىقىدا",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "ھەر ئايدا%1 $ s دىكى%2 $ s -%3 $ s ئارىلىقىدا%4 $ s غىچە",
+ "Every %1$d Months on the %2$s for the entire day" : "ھەر%1 $ d ئايلار پۈتۈن كۈندىكى%2 $ s",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "ھەر%1 $ d ئايلار%2 $ s پۈتۈن بىر كۈن ئىچىدە%3 $ s غىچە",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "ھەر%1 $ d ئايلار%2 $ s دىكى%3 $ s -%4 $ s ئارىلىقىدا",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "ھەر%1 $ d ئايلار%2 $ s دىكى%3 $ s -%4 $ s ئارىلىقىدا%5 $ s غىچە",
+ "Every Year in %1$s on the %2$s for the entire day" : "ھەر يىلى%1 $ s دىكى%2 $ s پۈتۈن بىر كۈن",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "ھەر يىلى%1 $ s دىكى%2 $ s پۈتۈن بىر كۈن ئىچىدە%3 $ s غىچە",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "ھەر يىلى%1 $ s دىكى%2 $ s دىكى%3 $ s -%4 $ s ئارىلىقىدا",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "ھەر يىلى%1 $ s دىكى%2 $ s دىكى%3 $ s -%4 $ s ئارىلىقىدا%5 $ s غىچە",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "ھەر%1 $ d يىللار%2 $ s دىكى پۈتۈن كۈن%3 $ s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "ھەر%1 $ d يىللار%2 $ s يىل ئىچىدە%3 $ s پۈتۈن بىر كۈن ئىچىدە%4 $ s غىچە",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "ھەر%1 $ d يىللار%2 $ s دىكى%3 $ s دىكى%4 $ s -%5 $ s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "ھەر%1 $ d يىللار%2 $ s دىكى%3 $ s دىكى%4 $ s -%5 $ s ئارىلىقىدىكى%6 $ s",
+ "On specific dates for the entire day until %1$s" : "پۈتۈن كۈندىكى كونكرېت كۈنلەردە%1 $ s غىچە",
+ "On specific dates between %1$s - %2$s until %3$s" : "كونكرېت كۈنلەردە%1 $ s -%2 $ s ئارىلىقىدا%3 $ s غىچە",
+ "Could not generate next recurrence statement" : "كېيىنكى تەكرارلىنىش باياناتىنى ھاسىل قىلالمىدى",
+ "Cancelled: %1$s" : "ئەمەلدىن قالدۇرۇلدى:%1 $ s",
+ "\"%1$s\" has been canceled" : "\"%1 $ s\" ئەمەلدىن قالدۇرۇلدى",
+ "Re: %1$s" : "Re:%1 $ s",
+ "%1$s has accepted your invitation" : "%1 $ s سىزنىڭ تەكلىۋىڭىزنى قوبۇل قىلدى",
+ "%1$s has tentatively accepted your invitation" : "%1 $ s سىزنىڭ تەكلىۋىڭىزنى ۋاقتىنچە قوبۇل قىلدى",
+ "%1$s has declined your invitation" : "%1 $ s سىزنىڭ تەكلىۋىڭىزنى رەت قىلدى",
+ "%1$s has responded to your invitation" : "%1 $ s سىزنىڭ تەكلىۋىڭىزگە جاۋاب قايتۇردى",
+ "Invitation updated: %1$s" : "تەكلىپ يېڭىلاندى:%1 $ s",
+ "%1$s updated the event \"%2$s\"" : "%1 $ s پائالىيەتنى \"%2 $ s\" يېڭىلىدى",
+ "Invitation: %1$s" : "تەكلىپ:%1 $ s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1 $ s سىزنى «%2 $ s» غا تەكلىپ قىلماقچى",
+ "Organizer:" : "تەشكىللىگۈچى:",
+ "Attendees:" : "قاتناشقۇچىلار:",
+ "Title:" : "ماۋزۇ:",
+ "When:" : "قاچان:",
+ "Location:" : "ئورنى:",
+ "Link:" : "ئۇلىنىش:",
+ "Occurring:" : "يۈز بەرگەن ئىشلار:",
+ "Accept" : "قوبۇل قىلىڭ",
+ "Decline" : "رەت قىلىش",
+ "More options …" : "تېخىمۇ كۆپ تاللاشلار…",
+ "More options at %s" : "% S دىكى تېخىمۇ كۆپ تاللاشلار",
+ "Monday" : "دۈشەنبە",
+ "Tuesday" : "سەيشەنبە",
+ "Wednesday" : "چارشەنبە",
+ "Thursday" : "پەيشەنبە",
+ "Friday" : "جۈمە",
+ "Saturday" : "شەنبە",
+ "Sunday" : "يەكشەنبە",
+ "January" : "قەھرىتان",
+ "February" : "ھۇت",
+ "March" : "نەۋرۇز",
+ "April" : "ئۇمۇت",
+ "May" : "باھار",
+ "June" : "سەپەر",
+ "July" : "چىللە",
+ "August" : "تومۇز",
+ "September" : "مىزان",
+ "October" : "ئوغۇز",
+ "November" : "ئوغلاق",
+ "December" : "كۆنەك",
+ "First" : "بىرىنچى",
+ "Second" : "ئىككىنچى",
+ "Third" : "ئۈچىنچىسى",
+ "Fourth" : "تۆتىنچى",
+ "Last" : "ئاخىرقى",
+ "Second Last" : "Second Last",
+ "Third Last" : "ئۈچىنچى ئاخىرقى",
+ "Fourth Last" : "تۆتىنچى ئاخىرقى",
+ "Contacts" : "ئالاقەداشلار",
+ "{actor} created address book {addressbook}" : "{actor} قۇرغان ئادرېس دەپتىرى {addressbook} دەپتىرى}",
+ "You created address book {addressbook}" : "سىز ئادرېس دەپتىرى {addressbook} دەپتىرىنى قۇردىڭىز",
+ "{actor} deleted address book {addressbook}" : "{actor} ئۆچۈرۈلگەن ئادرېس دەپتىرى {addressbook} دەپتىرى}",
+ "You deleted address book {addressbook}" : "سىز ئادرېس دەپتىرىنى {addressbook} دەپتىرىنى ئۆچۈردىڭىز",
+ "{actor} updated address book {addressbook}" : "{actor} يېڭىلانغان ئادرېس دەپتىرى {addressbook} دەپتىرى}",
+ "You updated address book {addressbook}" : "سىز ئادرېس دەپتىرىنى {addressbook} دەپتىرىنى يېڭىلىدىڭىز",
+ "{actor} shared address book {addressbook} with you" : "{actor} ئورتاقلاشقان ئادرېس دەپتىرى {addressbook} دەپتىرى you سىز بىلەن",
+ "You shared address book {addressbook} with {user}" : "سىز ئادرېس دەپتىرى {addressbook} دەپتىرىنى {user} بىلەن ئورتاقلاشتىڭىز",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} ئورتاقلاشقان ئادرېس دەپتىرى {addressbook} دەپتىرى {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} ئورتاقلاشمىغان ئادرېس دەپتىرى {addressbook} دەپتىرى you سىزدىن",
+ "You unshared address book {addressbook} from {user}" : "سىز ئورتاق ئىشلەتمىگەن ئادرېس دەپتىرى {addressbook} دەپتىرى {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} ھەمبەھىرلەنمىگەن ئادرېس دەپتىرى {addressbook} دەپتىرى {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} ئورتاقلاشمىغان ئادرېس دەپتىرى {addressbook} دەپتىرى themselves ئۆزىدىن",
+ "You shared address book {addressbook} with group {group}" : "گۇرۇپپا {addressbook} with بىلەن ئادرېس دەپتىرى {group} دەپتىرىنى ئورتاقلاشتىڭىز",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} گۇرۇپپا {addressbook} بىلەن ئورتاقلاشقان ئادرېس دەپتىرى {group} دەپتىرى}",
+ "You unshared address book {addressbook} from group {group}" : "گۇرۇپپا {addressbook} ھەمبەھىرلەنگەن ئادرېس دەپتىرى {group} دەپتىرى}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} ئورتاق بەھرىلىنىدىغان ئادرېس دەپتىرى {addressbook} دەپتىرى group گۇرۇپپا {group}",
+ "You created contact {card} in address book {addressbook}" : "سىز ئادرېس دەپتىرى {card} دەپتىرىدە ئالاقىلىشىش {addressbook} قۇردىڭىز",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} contact ئادرېس دەپتىرى {card} دەپتىرىدىن ئالاقىلىشىش {addressbook} ئۆچۈرۈلگەن",
+ "You deleted contact {card} from address book {addressbook}" : "ئالاقىلىشىش {card} address ئادرېس دەپتىرى {addressbook} دەپتىرىدىن ئۆچۈرۈلدىڭىز",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} يېڭىلانغان ئالاقىلىشىش {card} address ئادرېس دەپتىرى {addressbook} دەپتىرى}",
+ "You updated contact {card} in address book {addressbook}" : "ئالاقىلىشىش {card} address ئادرېس دەپتىرى {addressbook} دەپتىرىنى يېڭىلىدىڭىز",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong> ئالاقىلىشىش </ strong> ياكى <strong> ئادرېس دەپتىرى </ strong> ئۆزگەرتىلدى",
+ "Accounts" : "ھېسابات",
+ "System address book which holds all accounts" : "بارلىق ھېساباتلارنى ئۆز ئىچىگە ئالغان سىستېما ئادرېس دەپتىرى",
+ "File is not updatable: %1$s" : "ھۆججەت يېڭىلانمايدۇ:%1 $ s",
+ "Failed to get storage for file" : "ھۆججەت ساقلاشقا ئېرىشەلمىدى",
+ "Could not write to final file, canceled by hook" : "ئاخىرقى ھۆججەتكە يازالمىدى ، قارماق تەرىپىدىن ئەمەلدىن قالدۇرۇلدى",
+ "Could not write file contents" : "ھۆججەت مەزمۇنىنى يازالمىدى",
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "نىشاننى نىشانغا كۆچۈرگەندە خاتالىق (كۆچۈرۈلگەن:%1 $ s ، مۆلچەردىكى ھۆججەتلەر:%2 $ s)",
+ "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." : "مۆلچەردىكى ھۆججەتلەر%1 $ s بولىدۇ ، ئەمما ئوقۇڭ (Nextcloud خېرىدارىدىن) ۋە (Nextcloud ساقلىغۇچقا)%2 $ s دەپ يازدى. ئەۋەتىش تەرەپتىكى تور مەسىلىسى ياكى مۇلازىمېتىر تەرەپتىكى ساقلاشقا يېزىش مەسىلىسى بولۇشى مۇمكىن.",
+ "Could not rename part file to final file, canceled by hook" : "بۆلەك ھۆججىتىنى ئاخىرقى ھۆججەتكە ئۆزگەرتەلمىدى ، قارماق تەرىپىدىن ئەمەلدىن قالدۇرۇلدى",
+ "Could not rename part file to final file" : "قىسمەن ھۆججەتنى ئاخىرقى ھۆججەتكە ئۆزگەرتەلمىدى",
+ "Failed to check file size: %1$s" : "ھۆججەتنىڭ چوڭ-كىچىكلىكىنى تەكشۈرەلمىدى:%1 $ s",
+ "Encryption not ready: %1$s" : "شىفىرلاش تەييار ئەمەس:%1 $ s",
+ "Failed to open file: %1$s" : "ھۆججەت ئېچىلمىدى:%1 $ s",
+ "Failed to unlink: %1$s" : "ئۇلانمىدى:%1 $ s",
+ "Failed to write file contents: %1$s" : "ھۆججەت مەزمۇنىنى يېزىش مەغلۇب بولدى:%1 $ s",
+ "File not found: %1$s" : "ھۆججەت تېپىلمىدى:%1 $ s",
+ "Invalid target path" : "نىشان يولى ئىناۋەتسىز",
+ "System is in maintenance mode." : "سىستېما ئاسراش ھالىتىدە.",
+ "Upgrade needed" : "يېڭىلاش كېرەك",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "IOS / macOS ئارقىلىق CalDAV ۋە CardDAV نى ئىشلىتىش ئۈچۈن% s نى HTTPS ئىشلىتىش ئۈچۈن تەڭشەش كېرەك.",
+ "Configures a CalDAV account" : "CalDAV ھېساباتىنى سەپلەيدۇ",
+ "Configures a CardDAV account" : "CardDAV ھېساباتىنى سەپلەيدۇ",
+ "Events" : "Events",
+ "Untitled task" : "نامسىز ۋەزىپە",
+ "Completed on %s" : "% S دا تاماملاندى",
+ "Due on %s by %s" : "% S نىڭ سەۋەبىدىن% s",
+ "Due on %s" : "% S سەۋەبىدىن",
+ "DAV system address book" : "DAV سىستېما ئادرېس دەپتىرى",
+ "No outstanding DAV system address book sync." : "مۇنەۋۋەر DAV سىستېمىسى ئادرېس كىتاب ماسقەدەملەش يوق.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "مىسالىڭىزدا 1000 دىن ئارتۇق ئىشلەتكۈچى بار ياكى خاتالىق يۈز بەرگەنلىكى ئۈچۈن DAV سىستېمىسى ئادرېس دەپتىرىنى ماسقەدەملەش تېخى ئىجرا بولمىدى. ئۇنى «occ dav: sync-system-addressbook» دەپ چاقىرىپ قولدا ئىجرا قىلىڭ.",
+ "WebDAV endpoint" : "WebDAV ئاخىرقى نۇقتىسى",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "تور مۇلازىمېتىرىڭىزنىڭ WebDAV ئارقىلىق ھۆججەت ماسقەدەملىشىگە يول قويۇلغانلىقىنى تەكشۈرەلمىدىڭىز. قولدا تەكشۈرۈپ بېقىڭ.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "تور مۇلازىمېتىرىڭىز ھۆججەتنىڭ ماس قەدەمدە بولۇشىغا يول قويۇلمىدى ، چۈنكى WebDAV كۆرۈنمە يۈزى بۇزۇلغاندەك قىلىدۇ.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "تور مۇلازىمېتىرىڭىز WebDAV ئارقىلىق ھۆججەت ماسقەدەملەش ئۈچۈن مۇۋاپىق تەڭشەلدى.",
+ "Migrated calendar (%1$s)" : "كۆچۈرۈلگەن كالېندار (%1 $ s)",
+ "Calendars including events, details and attendees" : "پائالىيەتلەر ، تەپسىلاتلار ۋە قاتناشقۇچىلارنى ئۆز ئىچىگە ئالغان كالېندارلار",
+ "Contacts and groups" : "ئالاقىلىشىش ۋە گۇرۇپپىلار",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "يوقلۇق ساقلاندى",
+ "Failed to save your absence settings" : "يوقلۇقىڭىزنى ساقلىيالمىدى",
+ "Absence cleared" : "يوقالغان",
+ "Failed to clear your absence settings" : "يوقلۇقىڭىزنى تەڭشىمىدى",
+ "First day" : "بىرىنچى كۈنى",
+ "Last day (inclusive)" : "ئالدىنقى كۈنى (ئۆز ئىچىگە ئالىدۇ)",
+ "Out of office replacement (optional)" : "ئىش ئورنىنى ئالماشتۇرۇش (ئىختىيارىي)",
+ "Name of the replacement" : "ئالماشتۇرغۇچىنىڭ ئىسمى",
+ "No results." : "ھېچقانداق نەتىجە يوق.",
+ "Start typing." : "يېزىشنى باشلاڭ.",
+ "Short absence status" : "قىسقا ۋاقىتلىق ھالەت",
+ "Long absence Message" : "ئۇزۇن بولمىغان ئۇچۇر",
+ "Save" : "ساقلا",
+ "Disable absence" : "يوقلۇقىنى چەكلەڭ",
+ "Failed to load availability" : "ئىشلىتىشچانلىقىنى يۈكلىيەلمىدى",
+ "Saved availability" : "تېجەشلىك",
+ "Failed to save availability" : "ئىشلەتكىلى بولمىدى",
+ "Time zone:" : "ۋاقىت رايونى:",
+ "to" : "to",
+ "Delete slot" : "ئورۇننى ئۆچۈرۈڭ",
+ "No working hours set" : "خىزمەت ۋاقتى بېكىتىلمىدى",
+ "Add slot" : "ئورۇن قوشۇڭ",
+ "Weekdays" : "ھەپتە كۈنلىرى",
+ "Pick a start time for {dayName}" : "{dayName} نىڭ باشلىنىش ۋاقتىنى تاللاڭ",
+ "Pick a end time for {dayName}" : "{dayName} نىڭ ئاخىرقى ۋاقتىنى تاللاڭ",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "بارلىق ئۇقتۇرۇشلارنى ئاۋازسىز قىلىش ئۈچۈن ئىشلەتكۈچى ھالىتىنى ئاپتوماتىك ھالدا «ئاۋارە قىلماڭ» قىلىپ تەڭشەڭ.",
+ "Cancel" : "بىكار قىلىش",
+ "Import" : "ئەكىر",
+ "Error while saving settings" : "تەڭشەكلەرنى ساقلاش جەريانىدا خاتالىق",
+ "Reset to default" : "سۈكۈتتىكى ھالىتىگە قايتىڭ",
+ "Availability" : "ئىشلەتكىلى بولىدۇ",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "ئەگەر خىزمەت ۋاقتىڭىزنى تەڭشىسىڭىز ، باشقىلار يىغىن زاكاز قىلغاندا سىزنىڭ ئىشتىن چۈشكەن ۋاقتىڭىزنى كۆرىدۇ.",
+ "Absence" : "يوق",
+ "Configure your next absence period." : "كېيىنكى يوقلۇق ۋاقتىڭىزنى سەپلەڭ.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "يەنە {calendarappstoreopen} كالېندار دېتالى {linkclose} ياكى {calendardocopen} نى قاچىلاڭ ئۈستەلئۈستىڭىز ۋە كۆچمە تېلېفونىڭىزنى ئۇلاڭ ↗ {linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "{emailopen} ئېلېكترونلۇق خەت مۇلازىمېتىرى {linkclose} نى مۇۋاپىق تەڭشەشكە كاپالەتلىك قىلىڭ.",
+ "Calendar server" : "كالېندار مۇلازىمېتىرى",
+ "Send invitations to attendees" : "يىغىنغا قاتناشقۇچىلارغا تەكلىپنامە ئەۋەتىڭ",
+ "Automatically generate a birthday calendar" : "تۇغۇلغان كۈن كالېندارىنى ئاپتوماتىك ھاسىل قىلىدۇ",
+ "Birthday calendars will be generated by a background job." : "تۇغۇلغان كۈن كالېندارى ئارقا كۆرۈنۈش خىزمىتى ئارقىلىق بارلىققا كېلىدۇ.",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "شۇڭلاشقا ئۇلار قوزغىتىلغاندىن كېيىن دەرھال تېپىلمايدۇ ، ئەمما مەلۇم ۋاقىتتىن كېيىن پەيدا بولىدۇ.",
+ "Send notifications for events" : "ۋەقەلەرگە ئۇقتۇرۇش ئەۋەتىڭ",
+ "Notifications are sent via background jobs, so these must occur often enough." : "ئۇقتۇرۇش ئارقا كۆرۈنۈش خىزمىتى ئارقىلىق ئەۋەتىلىدۇ ، شۇڭا بۇلار چوقۇم يېتەرلىك بولۇشى كېرەك.",
+ "Send reminder notifications to calendar sharees as well" : "كالېندار ھەمبەھىرلىرىگىمۇ ئەسكەرتىش ئۇقتۇرۇشى ئەۋەتىڭ",
+ "Reminders are always sent to organizers and attendees." : "ئەسكەرتىش ھەمىشە تەشكىللىگۈچىلەر ۋە قاتناشقۇچىلارغا ئەۋەتىلىدۇ.",
+ "Enable notifications for events via push" : "ئىتتىرىش ئارقىلىق ۋەقەلەرگە ئۇقتۇرۇشنى قوزغىتىڭ",
+ "There was an error updating your attendance status." : "قاتنىشىش ھالىتىڭىزنى يېڭىلاشتا خاتالىق كۆرۈلدى.",
+ "Please contact the organizer directly." : "تەشكىللىگۈچى بىلەن بىۋاسىتە ئالاقىلىشىڭ.",
+ "Are you accepting the invitation?" : "تەكلىپنى قوبۇل قىلامسىز؟",
+ "Tentative" : "Tentative",
+ "Your attendance was updated successfully." : "سىزنىڭ قاتنىشىشىڭىز مۇۋەپپەقىيەتلىك يېڭىلاندى."
+},
+"nplurals=2; plural=(n != 1);");
diff --git a/apps/dav/l10n/ug.json b/apps/dav/l10n/ug.json
new file mode 100644
index 00000000000..abbbc45161f
--- /dev/null
+++ b/apps/dav/l10n/ug.json
@@ -0,0 +1,266 @@
+{ "translations": {
+ "Calendar" : "يىلنامە",
+ "Tasks" : "ۋەزىپەلەر",
+ "Personal" : "شەخسىي",
+ "{actor} created calendar {calendar}" : "{actor} قۇرغان كالېندار {calendar}",
+ "You created calendar {calendar}" : "كالېندار {calendar} قۇردىڭىز",
+ "{actor} deleted calendar {calendar}" : "{actor} ئۆچۈرۈلگەن كالېندار {calendar}",
+ "You deleted calendar {calendar}" : "كالېندار {calendar} ئۆچۈردىڭىز",
+ "{actor} updated calendar {calendar}" : "{actor} يېڭىلانغان كالېندار {calendar}",
+ "You updated calendar {calendar}" : "كالېندار {calendar} يېڭىلىدىڭىز",
+ "{actor} restored calendar {calendar}" : "{actor} ئەسلىگە كەلتۈرۈلگەن كالېندار {calendar}",
+ "You restored calendar {calendar}" : "كالېندار {calendar} ئەسلىگە كەلتۈردىڭىز",
+ "You shared calendar {calendar} as public link" : "سىز كالېندار {calendar} public نى ئاممىۋى ئۇلىنىش سۈپىتىدە ھەمبەھىرلىدىڭىز",
+ "You removed public link for calendar {calendar}" : "كالېندار {calendar} ئۈچۈن ئاممىۋى ئۇلىنىشنى ئۆچۈردىڭىز",
+ "{actor} shared calendar {calendar} with you" : "{actor} ئورتاقلاشقان كالېندار {calendar} سىز بىلەن",
+ "You shared calendar {calendar} with {user}" : "سىز كالېندار {calendar} نى {user} بىلەن ئورتاقلاشتىڭىز",
+ "{actor} shared calendar {calendar} with {user}" : "{actor} ئورتاقلاشقان كالېندار {calendar} {user} بىلەن",
+ "{actor} unshared calendar {calendar} from you" : "{actor} ئورتاقلاشمىغان كالېندار {calendar} سىزدىن",
+ "You unshared calendar {calendar} from {user}" : "سىز كالىندار {calendar} نى {user} دىن ھەمبەرلەنمىدىڭىز",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor} ئورتاقلاشمىغان كالېندار {calendar} {user}",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} ئورتاقلاشمىغان كالېندار {calendar} ئۆزىدىن",
+ "You shared calendar {calendar} with group {group}" : "كالېندار {calendar} group گۇرۇپپا {group} بىلەن ئورتاقلاشتىڭىز",
+ "{actor} shared calendar {calendar} with group {group}" : "{actor} گۇرۇپپا {calendar} بىلەن ئورتاقلاشقان كالېندار {group}",
+ "You unshared calendar {calendar} from group {group}" : "گۇرۇپپا {calendar} ئورتاقلاشمىغان كالېندار {group}",
+ "{actor} unshared calendar {calendar} from group {group}" : "{actor} بولسا كالىندار {calendar} نى گۇرۇپپا {group} بىلەن ھەمبەھىرلىمىدى",
+ "Untitled event" : "نامسىز ھادىسە",
+ "{actor} created event {event} in calendar {calendar}" : "{actor} كالېندار {event} پائالىيەت {calendar} قۇرغان}",
+ "You created event {event} in calendar {calendar}" : "كالېندار {event} پائالىيەت {calendar} created قۇردىڭىز",
+ "{actor} deleted event {event} from calendar {calendar}" : "{actor} ئۆچۈرۈلگەن ھادىسە {event} كالېنداردىن {calendar}",
+ "You deleted event {event} from calendar {calendar}" : "كالېندار {event} ھادىسە {calendar} ئۆچۈردىڭىز",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} بولسا كالىندار {calendar} دىكى پائالىيەت {event} نى يېڭىلىدى",
+ "You updated event {event} in calendar {calendar}" : "كالېندار {event} پائالىيەت {calendar} نى يېڭىلىدىڭىز",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} پائالىيەت {event} كالېنداردىن {sourceCalendar} دىن كالېندارغا {targetCalendar} غا يۆتكەلدى",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "سىز ھادىسە {event} كالىندارىدىن {sourceCalendar} دىن كالېندار {targetCalendar} غا يۆتكىدىڭىز",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} ئەسلىگە كەلتۈرۈلگەن ھادىسە {event} كالىندارى {calendar}",
+ "You restored event {event} of calendar {calendar}" : "كالېندار {event} پائالىيەت {calendar} نى ئەسلىگە كەلتۈردىڭىز",
+ "Busy" : "ئالدىراش",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} list تىزىملىك {calendar} {todo} ئىجاد قىلىش",
+ "You created to-do {todo} in list {calendar}" : "سىز تىزىملىك {calendar} {todo} نى قۇردىڭىز",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} list تىزىملىكتىن {calendar} to ئۆچۈرۈلدى {todo}",
+ "You deleted to-do {todo} from list {calendar}" : "تىزىملىكتىن {todo} to نى ئۆچۈردىڭىز {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} تىزىملىك {calendar} {todo} يېڭىلانغان",
+ "You updated to-do {todo} in list {calendar}" : "تىزىملىك {calendar} دىكى {todo} نى يېڭىلىدىڭىز",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} بولسا {calendar} تىزىملىكتىكى {todo} نى ھەل قىلدى",
+ "You solved to-do {todo} in list {calendar}" : "تىزىملىك {calendar} دىكى {todo} نى ھەل قىلدىڭىز",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} تىزىملىك {calendar} {todo} قايتا ئېچىلدى",
+ "You reopened to-do {todo} in list {calendar}" : "تىزىملىك {calendar} دىكى {todo} نى قايتا ئاچتىڭىز",
+ "Calendar, contacts and tasks" : "كالېندار ، ئالاقىلىشىش ۋە ۋەزىپە",
+ "A <strong>calendar</strong> was modified" : "<strong> كالېندار </ strong> ئۆزگەرتىلدى",
+ "A calendar <strong>event</strong> was modified" : "كالېندار <strong> ھادىسە </ strong> ئۆزگەرتىلدى",
+ "A calendar <strong>to-do</strong> was modified" : "كالېندار <strong> قىلىش </ strong> ئۆزگەرتىلدى",
+ "Contact birthdays" : "تۇغۇلغان كۈنى بىلەن ئالاقىلىشىڭ",
+ "Death of %s" : "% S نىڭ ئۆلۈمى",
+ "Untitled calendar" : "نامسىز كالېندار",
+ "Calendar:" : "كالېندار:",
+ "Date:" : "چېسلا:",
+ "Where:" : "قەيەردە:",
+ "Description:" : "چۈشەندۈرۈش:",
+ "%s (in %s)" : "% s (% s)",
+ "%s (%s ago)" : "% s (% s ago)",
+ "Calendar: %s" : "كالېندار:% s",
+ "Date: %s" : "چېسلا:% s",
+ "Description: %s" : "چۈشەندۈرۈش:% s",
+ "Where: %s" : "قەيەردە:% s",
+ "%1$s via %2$s" : "%1 $ s ئارقىلىق%2 $ s",
+ "Could not generate when statement" : "بايان قىلغاندا ھاسىل قىلالمىدى",
+ "Every Day for the entire day" : "ھەر بىر كۈن پۈتۈن بىر كۈن",
+ "Every Day for the entire day until %1$s" : "ھەر بىر كۈن پۈتۈن كۈن%1 $ s غىچە",
+ "Every Day between %1$s - %2$s" : "ھەر كۈنى%1 $ s -%2 $ s ئارىلىقىدا",
+ "Every Day between %1$s - %2$s until %3$s" : "ھەر كۈنى%1 $ s -%2 $ s ئارىلىقىدا%3 $ s غىچە",
+ "Every %1$d Days for the entire day" : "پۈتۈن%1 $ d كۈن",
+ "Every %1$d Days for the entire day until %2$s" : "ھەر%1 $ d پۈتۈن بىر كۈن ئىچىدە%2 $ s غىچە",
+ "Every %1$d Days between %2$s - %3$s" : "ھەر%1 $ d كۈنلەر%2 $ s -%3 $ s ئارىلىقىدا",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "ھەر%1 $ d كۈنلىرى%2 $ s -%3 $ s ئارىلىقىدا%4 $ s غىچە",
+ "Could not generate event recurrence statement" : "ھادىسە قايتا-قايتا بايانات ھاسىل قىلالمىدى",
+ "Every Week on %1$s for the entire day" : "ھەر ھەپتە پۈتۈن كۈن ئۈچۈن%1 $ s",
+ "Every Week on %1$s for the entire day until %2$s" : "ھەر ھەپتە%1 $ s پۈتۈن بىر كۈن ئىچىدە%2 $ s غىچە",
+ "Every Week on %1$s between %2$s - %3$s" : "ھەر ھەپتە%1 $ s دىكى%2 $ s -%3 $ s ئارىلىقىدا",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "ھەر ھەپتە%1 $ s دىكى%2 $ s -%3 $ s ئارىلىقىدا%4 $ s غىچە",
+ "Every %1$d Weeks on %2$s for the entire day" : "پۈتۈن%1 $ d ھەپتىلىك%2 $ s",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "ھەر%1 $ d ھەپتىلىك%2 $ s پۈتۈن بىر كۈن ئىچىدە%3 $ s غىچە",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "ھەر%1 $ d ھەپتىلىك%2 $ s دىكى%3 $ s -%4 $ s ئارىلىقىدا",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "ھەر%1 $ d ھەپتىلىكى%2 $ s دىكى%3 $ s -%4 $ s ئارىلىقىدا%5 $ s گىچە",
+ "Every Month on the %1$s for the entire day" : "ھەر ئايدا%1 $ s پۈتۈن بىر كۈن",
+ "Every Month on the %1$s for the entire day until %2$s" : "ھەر ئايدا%1 $ s پۈتۈن بىر كۈن ئىچىدە%2 $ s غىچە",
+ "Every Month on the %1$s between %2$s - %3$s" : "ھەر ئايدا%1 $ s دىكى%2 $ s -%3 $ s ئارىلىقىدا",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "ھەر ئايدا%1 $ s دىكى%2 $ s -%3 $ s ئارىلىقىدا%4 $ s غىچە",
+ "Every %1$d Months on the %2$s for the entire day" : "ھەر%1 $ d ئايلار پۈتۈن كۈندىكى%2 $ s",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "ھەر%1 $ d ئايلار%2 $ s پۈتۈن بىر كۈن ئىچىدە%3 $ s غىچە",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "ھەر%1 $ d ئايلار%2 $ s دىكى%3 $ s -%4 $ s ئارىلىقىدا",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "ھەر%1 $ d ئايلار%2 $ s دىكى%3 $ s -%4 $ s ئارىلىقىدا%5 $ s غىچە",
+ "Every Year in %1$s on the %2$s for the entire day" : "ھەر يىلى%1 $ s دىكى%2 $ s پۈتۈن بىر كۈن",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "ھەر يىلى%1 $ s دىكى%2 $ s پۈتۈن بىر كۈن ئىچىدە%3 $ s غىچە",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "ھەر يىلى%1 $ s دىكى%2 $ s دىكى%3 $ s -%4 $ s ئارىلىقىدا",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "ھەر يىلى%1 $ s دىكى%2 $ s دىكى%3 $ s -%4 $ s ئارىلىقىدا%5 $ s غىچە",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "ھەر%1 $ d يىللار%2 $ s دىكى پۈتۈن كۈن%3 $ s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "ھەر%1 $ d يىللار%2 $ s يىل ئىچىدە%3 $ s پۈتۈن بىر كۈن ئىچىدە%4 $ s غىچە",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "ھەر%1 $ d يىللار%2 $ s دىكى%3 $ s دىكى%4 $ s -%5 $ s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "ھەر%1 $ d يىللار%2 $ s دىكى%3 $ s دىكى%4 $ s -%5 $ s ئارىلىقىدىكى%6 $ s",
+ "On specific dates for the entire day until %1$s" : "پۈتۈن كۈندىكى كونكرېت كۈنلەردە%1 $ s غىچە",
+ "On specific dates between %1$s - %2$s until %3$s" : "كونكرېت كۈنلەردە%1 $ s -%2 $ s ئارىلىقىدا%3 $ s غىچە",
+ "Could not generate next recurrence statement" : "كېيىنكى تەكرارلىنىش باياناتىنى ھاسىل قىلالمىدى",
+ "Cancelled: %1$s" : "ئەمەلدىن قالدۇرۇلدى:%1 $ s",
+ "\"%1$s\" has been canceled" : "\"%1 $ s\" ئەمەلدىن قالدۇرۇلدى",
+ "Re: %1$s" : "Re:%1 $ s",
+ "%1$s has accepted your invitation" : "%1 $ s سىزنىڭ تەكلىۋىڭىزنى قوبۇل قىلدى",
+ "%1$s has tentatively accepted your invitation" : "%1 $ s سىزنىڭ تەكلىۋىڭىزنى ۋاقتىنچە قوبۇل قىلدى",
+ "%1$s has declined your invitation" : "%1 $ s سىزنىڭ تەكلىۋىڭىزنى رەت قىلدى",
+ "%1$s has responded to your invitation" : "%1 $ s سىزنىڭ تەكلىۋىڭىزگە جاۋاب قايتۇردى",
+ "Invitation updated: %1$s" : "تەكلىپ يېڭىلاندى:%1 $ s",
+ "%1$s updated the event \"%2$s\"" : "%1 $ s پائالىيەتنى \"%2 $ s\" يېڭىلىدى",
+ "Invitation: %1$s" : "تەكلىپ:%1 $ s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1 $ s سىزنى «%2 $ s» غا تەكلىپ قىلماقچى",
+ "Organizer:" : "تەشكىللىگۈچى:",
+ "Attendees:" : "قاتناشقۇچىلار:",
+ "Title:" : "ماۋزۇ:",
+ "When:" : "قاچان:",
+ "Location:" : "ئورنى:",
+ "Link:" : "ئۇلىنىش:",
+ "Occurring:" : "يۈز بەرگەن ئىشلار:",
+ "Accept" : "قوبۇل قىلىڭ",
+ "Decline" : "رەت قىلىش",
+ "More options …" : "تېخىمۇ كۆپ تاللاشلار…",
+ "More options at %s" : "% S دىكى تېخىمۇ كۆپ تاللاشلار",
+ "Monday" : "دۈشەنبە",
+ "Tuesday" : "سەيشەنبە",
+ "Wednesday" : "چارشەنبە",
+ "Thursday" : "پەيشەنبە",
+ "Friday" : "جۈمە",
+ "Saturday" : "شەنبە",
+ "Sunday" : "يەكشەنبە",
+ "January" : "قەھرىتان",
+ "February" : "ھۇت",
+ "March" : "نەۋرۇز",
+ "April" : "ئۇمۇت",
+ "May" : "باھار",
+ "June" : "سەپەر",
+ "July" : "چىللە",
+ "August" : "تومۇز",
+ "September" : "مىزان",
+ "October" : "ئوغۇز",
+ "November" : "ئوغلاق",
+ "December" : "كۆنەك",
+ "First" : "بىرىنچى",
+ "Second" : "ئىككىنچى",
+ "Third" : "ئۈچىنچىسى",
+ "Fourth" : "تۆتىنچى",
+ "Last" : "ئاخىرقى",
+ "Second Last" : "Second Last",
+ "Third Last" : "ئۈچىنچى ئاخىرقى",
+ "Fourth Last" : "تۆتىنچى ئاخىرقى",
+ "Contacts" : "ئالاقەداشلار",
+ "{actor} created address book {addressbook}" : "{actor} قۇرغان ئادرېس دەپتىرى {addressbook} دەپتىرى}",
+ "You created address book {addressbook}" : "سىز ئادرېس دەپتىرى {addressbook} دەپتىرىنى قۇردىڭىز",
+ "{actor} deleted address book {addressbook}" : "{actor} ئۆچۈرۈلگەن ئادرېس دەپتىرى {addressbook} دەپتىرى}",
+ "You deleted address book {addressbook}" : "سىز ئادرېس دەپتىرىنى {addressbook} دەپتىرىنى ئۆچۈردىڭىز",
+ "{actor} updated address book {addressbook}" : "{actor} يېڭىلانغان ئادرېس دەپتىرى {addressbook} دەپتىرى}",
+ "You updated address book {addressbook}" : "سىز ئادرېس دەپتىرىنى {addressbook} دەپتىرىنى يېڭىلىدىڭىز",
+ "{actor} shared address book {addressbook} with you" : "{actor} ئورتاقلاشقان ئادرېس دەپتىرى {addressbook} دەپتىرى you سىز بىلەن",
+ "You shared address book {addressbook} with {user}" : "سىز ئادرېس دەپتىرى {addressbook} دەپتىرىنى {user} بىلەن ئورتاقلاشتىڭىز",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} ئورتاقلاشقان ئادرېس دەپتىرى {addressbook} دەپتىرى {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} ئورتاقلاشمىغان ئادرېس دەپتىرى {addressbook} دەپتىرى you سىزدىن",
+ "You unshared address book {addressbook} from {user}" : "سىز ئورتاق ئىشلەتمىگەن ئادرېس دەپتىرى {addressbook} دەپتىرى {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} ھەمبەھىرلەنمىگەن ئادرېس دەپتىرى {addressbook} دەپتىرى {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} ئورتاقلاشمىغان ئادرېس دەپتىرى {addressbook} دەپتىرى themselves ئۆزىدىن",
+ "You shared address book {addressbook} with group {group}" : "گۇرۇپپا {addressbook} with بىلەن ئادرېس دەپتىرى {group} دەپتىرىنى ئورتاقلاشتىڭىز",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} گۇرۇپپا {addressbook} بىلەن ئورتاقلاشقان ئادرېس دەپتىرى {group} دەپتىرى}",
+ "You unshared address book {addressbook} from group {group}" : "گۇرۇپپا {addressbook} ھەمبەھىرلەنگەن ئادرېس دەپتىرى {group} دەپتىرى}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} ئورتاق بەھرىلىنىدىغان ئادرېس دەپتىرى {addressbook} دەپتىرى group گۇرۇپپا {group}",
+ "You created contact {card} in address book {addressbook}" : "سىز ئادرېس دەپتىرى {card} دەپتىرىدە ئالاقىلىشىش {addressbook} قۇردىڭىز",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} contact ئادرېس دەپتىرى {card} دەپتىرىدىن ئالاقىلىشىش {addressbook} ئۆچۈرۈلگەن",
+ "You deleted contact {card} from address book {addressbook}" : "ئالاقىلىشىش {card} address ئادرېس دەپتىرى {addressbook} دەپتىرىدىن ئۆچۈرۈلدىڭىز",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} يېڭىلانغان ئالاقىلىشىش {card} address ئادرېس دەپتىرى {addressbook} دەپتىرى}",
+ "You updated contact {card} in address book {addressbook}" : "ئالاقىلىشىش {card} address ئادرېس دەپتىرى {addressbook} دەپتىرىنى يېڭىلىدىڭىز",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong> ئالاقىلىشىش </ strong> ياكى <strong> ئادرېس دەپتىرى </ strong> ئۆزگەرتىلدى",
+ "Accounts" : "ھېسابات",
+ "System address book which holds all accounts" : "بارلىق ھېساباتلارنى ئۆز ئىچىگە ئالغان سىستېما ئادرېس دەپتىرى",
+ "File is not updatable: %1$s" : "ھۆججەت يېڭىلانمايدۇ:%1 $ s",
+ "Failed to get storage for file" : "ھۆججەت ساقلاشقا ئېرىشەلمىدى",
+ "Could not write to final file, canceled by hook" : "ئاخىرقى ھۆججەتكە يازالمىدى ، قارماق تەرىپىدىن ئەمەلدىن قالدۇرۇلدى",
+ "Could not write file contents" : "ھۆججەت مەزمۇنىنى يازالمىدى",
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "نىشاننى نىشانغا كۆچۈرگەندە خاتالىق (كۆچۈرۈلگەن:%1 $ s ، مۆلچەردىكى ھۆججەتلەر:%2 $ s)",
+ "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." : "مۆلچەردىكى ھۆججەتلەر%1 $ s بولىدۇ ، ئەمما ئوقۇڭ (Nextcloud خېرىدارىدىن) ۋە (Nextcloud ساقلىغۇچقا)%2 $ s دەپ يازدى. ئەۋەتىش تەرەپتىكى تور مەسىلىسى ياكى مۇلازىمېتىر تەرەپتىكى ساقلاشقا يېزىش مەسىلىسى بولۇشى مۇمكىن.",
+ "Could not rename part file to final file, canceled by hook" : "بۆلەك ھۆججىتىنى ئاخىرقى ھۆججەتكە ئۆزگەرتەلمىدى ، قارماق تەرىپىدىن ئەمەلدىن قالدۇرۇلدى",
+ "Could not rename part file to final file" : "قىسمەن ھۆججەتنى ئاخىرقى ھۆججەتكە ئۆزگەرتەلمىدى",
+ "Failed to check file size: %1$s" : "ھۆججەتنىڭ چوڭ-كىچىكلىكىنى تەكشۈرەلمىدى:%1 $ s",
+ "Encryption not ready: %1$s" : "شىفىرلاش تەييار ئەمەس:%1 $ s",
+ "Failed to open file: %1$s" : "ھۆججەت ئېچىلمىدى:%1 $ s",
+ "Failed to unlink: %1$s" : "ئۇلانمىدى:%1 $ s",
+ "Failed to write file contents: %1$s" : "ھۆججەت مەزمۇنىنى يېزىش مەغلۇب بولدى:%1 $ s",
+ "File not found: %1$s" : "ھۆججەت تېپىلمىدى:%1 $ s",
+ "Invalid target path" : "نىشان يولى ئىناۋەتسىز",
+ "System is in maintenance mode." : "سىستېما ئاسراش ھالىتىدە.",
+ "Upgrade needed" : "يېڭىلاش كېرەك",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "IOS / macOS ئارقىلىق CalDAV ۋە CardDAV نى ئىشلىتىش ئۈچۈن% s نى HTTPS ئىشلىتىش ئۈچۈن تەڭشەش كېرەك.",
+ "Configures a CalDAV account" : "CalDAV ھېساباتىنى سەپلەيدۇ",
+ "Configures a CardDAV account" : "CardDAV ھېساباتىنى سەپلەيدۇ",
+ "Events" : "Events",
+ "Untitled task" : "نامسىز ۋەزىپە",
+ "Completed on %s" : "% S دا تاماملاندى",
+ "Due on %s by %s" : "% S نىڭ سەۋەبىدىن% s",
+ "Due on %s" : "% S سەۋەبىدىن",
+ "DAV system address book" : "DAV سىستېما ئادرېس دەپتىرى",
+ "No outstanding DAV system address book sync." : "مۇنەۋۋەر DAV سىستېمىسى ئادرېس كىتاب ماسقەدەملەش يوق.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "مىسالىڭىزدا 1000 دىن ئارتۇق ئىشلەتكۈچى بار ياكى خاتالىق يۈز بەرگەنلىكى ئۈچۈن DAV سىستېمىسى ئادرېس دەپتىرىنى ماسقەدەملەش تېخى ئىجرا بولمىدى. ئۇنى «occ dav: sync-system-addressbook» دەپ چاقىرىپ قولدا ئىجرا قىلىڭ.",
+ "WebDAV endpoint" : "WebDAV ئاخىرقى نۇقتىسى",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "تور مۇلازىمېتىرىڭىزنىڭ WebDAV ئارقىلىق ھۆججەت ماسقەدەملىشىگە يول قويۇلغانلىقىنى تەكشۈرەلمىدىڭىز. قولدا تەكشۈرۈپ بېقىڭ.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "تور مۇلازىمېتىرىڭىز ھۆججەتنىڭ ماس قەدەمدە بولۇشىغا يول قويۇلمىدى ، چۈنكى WebDAV كۆرۈنمە يۈزى بۇزۇلغاندەك قىلىدۇ.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "تور مۇلازىمېتىرىڭىز WebDAV ئارقىلىق ھۆججەت ماسقەدەملەش ئۈچۈن مۇۋاپىق تەڭشەلدى.",
+ "Migrated calendar (%1$s)" : "كۆچۈرۈلگەن كالېندار (%1 $ s)",
+ "Calendars including events, details and attendees" : "پائالىيەتلەر ، تەپسىلاتلار ۋە قاتناشقۇچىلارنى ئۆز ئىچىگە ئالغان كالېندارلار",
+ "Contacts and groups" : "ئالاقىلىشىش ۋە گۇرۇپپىلار",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "يوقلۇق ساقلاندى",
+ "Failed to save your absence settings" : "يوقلۇقىڭىزنى ساقلىيالمىدى",
+ "Absence cleared" : "يوقالغان",
+ "Failed to clear your absence settings" : "يوقلۇقىڭىزنى تەڭشىمىدى",
+ "First day" : "بىرىنچى كۈنى",
+ "Last day (inclusive)" : "ئالدىنقى كۈنى (ئۆز ئىچىگە ئالىدۇ)",
+ "Out of office replacement (optional)" : "ئىش ئورنىنى ئالماشتۇرۇش (ئىختىيارىي)",
+ "Name of the replacement" : "ئالماشتۇرغۇچىنىڭ ئىسمى",
+ "No results." : "ھېچقانداق نەتىجە يوق.",
+ "Start typing." : "يېزىشنى باشلاڭ.",
+ "Short absence status" : "قىسقا ۋاقىتلىق ھالەت",
+ "Long absence Message" : "ئۇزۇن بولمىغان ئۇچۇر",
+ "Save" : "ساقلا",
+ "Disable absence" : "يوقلۇقىنى چەكلەڭ",
+ "Failed to load availability" : "ئىشلىتىشچانلىقىنى يۈكلىيەلمىدى",
+ "Saved availability" : "تېجەشلىك",
+ "Failed to save availability" : "ئىشلەتكىلى بولمىدى",
+ "Time zone:" : "ۋاقىت رايونى:",
+ "to" : "to",
+ "Delete slot" : "ئورۇننى ئۆچۈرۈڭ",
+ "No working hours set" : "خىزمەت ۋاقتى بېكىتىلمىدى",
+ "Add slot" : "ئورۇن قوشۇڭ",
+ "Weekdays" : "ھەپتە كۈنلىرى",
+ "Pick a start time for {dayName}" : "{dayName} نىڭ باشلىنىش ۋاقتىنى تاللاڭ",
+ "Pick a end time for {dayName}" : "{dayName} نىڭ ئاخىرقى ۋاقتىنى تاللاڭ",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "بارلىق ئۇقتۇرۇشلارنى ئاۋازسىز قىلىش ئۈچۈن ئىشلەتكۈچى ھالىتىنى ئاپتوماتىك ھالدا «ئاۋارە قىلماڭ» قىلىپ تەڭشەڭ.",
+ "Cancel" : "بىكار قىلىش",
+ "Import" : "ئەكىر",
+ "Error while saving settings" : "تەڭشەكلەرنى ساقلاش جەريانىدا خاتالىق",
+ "Reset to default" : "سۈكۈتتىكى ھالىتىگە قايتىڭ",
+ "Availability" : "ئىشلەتكىلى بولىدۇ",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "ئەگەر خىزمەت ۋاقتىڭىزنى تەڭشىسىڭىز ، باشقىلار يىغىن زاكاز قىلغاندا سىزنىڭ ئىشتىن چۈشكەن ۋاقتىڭىزنى كۆرىدۇ.",
+ "Absence" : "يوق",
+ "Configure your next absence period." : "كېيىنكى يوقلۇق ۋاقتىڭىزنى سەپلەڭ.",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "يەنە {calendarappstoreopen} كالېندار دېتالى {linkclose} ياكى {calendardocopen} نى قاچىلاڭ ئۈستەلئۈستىڭىز ۋە كۆچمە تېلېفونىڭىزنى ئۇلاڭ ↗ {linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "{emailopen} ئېلېكترونلۇق خەت مۇلازىمېتىرى {linkclose} نى مۇۋاپىق تەڭشەشكە كاپالەتلىك قىلىڭ.",
+ "Calendar server" : "كالېندار مۇلازىمېتىرى",
+ "Send invitations to attendees" : "يىغىنغا قاتناشقۇچىلارغا تەكلىپنامە ئەۋەتىڭ",
+ "Automatically generate a birthday calendar" : "تۇغۇلغان كۈن كالېندارىنى ئاپتوماتىك ھاسىل قىلىدۇ",
+ "Birthday calendars will be generated by a background job." : "تۇغۇلغان كۈن كالېندارى ئارقا كۆرۈنۈش خىزمىتى ئارقىلىق بارلىققا كېلىدۇ.",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "شۇڭلاشقا ئۇلار قوزغىتىلغاندىن كېيىن دەرھال تېپىلمايدۇ ، ئەمما مەلۇم ۋاقىتتىن كېيىن پەيدا بولىدۇ.",
+ "Send notifications for events" : "ۋەقەلەرگە ئۇقتۇرۇش ئەۋەتىڭ",
+ "Notifications are sent via background jobs, so these must occur often enough." : "ئۇقتۇرۇش ئارقا كۆرۈنۈش خىزمىتى ئارقىلىق ئەۋەتىلىدۇ ، شۇڭا بۇلار چوقۇم يېتەرلىك بولۇشى كېرەك.",
+ "Send reminder notifications to calendar sharees as well" : "كالېندار ھەمبەھىرلىرىگىمۇ ئەسكەرتىش ئۇقتۇرۇشى ئەۋەتىڭ",
+ "Reminders are always sent to organizers and attendees." : "ئەسكەرتىش ھەمىشە تەشكىللىگۈچىلەر ۋە قاتناشقۇچىلارغا ئەۋەتىلىدۇ.",
+ "Enable notifications for events via push" : "ئىتتىرىش ئارقىلىق ۋەقەلەرگە ئۇقتۇرۇشنى قوزغىتىڭ",
+ "There was an error updating your attendance status." : "قاتنىشىش ھالىتىڭىزنى يېڭىلاشتا خاتالىق كۆرۈلدى.",
+ "Please contact the organizer directly." : "تەشكىللىگۈچى بىلەن بىۋاسىتە ئالاقىلىشىڭ.",
+ "Are you accepting the invitation?" : "تەكلىپنى قوبۇل قىلامسىز؟",
+ "Tentative" : "Tentative",
+ "Your attendance was updated successfully." : "سىزنىڭ قاتنىشىشىڭىز مۇۋەپپەقىيەتلىك يېڭىلاندى."
+},"pluralForm" :"nplurals=2; plural=(n != 1);"
+} \ No newline at end of file
diff --git a/apps/dav/l10n/uk.js b/apps/dav/l10n/uk.js
index 1f4e0a085be..3cc1b76d603 100644
--- a/apps/dav/l10n/uk.js
+++ b/apps/dav/l10n/uk.js
@@ -2,14 +2,16 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Календар",
- "Todos" : "До роботи",
+ "Tasks" : "Завдання",
"Personal" : "Особисте",
- "{actor} created calendar {calendar}" : "{actor} створив календар {calendar}",
+ "{actor} created calendar {calendar}" : "{actor} створив(-ла) календар {calendar}",
"You created calendar {calendar}" : "Ви створили календар {calendar}",
"{actor} deleted calendar {calendar}" : "{actor} вилучив календар {calendar}",
"You deleted calendar {calendar}" : "Ви вилучили календар {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} оновив календар {calendar}",
+ "{actor} updated calendar {calendar}" : "{actor} оновив(-ла) календар {calendar}",
"You updated calendar {calendar}" : "Ви оновили календар {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} відновив календар {calendar}",
+ "You restored calendar {calendar}" : "Ви відновили календар {calendar}",
"You shared calendar {calendar} as public link" : "Ви поширили календар {calendar} як публічне посилання",
"You removed public link for calendar {calendar}" : "Ви вилучили публічне посилання на календар {calendar}",
"{actor} shared calendar {calendar} with you" : "{actor} поширив календар {calendar} з вами",
@@ -23,33 +25,41 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} поширив календар {calendar} з групою {group}",
"You unshared calendar {calendar} from group {group}" : "Ви припинили поширення календаря {calendar} з групою {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} припинив поширення календаря {calendar} з групою {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} створив подію {event} у календарі {calendar}",
+ "Untitled event" : "Подія без назви",
+ "{actor} created event {event} in calendar {calendar}" : "{actor} створив(-ла) подію {event} у календарі {calendar}",
"You created event {event} in calendar {calendar}" : "Ви створили подію {event} у календарі {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} вилучив подію {event} з календаря {calendar}",
"You deleted event {event} from calendar {calendar}" : "Ви вилучили подію {event} з календаря {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} оновив подію {event} у календарі {calendar}",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} оновив(-ла) подію {event} у календарі {calendar}",
"You updated event {event} in calendar {calendar}" : "Ви оновили подію {event} у календарі {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} перемістив подію {event} з календаря {sourceCalendar} до календаря {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Ви перемістили подію {event} з календаря {sourceCalendar} до календаря {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} відновив подію {event} календаря {calendar}",
+ "You restored event {event} of calendar {calendar}" : "Ви відновили подію {event} календаря {calendar}",
"Busy" : "Зайнято",
- "{actor} created todo {todo} in list {calendar}" : "{actor} створив завдання {todo} у списку {calendar}",
- "You created todo {todo} in list {calendar}" : "Ви створили завдання {todo} у списку {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} вилучили завдання {todo} зі списку {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Ви вилучили завдання {todo} зі списку {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} оновив завдання {todo} у списку {calendar}",
- "You updated todo {todo} in list {calendar}" : "Ви оновили завдання {todo} у списку {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} виконав завдання {todo} зі списку {calendar}",
- "You solved todo {todo} in list {calendar}" : "Ви виконали завдання {todo} зі списку {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} повторно відкрив завдання {todo} у списку {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Ви повторно відкрили завдання {todo} у списку {calendar}",
- "A <strong>calendar</strong> was modified" : "<strong>Календар</strong> був змінений",
- "A calendar <strong>event</strong> was modified" : "Календарна <strong>подія</strong> була змінена",
- "A calendar <strong>todo</strong> was modified" : "Календарне <strong>завдання</strong> було змінене",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} створив(-ла) завдання {todo} у списку {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Ви створили завдання {todo} у списку {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} вилучив(-ла) справу {todo} зі списку {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Ви вилучили завдання {todo} зі списку {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} оновив(-ла) завдання {todo} у списку {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Ви оновили завдання {todo} у списку {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} виконав(-ла) завдання {todo} зі списку {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Ви виконали завдання {todo} зі списку {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} знову відкрив(-ла) завдання у списку {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Ви знову відкрили завдання {todo} зі списку {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} перемістив(-ла) завдання {todo} зі списку {sourceCalendar} до списку {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Ви перемістили завдання {todo} зі списку {sourceCalendar} до списку {targetCalendar}",
+ "Calendar, contacts and tasks" : "Календар, контакти та завдання",
+ "A <strong>calendar</strong> was modified" : "<strong>Календар</strong> було змінено",
+ "A calendar <strong>event</strong> was modified" : "Календарну <strong>подію</strong> було змінено",
+ "A calendar <strong>to-do</strong> was modified" : "<strong>Завдання</strong> в календарі змінено",
"Contact birthdays" : "Дні народження контактів",
"Death of %s" : "Смерть %s",
+ "Untitled calendar" : "Календар без назви",
"Calendar:" : "Календар:",
"Date:" : "Дата:",
"Where:" : "Місце:",
"Description:" : "Опис:",
- "Untitled event" : "Подія без назви",
"_%n year_::_%n years_" : ["%n рік","%n років","%n років","%n роки "],
"_%n month_::_%n months_" : ["%n місяць","%n місяців","%n місяців","%n місяці"],
"_%n day_::_%n days_" : ["%n день","%n днів","%n днів","%n дні"],
@@ -62,47 +72,267 @@ OC.L10N.register(
"Description: %s" : "Опис: %s",
"Where: %s" : "Місце: %s",
"%1$s via %2$s" : "%1$s через %2$s",
- "Invitation canceled" : "Запрошення скасоване",
- "Invitation updated" : "Запрошення оновлене",
+ "In the past on %1$s for the entire day" : "Раніше у %1$s протягом цілого дня",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["За хвилину на %1$s протягом усього дня","За %n хвилини на %1$s протягом усього дня","За %n хвилин на %1$s протягом усього дня","За %n хвилин на %1$s протягом усього дня"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["За годину на %1$s протягом усього дня","За %n години на %1$s протягом усього дня","За %n годин на %1$s протягом усього дня","За %n годин на %1$s протягом усього дня"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["За день на %1$s протягом усього дня","За %n дня на %1$s протягом усього дня","За %n днів на %1$s протягом усього дня","За %n днів на %1$s протягом усього дня"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Через тиждень на %1$ за весь день","Через %n тижнів на %1$ на весь день","Через %n тижнів на %1$ на весь день","Через %n тижнів на %1$ на весь день"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Через місяць на %1$ за весь день","За %n місяців на %1$ за весь день","За %n місяців на %1$ за весь день","За %n місяців на %1$ за весь день"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Через рік на %1$ за весь день","За %n років на %1$ за весь день","За %n років на %1$ за весь день","За %n років на %1$ за весь день"],
+ "In the past on %1$s between %2$s - %3$s" : "У минулому на %1$ між %2$ - %3$",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["За хвилину на %1$s між %2$s - %3$s","За %n хвилин на %1$s між %2$s - %3$s","За %n хвилин на %1$s між %2$s - %3$s","За %n хвилин на %1$s між %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["За годину з %1$s між %2$s - %3$s","За %n годин на %1$s між %2$s - %3$s","За %n годин на %1$s між %2$s - %3$s","За %n годин на %1$s між %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["За день на %1$s між %2$s - %3$s","За %n днів на %1$s між %2$s - %3$s","За %n днів на %1$s між %2$s - %3$s","За %n днів на %1$s між %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["За тиждень на %1$ між %2$ - %3$","За %n тижнів на %1$s між %2$s - %3$s","За %n тижнів на %1$s між %2$s - %3$s","За %n тижнів на %1$s між %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["За місяць на %1$ між %2$ - %3$","За %n місяців на %1$s між %2$s - %3$s","За %n місяців на %1$s між %2$s - %3$s","За %n місяців на %1$s між %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["За рік на %1$s між %2$s - %3$s","У %n років на %1$s між %2$s - %3$s","У %n років на %1$s між %2$s - %3$s","У %n років на %1$s між %2$s - %3$s"],
+ "Could not generate when statement" : "Не вдалося створити оператор \"коли\"",
+ "Every Day for the entire day" : "Щодня впродовж усього дня",
+ "Every Day for the entire day until %1$s" : "Щодня впродовж усього дня до %1$s",
+ "Every Day between %1$s - %2$s" : "Щодня між %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Щодня між %1$s- %2$sдо %3$s ",
+ "Every %1$d Days for the entire day" : "Що %1$d днів впродовж усього дня",
+ "Every %1$d Days for the entire day until %2$s" : "Що %1$d днів впродовж усього дня до %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Що %1$dднів між %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Що %1$d днів між %2$s - %3$s до %4$s",
+ "Could not generate event recurrence statement" : "Не вдалося створити оператор повторення події",
+ "Every Week on %1$s for the entire day" : "Щотижня в %1$s впродовж усього дня",
+ "Every Week on %1$s for the entire day until %2$s" : "Щотижня в %1$s впродовж усього дня до %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Щотижня в %1$s між %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Щотижня в %1$s між %2$s - %3$s до %4$s ",
+ "Every %1$d Weeks on %2$s for the entire day" : "Що %1$d тижні в %2$s впродовж усього дня ",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Що %1$dтижні в %2$s впродовж усього дня до %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Що %1$d тижні в %2$s між %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Що %1$d тижні в %2$s між %3$s - %4$s до %5$s",
+ "Every Month on the %1$s for the entire day" : "Щомісяця %1$s впродовж усього дня ",
+ "Every Month on the %1$s for the entire day until %2$s" : "Щомісяця %1$s впродовж усього дня до %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Щомісяця %1$s між %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Щомісяця %1$s між %2$s - %3$s до %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Що %1$d місяці %2$s впродовж усього дня",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Що %1$d місяці %2$s впродовж усього дня до %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Що %1$d місяці %2$s між %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Що %1$dмісяці %2$s між %3$s - %4$s до %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Щороку в %1$s, %2$s, впродовж усього дня",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Щороку в %1$s, %2$s, впродовж усього дня до %3$s ",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Щороку в %1$s, %2$s, між %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Щороку в %1$s, %2$s, між %3$s - %4$s до %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Що %1$d роки в %2$s, %3$s, впродовж усього дня",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Що %1$d роки в %2$s, %3$s, впродовж усього дня до %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Що %1$d роки в %2$s, %3$s, між %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Що %1$d роки в %2$s, %3$s, між %4$s - %5$s до %6$s",
+ "On specific dates for the entire day until %1$s" : "У визначені дати впродовж усього дня до %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "У визначені дати між %1$s - %2$s до %3$s",
+ "In the past on %1$s" : "Раніше у %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["За хвилину на %1$s","Через %n хвилин на %1$s","Через %n хвилин на %1$s","Через %n хвилин на %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Через годину на %1$s","Через %n годин на %1$s","Через %n годин на %1$s","Через %n годин на %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["За день на %1$s","Через %n днів на %1$s","Через %n днів на %1$s","Через %n днів на %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Через тиждень на %1$s","Через %n тижнів на %1$s","Через %n тижнів на %1$s","Через %n тижнів на %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Через місяць на %1$s","За %n місяців на %1$s","За %n місяців на %1$s","За %n місяців на %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Через рік на %1$s","Через %n років на %1$s","Через %n років на %1$s","Через %n років на %1$s"],
+ "In the past on %1$s then on %2$s" : "Раніше у %1$s, потім у %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Через хвилину на %1$, потім на %2$","Через %n хвилин на %1$s, потім на %2$s","Через %n хвилин на %1$s, потім на %2$s","Через %n хвилин на %1$s, потім на %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Через годину на %1$s, потім на %2$s","Через %n годин на %1$s, потім на %2$s","Через %n годин на %1$s, потім на %2$s","Через %n годин на %1$s, потім на %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Через день на %1$, потім на %2$","Через %n днів на %1$s, потім на %2$s","Через %n днів на %1$s, потім на %2$s","Через %n днів на %1$s, потім на %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Через тиждень на %1$, потім на %2$","Через %n тижнів на %1$, потім на %2$","Через %n тижнів на %1$, потім на %2$","Через %n тижнів на %1$, потім на %2$"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Через місяць на %1$, потім на %2$","Через %n місяців на %1$, потім на %2$","Через %n місяців на %1$, потім на %2$","Через %n місяців на %1$, потім на %2$"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Через рік на %1$, потім на %2$","Через %n років на %1$, потім на %2$","Через %n років на %1$, потім на %2$","Через %n років на %1$, потім на %2$"],
+ "In the past on %1$s then on %2$s and %3$s" : "У минулому на %1$, потім на %2$ і %3$",
+ "_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_" : ["За хвилину на %1$, потім на %2$ і %3$","Через %n хвилин на %1$s, потім на %2$s і %3$s","Через %n хвилин на %1$s, потім на %2$s і %3$s","Через %n хвилин на %1$s, потім на %2$s і %3$s"],
+ "_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_" : ["Через годину на %1$, потім на %2$ і %3$","Через %n годин на %1$, потім на %2$ і %3$","Через %n годин на %1$, потім на %2$ і %3$","Через %n годин на %1$, потім на %2$ і %3$"],
+ "_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_" : ["Через день на %1$, потім на %2$ і %3$","Через %n днів на %1$, потім на %2$ і %3$","Через %n днів на %1$, потім на %2$ і %3$","Через %n днів на %1$, потім на %2$ і %3$"],
+ "_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_" : ["Через тиждень на %1$, потім на %2$ і %3$","Через %n тижнів на %1$, потім на %2$ і %3$","Через %n тижнів на %1$, потім на %2$ і %3$","Через %n тижнів на %1$, потім на %2$ і %3$"],
+ "_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_" : ["Через місяць на %1$, потім на %2$ і %3$","Через %n місяців на %1$, потім на %2$ і %3$","Через %n місяців на %1$, потім на %2$ і %3$","Через %n місяців на %1$, потім на %2$ і %3$"],
+ "_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_" : ["Через рік на %1$, потім на %2$ і %3$","Через %n років на %1$, потім на %2$ і %3$","Через %n років на %1$, потім на %2$ і %3$","Через %n років на %1$, потім на %2$ і %3$"],
+ "Could not generate next recurrence statement" : "Не вдалося створити оператор наступного повторення",
+ "Cancelled: %1$s" : "Скасовано: %1$s",
+ "\"%1$s\" has been canceled" : "\"%1$s\" скасовано",
+ "Re: %1$s" : "Re: %1$s",
+ "%1$s has accepted your invitation" : "%1$s прийняв ваше запрошення",
+ "%1$s has tentatively accepted your invitation" : "%1$s попередньо прийняв ваше запрошення",
+ "%1$s has declined your invitation" : "%1$s відмовився від вашого запрошення",
+ "%1$s has responded to your invitation" : "%1$sвідповів(-ла) на ваше запрошення",
+ "Invitation updated: %1$s" : "Запрошення оновлено: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s оновив(-ла) подію \"%2$s\"",
+ "Invitation: %1$s" : "Запрошення: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s запрошує вас до \"%2$s\"",
+ "Organizer:" : "Організатор:",
+ "Attendees:" : "Учасники:",
+ "Title:" : "Назва:",
+ "When:" : "Коли:",
"Location:" : "Місцевість:",
"Link:" : "Посилання:",
+ "Occurring:" : "Відбувається:",
"Accept" : "Прийняти",
"Decline" : "Відхилити",
"More options …" : "Більше варіантів …",
"More options at %s" : "Більше варіантів на %s",
- "Contacts" : "Контакти",
- "Upgrade needed" : "Потрібно оновитися",
- "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Ваш %s мусить бути налаштований на використання HTTPS для надання доступу до CalDAV і CardDAV з iOS/macOS.",
- "Configures a CalDAV account" : "Налаштовує обліковий запис CalDAV",
- "Configures a CardDAV account" : "Налаштовує обліковий запис CardDAV",
- "Tasks" : "Завдання",
- "Untitled task" : "Завдання без назви",
- "WebDAV" : "WebDAV",
- "WebDAV endpoint" : "Точка доступу WebDAV",
- "to" : "до",
- "Monday" : "понеділок",
+ "Monday" : "Понеділок",
"Tuesday" : "Вівторок",
"Wednesday" : "Середа",
"Thursday" : "Четвер",
"Friday" : "П'ятниця",
"Saturday" : "Субота",
"Sunday" : "Неділя",
+ "January" : "Січень",
+ "February" : "Лютий",
+ "March" : "Березень",
+ "April" : "Квітень",
+ "May" : "Травень",
+ "June" : "Червень",
+ "July" : "Липень",
+ "August" : "Серпень",
+ "September" : "Вересень",
+ "October" : "Жовтень",
+ "November" : "Листопад",
+ "December" : "Грудень",
+ "First" : "Перший",
+ "Second" : "Другий",
+ "Third" : "Третій",
+ "Fourth" : "Четвертий",
+ "Fifth" : "П'ятий",
+ "Last" : "Останній",
+ "Second Last" : "Другий(-а) останній(-я)",
+ "Third Last" : "Третій(-я) останній(-я)",
+ "Fourth Last" : "Четвертий(-а) останній(-я)",
+ "Fifth Last" : "Останній п'ятий",
+ "Contacts" : "Контакти",
+ "{actor} created address book {addressbook}" : "{actor} створив(-ла) адресну книгу {addressbook}",
+ "You created address book {addressbook}" : "Ви створили адресну книгу {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} вилучив(-ла) адресну книгу {addressbook}",
+ "You deleted address book {addressbook}" : "Ви вилучили адресну книгу {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} оновив(-ла) адресну книгу {addressbook}",
+ "You updated address book {addressbook}" : "Ви оновили адресну книгу {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} поділив(-ла-)ся з вами адресною книгою {addressbook}",
+ "You shared address book {addressbook} with {user}" : "Ви надали доступ до адресної книги {addressbook} користувачу {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} поділив(-ла-)ся адресною книгою {addressbook} з {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} не надіслана адресна книга {addressbook} від вас",
+ "You unshared address book {addressbook} from {user}" : "Ви скасували спільний доступ до адресної книги {addressbook} від користувача {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} не має спільного доступу до адресної книги {addressbook} від {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} забрав доступ до адресної книги {addressbook} від себе",
+ "You shared address book {addressbook} with group {group}" : "Ви надали спільний доступ до адресної книги {addressbook} групі {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} поділив(-ла-)ся адресною книгою {addressbook} із групою {group}",
+ "You unshared address book {addressbook} from group {group}" : "Ви скасували спільний доступ до адресної книги {addressbook} із групи {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} не має спільного доступу до адресної книги {addressbook} із групи {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} створив(-ла) контакт {card} в адресній книзі {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Ви створили контакт {card} в адресній книзі {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} вилучив(-ла) контакт {card} з адресної книги {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Ви вилучили контакт {card} з адресної книги {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} оновив(-ла) контакт {card} в адресній книзі {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Ви оновили контакт {card} в адресній книзі {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>Контактну</strong> або <strong>адресну книгу</strong> було змінено",
+ "Accounts" : "Облікові записи",
+ "System address book which holds all accounts" : "Системна адресна книга, в якій містяться всі облікові записи",
+ "File is not updatable: %1$s" : "Файл не оновлюється: %1$s",
+ "Failed to get storage for file" : "Не вдалося отримати дані про сховище для файлу",
+ "Could not write to final file, canceled by hook" : "Не вдалося записати в остаточний файл, скасовано перехопленням",
+ "Could not write file contents" : "Не вдалося записати вміст файлу",
+ "_%n byte_::_%n bytes_" : ["%n байт","%n байтів","%n байтів","%n байтів"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Помилка під час копіювання файлу в цільове розташування (скопійовано: %1$s, очікуваний розмір файлу: %2$s)",
+ "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." : "Очікуваний розмір файлу %1$s, але читання (з клієнта Nextcloud) і запис (до сховища Nextcloud) %2$s. Це може бути проблема мережі на стороні надсилання або проблема запису в сховище на стороні сервера.",
+ "Could not rename part file to final file, canceled by hook" : "Не вдалося перейменувати файл частини на остаточний файл, скасовано підхопленням",
+ "Could not rename part file to final file" : "Не вдалося перейменувати файл частини на остаточний файл",
+ "Failed to check file size: %1$s" : "Не вдалося перевірити розмір файлу: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "Неможливо відкрити файл: %1$s, хоча файл скоріше за все присутній",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Неможливо відкрити файл: %1$s, скоріше за все файл відсутній",
+ "Encryption not ready: %1$s" : "Шифрування не готове: %1$s",
+ "Failed to open file: %1$s" : "Не вдалося відкрити файл: %1$s",
+ "Failed to unlink: %1$s" : "Не вдалося від’єднати: %1$s",
+ "Failed to write file contents: %1$s" : "Не вдалося записати вміст файлу: %1$s",
+ "File not found: %1$s" : "Файл не знайдено: %1$s",
+ "Invalid target path" : "Недійсний шлях призначення",
+ "System is in maintenance mode." : "Система в режимі обслуговування.",
+ "Upgrade needed" : "Потрібне оновлення",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Ваш %s мусить бути налаштований на використання HTTPS для надання доступу до CalDAV і CardDAV з iOS/macOS.",
+ "Configures a CalDAV account" : "Налаштовує обліковий запис CalDAV",
+ "Configures a CardDAV account" : "Налаштовує обліковий запис CardDAV",
+ "Events" : "Події",
+ "Untitled task" : "Завдання без назви",
+ "Completed on %s" : "Завершено %s",
+ "Due on %s by %s" : "До %s з боку %s",
+ "Due on %s" : "До %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Ласкаво просимо до календаря Nextcloud!\n\nЦе зразок події — ознайомтеся з гнучкістю планування за допомогою календаря Nextcloud, вносячи будь-які зміни, які вам заманеться!\n\nЗа допомогою календаря Nextcloud ви можете:\n- Легко створювати, редагувати та керувати подіями.\n- Створювати кілька календарів і ділитися ними з колегами, друзями або родиною.\n- Перевіряти доступність і показувати іншим, коли ви зайняті.\n- Безперешкодно інтегруватися з додатками та пристроями через CalDAV.\n- Налаштовувати свій досвід: планувати повторювані події, налаштовувати сповіщення та інші параметри.",
+ "Example event - open me!" : "Приклад події - відкрий мене!",
+ "System Address Book" : "Системна адресна книга",
+ "The system address book contains contact information for all users in your instance." : "Системна адресна книга містить контактну інформацію для всіх користувачів вашого примірника хмари.",
+ "Enable System Address Book" : "Увімкнути системну адресну книгу",
+ "DAV system address book" : "Системна адресна книга DAV",
+ "No outstanding DAV system address book sync." : "Немає незавершеної синхронізації системної адресної книги DAV.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Синхронізація системної адресної книги DAV ще не запускалася, оскільки, або ваша система вже має понад 1000 користувачів, або сталася помилка. Будь ласка, запустіть синхронізацію вручну за допомогою команди \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "Точка доступу WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Неможливо перевірити, чи на вашому вебсервері правильно налаштовано доступ для синхронізації файлів через протокол WebDAV. Перевірте це вручну.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Ваш вебсервер не налаштований як треба для синхронізації файлів, схоже інтерфейс WebDAV поламаний.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "На вашому вебсерверві правильно налаштовано доступ для синхронізації файлів через протокол WevDAV.",
+ "Migrated calendar (%1$s)" : "Перенесений календар (%1$s)",
+ "Calendars including events, details and attendees" : "Календарі, включно з подіями, деталями та відвідувачами",
+ "Contacts and groups" : "Контакти та групи",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "Відсутність збережено",
+ "Failed to save your absence settings" : "Не вдалося зберегти налаштуванння про відсутність",
+ "Absence cleared" : "Відсутність вилучено",
+ "Failed to clear your absence settings" : "Не вдалося скинути ваші налаштування про відсутність",
+ "First day" : "Перший день",
+ "Last day (inclusive)" : "Останній день (включно)",
+ "Out of office replacement (optional)" : "Заміна під час відсутності (необов'язково)",
+ "Name of the replacement" : "Ім'я заступника",
+ "No results." : "Відсутні результати.",
+ "Start typing." : "Почніть вводити.",
+ "Short absence status" : "Короткий статус відсутности",
+ "Long absence Message" : "Довге повідомлення про відсутність",
"Save" : "Зберегти",
- "Calendar server" : "Сервер календаря",
- "Send invitations to attendees" : "Надіслати запрошення учасникам",
- "Automatically generate a birthday calendar" : "Автоматично згенерувати календар днів народження",
+ "Disable absence" : "Вимкнути відсутність",
+ "Failed to load availability" : "Не вдалося завантажити доступність",
+ "Saved availability" : "Збережена наявність",
+ "Failed to save availability" : "Не вдалося зберегти наявність",
+ "Time zone:" : "Часовий пояс:",
+ "to" : "до",
+ "Delete slot" : "Вилучити діапазон",
+ "No working hours set" : "Робочий час не встановлено",
+ "Add slot" : "Додати діапазон",
+ "Weekdays" : "Дні тижня",
+ "Pick a start time for {dayName}" : "Виберіть час початку для {dayName}",
+ "Pick a end time for {dayName}" : "Виберіть час завершення для {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Автоматично встановлювати статус користувача у \"Не турбувати\", коли ви не доступні. В цей проміжок часу ви не отримуватимете сповіщення.",
+ "Cancel" : "Скасувати",
+ "Import" : "Імпорт",
+ "Error while saving settings" : "Помилка під час збереження налаштувань",
+ "Contact reset successfully" : "Контакт успішно скинуто",
+ "Error while resetting contact" : "Помилка під час скидання контакту",
+ "Contact imported successfully" : "Контакт імпортовано успішно",
+ "Error while importing contact" : "Помилка під час імпортування контакту",
+ "Import contact" : "Імпортувати контакт",
+ "Reset to default" : "Скинути до типового",
+ "Import contacts" : "Імпортувати контакти",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Імпорт нового файлу .vcf призведе до видалення наявного контакту за замовчуванням і заміни його новим. Чи хочете ви продовжити?",
+ "Failed to save example event creation setting" : "Не вдалося зберегти приклад налаштування створення події",
+ "Failed to upload the example event" : "Не вдалося завантажити приклад події",
+ "Custom example event was saved successfully" : "Приклад користувацької події було успішно збережено",
+ "Failed to delete the custom example event" : "Не вдалося видалити приклад користувацького події",
+ "Custom example event was deleted successfully" : "Приклад користувацької події було успішно видалено",
+ "Import calendar event" : "Імпортувати подію календаря",
+ "Uploading a new event will overwrite the existing one." : "Завантаження нового заходу призведе до заміни наявного.",
+ "Upload event" : "Завантажити подію",
+ "Availability" : "Доступність",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Будь ласка, налаштуйте ваші робочі години, щоб інші користувачі могли бачити, коли ви відсутні під час створення зустрічей.",
+ "Absence" : "Відсутність",
+ "Configure your next absence period." : "Налаштувати проміжки вашої відсутности",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Встановіть {calendarappstoreopen}застосунок Календар{linkclose}, {calendardocopen}з'єднайте ваш ноутбук та смартфон для синхронізації вашого календаря ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Будь-ласка переконайтеся у коректності налаштувань {emailopen}поштового сервера{linkclose}.",
+ "Calendar server" : "Календар",
+ "Send invitations to attendees" : "Надсилати запрошення учасникам",
+ "Automatically generate a birthday calendar" : "Автоматично створити календар днів народження",
"Birthday calendars will be generated by a background job." : "Календар днів народження буде згенеровано у фоновому завданні.",
"Hence they will not be available immediately after enabling but will show up after some time." : "Отже вони не будуть доступні одразу після увімкнення, але з'являться згодом.",
- "Send notifications for events" : "Відправити сповіщення для подій",
+ "Send notifications for events" : "Надсилати сповіщення про події",
"Notifications are sent via background jobs, so these must occur often enough." : "Сповіщення надсилаються у фонових завданнях, тож вони мають запускатися достатньо часто.",
+ "Send reminder notifications to calendar sharees as well" : "Також надсилайте нагадування користувачам вашого спільного календаря",
+ "Reminders are always sent to organizers and attendees." : "Нагадування завжди надсилаються організаторам і учасникам.",
"Enable notifications for events via push" : "Увімкнути push-сповіщення для подій",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Встановіть також {calendarappstoreopen}застосунок Календар{linkclose}, або {calendardocopen}з'єднайте ваш декстоп і мобільний для синхронізації ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Будь-ласка переконайтеся у коректності налаштувань {emailopen}поштового сервера{linkclose}.",
+ "Example content" : "Приклад вмісту",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Приклад вмісту служить для демонстрації функцій Nextcloud. Стандартний вміст постачається разом із Nextcloud і може бути замінений на власний вміст.",
"There was an error updating your attendance status." : "Виникла помилка при оновленні вашого статусу учасника.",
"Please contact the organizer directly." : "Будь-ласка повідомте організатора.",
"Are you accepting the invitation?" : "Чи приймаєте ви запрошення?",
"Tentative" : "Попередній",
- "Comment" : "Коментар",
"Your attendance was updated successfully." : "Ваша участь успішно оновлена."
},
"nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);");
diff --git a/apps/dav/l10n/uk.json b/apps/dav/l10n/uk.json
index 177d7e8f2b0..ae2f2b370ec 100644
--- a/apps/dav/l10n/uk.json
+++ b/apps/dav/l10n/uk.json
@@ -1,13 +1,15 @@
{ "translations": {
"Calendar" : "Календар",
- "Todos" : "До роботи",
+ "Tasks" : "Завдання",
"Personal" : "Особисте",
- "{actor} created calendar {calendar}" : "{actor} створив календар {calendar}",
+ "{actor} created calendar {calendar}" : "{actor} створив(-ла) календар {calendar}",
"You created calendar {calendar}" : "Ви створили календар {calendar}",
"{actor} deleted calendar {calendar}" : "{actor} вилучив календар {calendar}",
"You deleted calendar {calendar}" : "Ви вилучили календар {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} оновив календар {calendar}",
+ "{actor} updated calendar {calendar}" : "{actor} оновив(-ла) календар {calendar}",
"You updated calendar {calendar}" : "Ви оновили календар {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} відновив календар {calendar}",
+ "You restored calendar {calendar}" : "Ви відновили календар {calendar}",
"You shared calendar {calendar} as public link" : "Ви поширили календар {calendar} як публічне посилання",
"You removed public link for calendar {calendar}" : "Ви вилучили публічне посилання на календар {calendar}",
"{actor} shared calendar {calendar} with you" : "{actor} поширив календар {calendar} з вами",
@@ -21,33 +23,41 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} поширив календар {calendar} з групою {group}",
"You unshared calendar {calendar} from group {group}" : "Ви припинили поширення календаря {calendar} з групою {group}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} припинив поширення календаря {calendar} з групою {group}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} створив подію {event} у календарі {calendar}",
+ "Untitled event" : "Подія без назви",
+ "{actor} created event {event} in calendar {calendar}" : "{actor} створив(-ла) подію {event} у календарі {calendar}",
"You created event {event} in calendar {calendar}" : "Ви створили подію {event} у календарі {calendar}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} вилучив подію {event} з календаря {calendar}",
"You deleted event {event} from calendar {calendar}" : "Ви вилучили подію {event} з календаря {calendar}",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} оновив подію {event} у календарі {calendar}",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} оновив(-ла) подію {event} у календарі {calendar}",
"You updated event {event} in calendar {calendar}" : "Ви оновили подію {event} у календарі {calendar}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} перемістив подію {event} з календаря {sourceCalendar} до календаря {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Ви перемістили подію {event} з календаря {sourceCalendar} до календаря {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} відновив подію {event} календаря {calendar}",
+ "You restored event {event} of calendar {calendar}" : "Ви відновили подію {event} календаря {calendar}",
"Busy" : "Зайнято",
- "{actor} created todo {todo} in list {calendar}" : "{actor} створив завдання {todo} у списку {calendar}",
- "You created todo {todo} in list {calendar}" : "Ви створили завдання {todo} у списку {calendar}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} вилучили завдання {todo} зі списку {calendar}",
- "You deleted todo {todo} from list {calendar}" : "Ви вилучили завдання {todo} зі списку {calendar}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} оновив завдання {todo} у списку {calendar}",
- "You updated todo {todo} in list {calendar}" : "Ви оновили завдання {todo} у списку {calendar}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} виконав завдання {todo} зі списку {calendar}",
- "You solved todo {todo} in list {calendar}" : "Ви виконали завдання {todo} зі списку {calendar}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} повторно відкрив завдання {todo} у списку {calendar}",
- "You reopened todo {todo} in list {calendar}" : "Ви повторно відкрили завдання {todo} у списку {calendar}",
- "A <strong>calendar</strong> was modified" : "<strong>Календар</strong> був змінений",
- "A calendar <strong>event</strong> was modified" : "Календарна <strong>подія</strong> була змінена",
- "A calendar <strong>todo</strong> was modified" : "Календарне <strong>завдання</strong> було змінене",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} створив(-ла) завдання {todo} у списку {calendar}",
+ "You created to-do {todo} in list {calendar}" : "Ви створили завдання {todo} у списку {calendar}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} вилучив(-ла) справу {todo} зі списку {calendar}",
+ "You deleted to-do {todo} from list {calendar}" : "Ви вилучили завдання {todo} зі списку {calendar}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} оновив(-ла) завдання {todo} у списку {calendar}",
+ "You updated to-do {todo} in list {calendar}" : "Ви оновили завдання {todo} у списку {calendar}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} виконав(-ла) завдання {todo} зі списку {calendar}",
+ "You solved to-do {todo} in list {calendar}" : "Ви виконали завдання {todo} зі списку {calendar}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} знову відкрив(-ла) завдання у списку {calendar}",
+ "You reopened to-do {todo} in list {calendar}" : "Ви знову відкрили завдання {todo} зі списку {calendar}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} перемістив(-ла) завдання {todo} зі списку {sourceCalendar} до списку {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Ви перемістили завдання {todo} зі списку {sourceCalendar} до списку {targetCalendar}",
+ "Calendar, contacts and tasks" : "Календар, контакти та завдання",
+ "A <strong>calendar</strong> was modified" : "<strong>Календар</strong> було змінено",
+ "A calendar <strong>event</strong> was modified" : "Календарну <strong>подію</strong> було змінено",
+ "A calendar <strong>to-do</strong> was modified" : "<strong>Завдання</strong> в календарі змінено",
"Contact birthdays" : "Дні народження контактів",
"Death of %s" : "Смерть %s",
+ "Untitled calendar" : "Календар без назви",
"Calendar:" : "Календар:",
"Date:" : "Дата:",
"Where:" : "Місце:",
"Description:" : "Опис:",
- "Untitled event" : "Подія без назви",
"_%n year_::_%n years_" : ["%n рік","%n років","%n років","%n роки "],
"_%n month_::_%n months_" : ["%n місяць","%n місяців","%n місяців","%n місяці"],
"_%n day_::_%n days_" : ["%n день","%n днів","%n днів","%n дні"],
@@ -60,47 +70,267 @@
"Description: %s" : "Опис: %s",
"Where: %s" : "Місце: %s",
"%1$s via %2$s" : "%1$s через %2$s",
- "Invitation canceled" : "Запрошення скасоване",
- "Invitation updated" : "Запрошення оновлене",
+ "In the past on %1$s for the entire day" : "Раніше у %1$s протягом цілого дня",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["За хвилину на %1$s протягом усього дня","За %n хвилини на %1$s протягом усього дня","За %n хвилин на %1$s протягом усього дня","За %n хвилин на %1$s протягом усього дня"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["За годину на %1$s протягом усього дня","За %n години на %1$s протягом усього дня","За %n годин на %1$s протягом усього дня","За %n годин на %1$s протягом усього дня"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["За день на %1$s протягом усього дня","За %n дня на %1$s протягом усього дня","За %n днів на %1$s протягом усього дня","За %n днів на %1$s протягом усього дня"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Через тиждень на %1$ за весь день","Через %n тижнів на %1$ на весь день","Через %n тижнів на %1$ на весь день","Через %n тижнів на %1$ на весь день"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["Через місяць на %1$ за весь день","За %n місяців на %1$ за весь день","За %n місяців на %1$ за весь день","За %n місяців на %1$ за весь день"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Через рік на %1$ за весь день","За %n років на %1$ за весь день","За %n років на %1$ за весь день","За %n років на %1$ за весь день"],
+ "In the past on %1$s between %2$s - %3$s" : "У минулому на %1$ між %2$ - %3$",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["За хвилину на %1$s між %2$s - %3$s","За %n хвилин на %1$s між %2$s - %3$s","За %n хвилин на %1$s між %2$s - %3$s","За %n хвилин на %1$s між %2$s - %3$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["За годину з %1$s між %2$s - %3$s","За %n годин на %1$s між %2$s - %3$s","За %n годин на %1$s між %2$s - %3$s","За %n годин на %1$s між %2$s - %3$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["За день на %1$s між %2$s - %3$s","За %n днів на %1$s між %2$s - %3$s","За %n днів на %1$s між %2$s - %3$s","За %n днів на %1$s між %2$s - %3$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["За тиждень на %1$ між %2$ - %3$","За %n тижнів на %1$s між %2$s - %3$s","За %n тижнів на %1$s між %2$s - %3$s","За %n тижнів на %1$s між %2$s - %3$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["За місяць на %1$ між %2$ - %3$","За %n місяців на %1$s між %2$s - %3$s","За %n місяців на %1$s між %2$s - %3$s","За %n місяців на %1$s між %2$s - %3$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["За рік на %1$s між %2$s - %3$s","У %n років на %1$s між %2$s - %3$s","У %n років на %1$s між %2$s - %3$s","У %n років на %1$s між %2$s - %3$s"],
+ "Could not generate when statement" : "Не вдалося створити оператор \"коли\"",
+ "Every Day for the entire day" : "Щодня впродовж усього дня",
+ "Every Day for the entire day until %1$s" : "Щодня впродовж усього дня до %1$s",
+ "Every Day between %1$s - %2$s" : "Щодня між %1$s - %2$s",
+ "Every Day between %1$s - %2$s until %3$s" : "Щодня між %1$s- %2$sдо %3$s ",
+ "Every %1$d Days for the entire day" : "Що %1$d днів впродовж усього дня",
+ "Every %1$d Days for the entire day until %2$s" : "Що %1$d днів впродовж усього дня до %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Що %1$dднів між %2$s - %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Що %1$d днів між %2$s - %3$s до %4$s",
+ "Could not generate event recurrence statement" : "Не вдалося створити оператор повторення події",
+ "Every Week on %1$s for the entire day" : "Щотижня в %1$s впродовж усього дня",
+ "Every Week on %1$s for the entire day until %2$s" : "Щотижня в %1$s впродовж усього дня до %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Щотижня в %1$s між %2$s - %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Щотижня в %1$s між %2$s - %3$s до %4$s ",
+ "Every %1$d Weeks on %2$s for the entire day" : "Що %1$d тижні в %2$s впродовж усього дня ",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Що %1$dтижні в %2$s впродовж усього дня до %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Що %1$d тижні в %2$s між %3$s - %4$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Що %1$d тижні в %2$s між %3$s - %4$s до %5$s",
+ "Every Month on the %1$s for the entire day" : "Щомісяця %1$s впродовж усього дня ",
+ "Every Month on the %1$s for the entire day until %2$s" : "Щомісяця %1$s впродовж усього дня до %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "Щомісяця %1$s між %2$s - %3$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Щомісяця %1$s між %2$s - %3$s до %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "Що %1$d місяці %2$s впродовж усього дня",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "Що %1$d місяці %2$s впродовж усього дня до %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "Що %1$d місяці %2$s між %3$s - %4$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Що %1$dмісяці %2$s між %3$s - %4$s до %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "Щороку в %1$s, %2$s, впродовж усього дня",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Щороку в %1$s, %2$s, впродовж усього дня до %3$s ",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Щороку в %1$s, %2$s, між %3$s - %4$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Щороку в %1$s, %2$s, між %3$s - %4$s до %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "Що %1$d роки в %2$s, %3$s, впродовж усього дня",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Що %1$d роки в %2$s, %3$s, впродовж усього дня до %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Що %1$d роки в %2$s, %3$s, між %4$s - %5$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Що %1$d роки в %2$s, %3$s, між %4$s - %5$s до %6$s",
+ "On specific dates for the entire day until %1$s" : "У визначені дати впродовж усього дня до %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "У визначені дати між %1$s - %2$s до %3$s",
+ "In the past on %1$s" : "Раніше у %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["За хвилину на %1$s","Через %n хвилин на %1$s","Через %n хвилин на %1$s","Через %n хвилин на %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Через годину на %1$s","Через %n годин на %1$s","Через %n годин на %1$s","Через %n годин на %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["За день на %1$s","Через %n днів на %1$s","Через %n днів на %1$s","Через %n днів на %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["Через тиждень на %1$s","Через %n тижнів на %1$s","Через %n тижнів на %1$s","Через %n тижнів на %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["Через місяць на %1$s","За %n місяців на %1$s","За %n місяців на %1$s","За %n місяців на %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["Через рік на %1$s","Через %n років на %1$s","Через %n років на %1$s","Через %n років на %1$s"],
+ "In the past on %1$s then on %2$s" : "Раніше у %1$s, потім у %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Через хвилину на %1$, потім на %2$","Через %n хвилин на %1$s, потім на %2$s","Через %n хвилин на %1$s, потім на %2$s","Через %n хвилин на %1$s, потім на %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Через годину на %1$s, потім на %2$s","Через %n годин на %1$s, потім на %2$s","Через %n годин на %1$s, потім на %2$s","Через %n годин на %1$s, потім на %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Через день на %1$, потім на %2$","Через %n днів на %1$s, потім на %2$s","Через %n днів на %1$s, потім на %2$s","Через %n днів на %1$s, потім на %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Через тиждень на %1$, потім на %2$","Через %n тижнів на %1$, потім на %2$","Через %n тижнів на %1$, потім на %2$","Через %n тижнів на %1$, потім на %2$"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Через місяць на %1$, потім на %2$","Через %n місяців на %1$, потім на %2$","Через %n місяців на %1$, потім на %2$","Через %n місяців на %1$, потім на %2$"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Через рік на %1$, потім на %2$","Через %n років на %1$, потім на %2$","Через %n років на %1$, потім на %2$","Через %n років на %1$, потім на %2$"],
+ "In the past on %1$s then on %2$s and %3$s" : "У минулому на %1$, потім на %2$ і %3$",
+ "_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_" : ["За хвилину на %1$, потім на %2$ і %3$","Через %n хвилин на %1$s, потім на %2$s і %3$s","Через %n хвилин на %1$s, потім на %2$s і %3$s","Через %n хвилин на %1$s, потім на %2$s і %3$s"],
+ "_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_" : ["Через годину на %1$, потім на %2$ і %3$","Через %n годин на %1$, потім на %2$ і %3$","Через %n годин на %1$, потім на %2$ і %3$","Через %n годин на %1$, потім на %2$ і %3$"],
+ "_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_" : ["Через день на %1$, потім на %2$ і %3$","Через %n днів на %1$, потім на %2$ і %3$","Через %n днів на %1$, потім на %2$ і %3$","Через %n днів на %1$, потім на %2$ і %3$"],
+ "_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_" : ["Через тиждень на %1$, потім на %2$ і %3$","Через %n тижнів на %1$, потім на %2$ і %3$","Через %n тижнів на %1$, потім на %2$ і %3$","Через %n тижнів на %1$, потім на %2$ і %3$"],
+ "_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_" : ["Через місяць на %1$, потім на %2$ і %3$","Через %n місяців на %1$, потім на %2$ і %3$","Через %n місяців на %1$, потім на %2$ і %3$","Через %n місяців на %1$, потім на %2$ і %3$"],
+ "_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_" : ["Через рік на %1$, потім на %2$ і %3$","Через %n років на %1$, потім на %2$ і %3$","Через %n років на %1$, потім на %2$ і %3$","Через %n років на %1$, потім на %2$ і %3$"],
+ "Could not generate next recurrence statement" : "Не вдалося створити оператор наступного повторення",
+ "Cancelled: %1$s" : "Скасовано: %1$s",
+ "\"%1$s\" has been canceled" : "\"%1$s\" скасовано",
+ "Re: %1$s" : "Re: %1$s",
+ "%1$s has accepted your invitation" : "%1$s прийняв ваше запрошення",
+ "%1$s has tentatively accepted your invitation" : "%1$s попередньо прийняв ваше запрошення",
+ "%1$s has declined your invitation" : "%1$s відмовився від вашого запрошення",
+ "%1$s has responded to your invitation" : "%1$sвідповів(-ла) на ваше запрошення",
+ "Invitation updated: %1$s" : "Запрошення оновлено: %1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s оновив(-ла) подію \"%2$s\"",
+ "Invitation: %1$s" : "Запрошення: %1$s",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s запрошує вас до \"%2$s\"",
+ "Organizer:" : "Організатор:",
+ "Attendees:" : "Учасники:",
+ "Title:" : "Назва:",
+ "When:" : "Коли:",
"Location:" : "Місцевість:",
"Link:" : "Посилання:",
+ "Occurring:" : "Відбувається:",
"Accept" : "Прийняти",
"Decline" : "Відхилити",
"More options …" : "Більше варіантів …",
"More options at %s" : "Більше варіантів на %s",
- "Contacts" : "Контакти",
- "Upgrade needed" : "Потрібно оновитися",
- "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Ваш %s мусить бути налаштований на використання HTTPS для надання доступу до CalDAV і CardDAV з iOS/macOS.",
- "Configures a CalDAV account" : "Налаштовує обліковий запис CalDAV",
- "Configures a CardDAV account" : "Налаштовує обліковий запис CardDAV",
- "Tasks" : "Завдання",
- "Untitled task" : "Завдання без назви",
- "WebDAV" : "WebDAV",
- "WebDAV endpoint" : "Точка доступу WebDAV",
- "to" : "до",
- "Monday" : "понеділок",
+ "Monday" : "Понеділок",
"Tuesday" : "Вівторок",
"Wednesday" : "Середа",
"Thursday" : "Четвер",
"Friday" : "П'ятниця",
"Saturday" : "Субота",
"Sunday" : "Неділя",
+ "January" : "Січень",
+ "February" : "Лютий",
+ "March" : "Березень",
+ "April" : "Квітень",
+ "May" : "Травень",
+ "June" : "Червень",
+ "July" : "Липень",
+ "August" : "Серпень",
+ "September" : "Вересень",
+ "October" : "Жовтень",
+ "November" : "Листопад",
+ "December" : "Грудень",
+ "First" : "Перший",
+ "Second" : "Другий",
+ "Third" : "Третій",
+ "Fourth" : "Четвертий",
+ "Fifth" : "П'ятий",
+ "Last" : "Останній",
+ "Second Last" : "Другий(-а) останній(-я)",
+ "Third Last" : "Третій(-я) останній(-я)",
+ "Fourth Last" : "Четвертий(-а) останній(-я)",
+ "Fifth Last" : "Останній п'ятий",
+ "Contacts" : "Контакти",
+ "{actor} created address book {addressbook}" : "{actor} створив(-ла) адресну книгу {addressbook}",
+ "You created address book {addressbook}" : "Ви створили адресну книгу {addressbook}",
+ "{actor} deleted address book {addressbook}" : "{actor} вилучив(-ла) адресну книгу {addressbook}",
+ "You deleted address book {addressbook}" : "Ви вилучили адресну книгу {addressbook}",
+ "{actor} updated address book {addressbook}" : "{actor} оновив(-ла) адресну книгу {addressbook}",
+ "You updated address book {addressbook}" : "Ви оновили адресну книгу {addressbook}",
+ "{actor} shared address book {addressbook} with you" : "{actor} поділив(-ла-)ся з вами адресною книгою {addressbook}",
+ "You shared address book {addressbook} with {user}" : "Ви надали доступ до адресної книги {addressbook} користувачу {user}",
+ "{actor} shared address book {addressbook} with {user}" : "{actor} поділив(-ла-)ся адресною книгою {addressbook} з {user}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} не надіслана адресна книга {addressbook} від вас",
+ "You unshared address book {addressbook} from {user}" : "Ви скасували спільний доступ до адресної книги {addressbook} від користувача {user}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} не має спільного доступу до адресної книги {addressbook} від {user}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} забрав доступ до адресної книги {addressbook} від себе",
+ "You shared address book {addressbook} with group {group}" : "Ви надали спільний доступ до адресної книги {addressbook} групі {group}",
+ "{actor} shared address book {addressbook} with group {group}" : "{actor} поділив(-ла-)ся адресною книгою {addressbook} із групою {group}",
+ "You unshared address book {addressbook} from group {group}" : "Ви скасували спільний доступ до адресної книги {addressbook} із групи {group}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} не має спільного доступу до адресної книги {addressbook} із групи {group}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} створив(-ла) контакт {card} в адресній книзі {addressbook}",
+ "You created contact {card} in address book {addressbook}" : "Ви створили контакт {card} в адресній книзі {addressbook}",
+ "{actor} deleted contact {card} from address book {addressbook}" : "{actor} вилучив(-ла) контакт {card} з адресної книги {addressbook}",
+ "You deleted contact {card} from address book {addressbook}" : "Ви вилучили контакт {card} з адресної книги {addressbook}",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} оновив(-ла) контакт {card} в адресній книзі {addressbook}",
+ "You updated contact {card} in address book {addressbook}" : "Ви оновили контакт {card} в адресній книзі {addressbook}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>Контактну</strong> або <strong>адресну книгу</strong> було змінено",
+ "Accounts" : "Облікові записи",
+ "System address book which holds all accounts" : "Системна адресна книга, в якій містяться всі облікові записи",
+ "File is not updatable: %1$s" : "Файл не оновлюється: %1$s",
+ "Failed to get storage for file" : "Не вдалося отримати дані про сховище для файлу",
+ "Could not write to final file, canceled by hook" : "Не вдалося записати в остаточний файл, скасовано перехопленням",
+ "Could not write file contents" : "Не вдалося записати вміст файлу",
+ "_%n byte_::_%n bytes_" : ["%n байт","%n байтів","%n байтів","%n байтів"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Помилка під час копіювання файлу в цільове розташування (скопійовано: %1$s, очікуваний розмір файлу: %2$s)",
+ "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." : "Очікуваний розмір файлу %1$s, але читання (з клієнта Nextcloud) і запис (до сховища Nextcloud) %2$s. Це може бути проблема мережі на стороні надсилання або проблема запису в сховище на стороні сервера.",
+ "Could not rename part file to final file, canceled by hook" : "Не вдалося перейменувати файл частини на остаточний файл, скасовано підхопленням",
+ "Could not rename part file to final file" : "Не вдалося перейменувати файл частини на остаточний файл",
+ "Failed to check file size: %1$s" : "Не вдалося перевірити розмір файлу: %1$s",
+ "Could not open file: %1$s, file does seem to exist" : "Неможливо відкрити файл: %1$s, хоча файл скоріше за все присутній",
+ "Could not open file: %1$s, file doesn't seem to exist" : "Неможливо відкрити файл: %1$s, скоріше за все файл відсутній",
+ "Encryption not ready: %1$s" : "Шифрування не готове: %1$s",
+ "Failed to open file: %1$s" : "Не вдалося відкрити файл: %1$s",
+ "Failed to unlink: %1$s" : "Не вдалося від’єднати: %1$s",
+ "Failed to write file contents: %1$s" : "Не вдалося записати вміст файлу: %1$s",
+ "File not found: %1$s" : "Файл не знайдено: %1$s",
+ "Invalid target path" : "Недійсний шлях призначення",
+ "System is in maintenance mode." : "Система в режимі обслуговування.",
+ "Upgrade needed" : "Потрібне оновлення",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Ваш %s мусить бути налаштований на використання HTTPS для надання доступу до CalDAV і CardDAV з iOS/macOS.",
+ "Configures a CalDAV account" : "Налаштовує обліковий запис CalDAV",
+ "Configures a CardDAV account" : "Налаштовує обліковий запис CardDAV",
+ "Events" : "Події",
+ "Untitled task" : "Завдання без назви",
+ "Completed on %s" : "Завершено %s",
+ "Due on %s by %s" : "До %s з боку %s",
+ "Due on %s" : "До %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Ласкаво просимо до календаря Nextcloud!\n\nЦе зразок події — ознайомтеся з гнучкістю планування за допомогою календаря Nextcloud, вносячи будь-які зміни, які вам заманеться!\n\nЗа допомогою календаря Nextcloud ви можете:\n- Легко створювати, редагувати та керувати подіями.\n- Створювати кілька календарів і ділитися ними з колегами, друзями або родиною.\n- Перевіряти доступність і показувати іншим, коли ви зайняті.\n- Безперешкодно інтегруватися з додатками та пристроями через CalDAV.\n- Налаштовувати свій досвід: планувати повторювані події, налаштовувати сповіщення та інші параметри.",
+ "Example event - open me!" : "Приклад події - відкрий мене!",
+ "System Address Book" : "Системна адресна книга",
+ "The system address book contains contact information for all users in your instance." : "Системна адресна книга містить контактну інформацію для всіх користувачів вашого примірника хмари.",
+ "Enable System Address Book" : "Увімкнути системну адресну книгу",
+ "DAV system address book" : "Системна адресна книга DAV",
+ "No outstanding DAV system address book sync." : "Немає незавершеної синхронізації системної адресної книги DAV.",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Синхронізація системної адресної книги DAV ще не запускалася, оскільки, або ваша система вже має понад 1000 користувачів, або сталася помилка. Будь ласка, запустіть синхронізацію вручну за допомогою команди \"occ dav:sync-system-addressbook\".",
+ "WebDAV endpoint" : "Точка доступу WebDAV",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Неможливо перевірити, чи на вашому вебсервері правильно налаштовано доступ для синхронізації файлів через протокол WebDAV. Перевірте це вручну.",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Ваш вебсервер не налаштований як треба для синхронізації файлів, схоже інтерфейс WebDAV поламаний.",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "На вашому вебсерверві правильно налаштовано доступ для синхронізації файлів через протокол WevDAV.",
+ "Migrated calendar (%1$s)" : "Перенесений календар (%1$s)",
+ "Calendars including events, details and attendees" : "Календарі, включно з подіями, деталями та відвідувачами",
+ "Contacts and groups" : "Контакти та групи",
+ "WebDAV" : "WebDAV",
+ "Absence saved" : "Відсутність збережено",
+ "Failed to save your absence settings" : "Не вдалося зберегти налаштуванння про відсутність",
+ "Absence cleared" : "Відсутність вилучено",
+ "Failed to clear your absence settings" : "Не вдалося скинути ваші налаштування про відсутність",
+ "First day" : "Перший день",
+ "Last day (inclusive)" : "Останній день (включно)",
+ "Out of office replacement (optional)" : "Заміна під час відсутності (необов'язково)",
+ "Name of the replacement" : "Ім'я заступника",
+ "No results." : "Відсутні результати.",
+ "Start typing." : "Почніть вводити.",
+ "Short absence status" : "Короткий статус відсутности",
+ "Long absence Message" : "Довге повідомлення про відсутність",
"Save" : "Зберегти",
- "Calendar server" : "Сервер календаря",
- "Send invitations to attendees" : "Надіслати запрошення учасникам",
- "Automatically generate a birthday calendar" : "Автоматично згенерувати календар днів народження",
+ "Disable absence" : "Вимкнути відсутність",
+ "Failed to load availability" : "Не вдалося завантажити доступність",
+ "Saved availability" : "Збережена наявність",
+ "Failed to save availability" : "Не вдалося зберегти наявність",
+ "Time zone:" : "Часовий пояс:",
+ "to" : "до",
+ "Delete slot" : "Вилучити діапазон",
+ "No working hours set" : "Робочий час не встановлено",
+ "Add slot" : "Додати діапазон",
+ "Weekdays" : "Дні тижня",
+ "Pick a start time for {dayName}" : "Виберіть час початку для {dayName}",
+ "Pick a end time for {dayName}" : "Виберіть час завершення для {dayName}",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Автоматично встановлювати статус користувача у \"Не турбувати\", коли ви не доступні. В цей проміжок часу ви не отримуватимете сповіщення.",
+ "Cancel" : "Скасувати",
+ "Import" : "Імпорт",
+ "Error while saving settings" : "Помилка під час збереження налаштувань",
+ "Contact reset successfully" : "Контакт успішно скинуто",
+ "Error while resetting contact" : "Помилка під час скидання контакту",
+ "Contact imported successfully" : "Контакт імпортовано успішно",
+ "Error while importing contact" : "Помилка під час імпортування контакту",
+ "Import contact" : "Імпортувати контакт",
+ "Reset to default" : "Скинути до типового",
+ "Import contacts" : "Імпортувати контакти",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "Імпорт нового файлу .vcf призведе до видалення наявного контакту за замовчуванням і заміни його новим. Чи хочете ви продовжити?",
+ "Failed to save example event creation setting" : "Не вдалося зберегти приклад налаштування створення події",
+ "Failed to upload the example event" : "Не вдалося завантажити приклад події",
+ "Custom example event was saved successfully" : "Приклад користувацької події було успішно збережено",
+ "Failed to delete the custom example event" : "Не вдалося видалити приклад користувацького події",
+ "Custom example event was deleted successfully" : "Приклад користувацької події було успішно видалено",
+ "Import calendar event" : "Імпортувати подію календаря",
+ "Uploading a new event will overwrite the existing one." : "Завантаження нового заходу призведе до заміни наявного.",
+ "Upload event" : "Завантажити подію",
+ "Availability" : "Доступність",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Будь ласка, налаштуйте ваші робочі години, щоб інші користувачі могли бачити, коли ви відсутні під час створення зустрічей.",
+ "Absence" : "Відсутність",
+ "Configure your next absence period." : "Налаштувати проміжки вашої відсутности",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Встановіть {calendarappstoreopen}застосунок Календар{linkclose}, {calendardocopen}з'єднайте ваш ноутбук та смартфон для синхронізації вашого календаря ↗{linkclose}.",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Будь-ласка переконайтеся у коректності налаштувань {emailopen}поштового сервера{linkclose}.",
+ "Calendar server" : "Календар",
+ "Send invitations to attendees" : "Надсилати запрошення учасникам",
+ "Automatically generate a birthday calendar" : "Автоматично створити календар днів народження",
"Birthday calendars will be generated by a background job." : "Календар днів народження буде згенеровано у фоновому завданні.",
"Hence they will not be available immediately after enabling but will show up after some time." : "Отже вони не будуть доступні одразу після увімкнення, але з'являться згодом.",
- "Send notifications for events" : "Відправити сповіщення для подій",
+ "Send notifications for events" : "Надсилати сповіщення про події",
"Notifications are sent via background jobs, so these must occur often enough." : "Сповіщення надсилаються у фонових завданнях, тож вони мають запускатися достатньо часто.",
+ "Send reminder notifications to calendar sharees as well" : "Також надсилайте нагадування користувачам вашого спільного календаря",
+ "Reminders are always sent to organizers and attendees." : "Нагадування завжди надсилаються організаторам і учасникам.",
"Enable notifications for events via push" : "Увімкнути push-сповіщення для подій",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Встановіть також {calendarappstoreopen}застосунок Календар{linkclose}, або {calendardocopen}з'єднайте ваш декстоп і мобільний для синхронізації ↗{linkclose}.",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Будь-ласка переконайтеся у коректності налаштувань {emailopen}поштового сервера{linkclose}.",
+ "Example content" : "Приклад вмісту",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Приклад вмісту служить для демонстрації функцій Nextcloud. Стандартний вміст постачається разом із Nextcloud і може бути замінений на власний вміст.",
"There was an error updating your attendance status." : "Виникла помилка при оновленні вашого статусу учасника.",
"Please contact the organizer directly." : "Будь-ласка повідомте організатора.",
"Are you accepting the invitation?" : "Чи приймаєте ви запрошення?",
"Tentative" : "Попередній",
- "Comment" : "Коментар",
"Your attendance was updated successfully." : "Ваша участь успішно оновлена."
},"pluralForm" :"nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);"
} \ No newline at end of file
diff --git a/apps/dav/l10n/zh_CN.js b/apps/dav/l10n/zh_CN.js
index 26a771f47c4..e126212636c 100644
--- a/apps/dav/l10n/zh_CN.js
+++ b/apps/dav/l10n/zh_CN.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "日历",
- "Todos" : "待办事项",
+ "Tasks" : "任务",
"Personal" : "个人",
"{actor} created calendar {calendar}" : "{actor} 创建了日历 {calendar}",
"You created calendar {calendar}" : "您创建的日历 {calendar}",
@@ -25,64 +25,176 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} 通过组 {group} 共享了日历 {calendar}",
"You unshared calendar {calendar} from group {group}" : "群组 {group} 取消了你共享的日历 {calendar}",
"{actor} unshared calendar {calendar} from group {group}" : "群组 {group} 取消了 {actor} 共享的日历 {calendar}",
+ "Untitled event" : "未命名事件",
"{actor} created event {event} in calendar {calendar}" : "{actor} 在日历 {calendar} 中创建了事件 {event}",
"You created event {event} in calendar {calendar}" : "您在日历 {calendar} 中创建了事件 {event}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} 在日历 {calendar} 中删除了事件 {event}",
"You deleted event {event} from calendar {calendar}" : "您在日历 {calendar} 中删除了事件 {event}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} 在日历 {calendar} 中更新了事件 {event}",
"You updated event {event} in calendar {calendar}" : "您在日历 {calendar} 中更新了事件 {event}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} 将事件 {event} 从日历 {sourceCalendar} 移动至日历 {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "您已将事件 {event} 从日历 {sourceCalendar} 移动至日历 {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} 还原了事件 {event},它位于日历 {calendar}",
"You restored event {event} of calendar {calendar}" : "你还原了事件 {event},它位于日历 {calendar}",
"Busy" : "忙碌",
- "{actor} created todo {todo} in list {calendar}" : "{actor} 在列表 {calendar} 中创建了待办事项 {todo}",
- "You created todo {todo} in list {calendar}" : "您在列表 {calendar} 中创建了待办事项 {todo}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} 在列表 {calendar} 中删除了待办事项 {todo}",
- "You deleted todo {todo} from list {calendar}" : "您在列表 {calendar} 中删除了待办事项 {todo}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} 在列表 {calendar} 中更新了待办事项 {todo}",
- "You updated todo {todo} in list {calendar}" : "您在列表 {calendar} 中更新了待办事项 {todo}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} 在列表 {calendar} 中解决了待办事项 {todo}",
- "You solved todo {todo} in list {calendar}" : "您在列表 {calendar} 中解决了待办事项 {todo}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} 在列表 {calendar} 中重新打开了待办事项 {todo}",
- "You reopened todo {todo} in list {calendar}" : "您在列表 {calendar} 中重新打开了待办事项 {todo}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} 在清单 {calendar} 中创建了待办事项 {todo}",
+ "You created to-do {todo} in list {calendar}" : "您已在清单 {calendar} 中创建了待办事项 {todo}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} 从列表 {calendar} 中删除了待办事项 {todo}",
+ "You deleted to-do {todo} from list {calendar}" : "你从列表 {calendar} 中删除了待办事项 {todo}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} 更新了列表 {calendar} 中的待办事项 {todo}",
+ "You updated to-do {todo} in list {calendar}" : "你更新了列表 {calendar} 中的待办事项 {todo}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} 解决了列表 {calendar} 中的待办事项 {todo}",
+ "You solved to-do {todo} in list {calendar}" : "你解决了列表 {calendar} 中的待办事项 {todo}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} 重新开启了列表 {calendar} 中的待办事项 {todo}",
+ "You reopened to-do {todo} in list {calendar}" : "你重新开启了列表 {calendar} 中的待办事项 {todo}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} 将待办事项 {todo} 从列表 {sourceCalendar} 移动到列表 {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "你将待办事项 {todo} 从列表 {sourceCalendar} 移动到列表 {targetCalendar}",
"Calendar, contacts and tasks" : "日历、联系人和任务",
"A <strong>calendar</strong> was modified" : "<strong>日历</strong>已经修改",
"A calendar <strong>event</strong> was modified" : "日历中<strong>事件</strong>已经修改",
- "A calendar <strong>todo</strong> was modified" : "列表中<strong>待办事项</strong>已经修改",
+ "A calendar <strong>to-do</strong> was modified" : "日历中的<strong>待办事项</strong>已修改",
"Contact birthdays" : "联系人生日",
"Death of %s" : "%s 的忌日",
+ "Untitled calendar" : "未命名的日历",
"Calendar:" : "日历:",
"Date:" : "日期:",
"Where:" : "地点:",
"Description:" : "描述:",
- "Untitled event" : "未命名事件",
"_%n year_::_%n years_" : ["%n 年"],
"_%n month_::_%n months_" : ["%n 月"],
"_%n day_::_%n days_" : ["%n 天"],
"_%n hour_::_%n hours_" : ["%n 小时"],
"_%n minute_::_%n minutes_" : ["%n 分钟"],
- "%s (in %s)" : "%s (在 %s)",
- "%s (%s ago)" : "%s (%s 前)",
+ "%s (in %s)" : "%s(在 %s)",
+ "%s (%s ago)" : "%s(%s 前)",
"Calendar: %s" : "日历:%s",
"Date: %s" : "日期:%s",
"Description: %s" : "描述:%s",
"Where: %s" : "地点:%s",
"%1$s via %2$s" : "%1$s 通过 %2$s",
+ "In the past on %1$s for the entire day" : "过去全天 %1$s ",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["在 %n 分钟后全天 %1$s"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["在 %n 小时后全天 %1$s"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["在 %n 天后全天 %1$s"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["在 %n 周后全天 %1$s"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["在 %n 个月后全天 %1$s"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["在 %n 年后全天 %1$s"],
+ "In the past on %1$s between %2$s - %3$s" : "过去 %2$s - %3$s %1$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["在 %2$s - %3$s %n 分钟后 %1$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["在 %2$s - %3$s %n 小时后 %1$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["在 %2$s - %3$s %n 天后 %1$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["在 %2$s - %3$s %n 周后 %1$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["在 %2$s - %3$s %n 个月后 %1$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["在 %2$s - %3$s %n 年后 %1$s"],
+ "Could not generate when statement" : "无法生成 when 语句",
+ "Every Day for the entire day" : "每天全天",
+ "Every Day for the entire day until %1$s" : "每天全天,直到 %1$s",
+ "Every Day between %1$s - %2$s" : "在 %1$s - %2$s 每天",
+ "Every Day between %1$s - %2$s until %3$s" : "在 %1$s - %2$s 每天,直到 %3$s",
+ "Every %1$d Days for the entire day" : "每 %1$d 天全天",
+ "Every %1$d Days for the entire day until %2$s" : "每 %1$d 天全天,直到 %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "在 %2$s - %3$s 每 %1$d 天",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "在 %2$s - %3$s 每 %1$d 天,直到 %4$s",
+ "Could not generate event recurrence statement" : "无法生成事件重复语句",
+ "Every Week on %1$s for the entire day" : "每周全天 %1$s",
+ "Every Week on %1$s for the entire day until %2$s" : "每周全天 %1$s,直到 %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "在 %2$s - %3$s 每周 %1$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "在 %2$s - %3$s 每周 %1$s,直到 %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "每 %1$d 周全天 %2$s",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "每 %1$d 周全天 %2$s,直到 %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "在 %3$s - %4$s 每 %1$d 周 %2$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "在 %3$s - %4$s 每 %1$d 周 %2$s,直到 %5$s",
+ "Every Month on the %1$s for the entire day" : "每个月全天 %1$s",
+ "Every Month on the %1$s for the entire day until %2$s" : "每个月全天 %1$s,直到 %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "在 %2$s - %3$s 每个月 %1$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "在 %2$s - %3$s 每个月 %1$s,直到 %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "每 %1$d 个月全天 %2$s",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "每 %1$d 个月全天 %2$s,直到 %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "在 %3$s - %4$s 每 %1$d 个月 %2$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "在 %3$s - %4$s 每 %1$d 个月 %2$s,直到 %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "每年 %1$s 全天 %2$s",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "每年 %1$s 全天 %2$s,直到 %3$s ",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "在 %3$s - %4$s 每年 %1$s %2$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "在 %3$s - %4$s 每年 %1$s %2$s,直到 %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "每 %1$d 年 %2$s 全天 %3$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "每 %1$d 年 %2$s 全天 %3$s,直到 %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "在 %4$s - %5$s 每 %1$d 年 %2$s %3$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "在 %4$s - %5$s 每 %1$d 年 %2$s %3$s,直到 %6$s",
+ "On specific dates for the entire day until %1$s" : "在特定日期全天,直到 %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "在 %1$s - %2$s 特定日期,直到 %3$s",
+ "In the past on %1$s" : "在过去 %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["%n 分钟后 %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["%n 小时后 %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["%n 天后 %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["%n 周后 %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["%n 个月后 %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["%n 年后 %1$s"],
+ "In the past on %1$s then on %2$s" : "过去 %1$s,然后 %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["%n 分钟后 %1$s,然后 %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["%n 小时后 %1$s,然后 %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["%n 天后 %1$s,然后 %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["%n 周后 %1$s,然后 %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["%n 个月后 %1$s,然后 %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["%n 年后 %1$s,然后 %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "在过去 %1$s,然后 %2$s 和 %3$s",
+ "_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_" : ["%n 分钟后 %1$s,然后 %2$s 和 %3$s"],
+ "_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_" : ["%n 小时后 %1$s,然后 %2$s 和 %3$s"],
+ "_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_" : ["%n 天后 %1$s,然后 %2$s 和 %3$s"],
+ "_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_" : ["%n 周后 %1$s,然后 %2$s 和 %3$s"],
+ "_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_" : ["%n 个月后 %1$s,然后 %2$s 和 %3$s"],
+ "_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_" : ["%n 年后 %1$s,然后 %2$s 和 %3$s"],
+ "Could not generate next recurrence statement" : "无法生成下一个重复语句",
"Cancelled: %1$s" : "已取消:%1$s",
- "Invitation canceled" : "邀请已取消",
+ "\"%1$s\" has been canceled" : "“%1$s”已取消",
"Re: %1$s" : "回复:%1$s",
- "Invitation updated" : "邀请已更新",
+ "%1$s has accepted your invitation" : "%1$s 已接受您的邀请",
+ "%1$s has tentatively accepted your invitation" : "%1$s 暂时接受了您的邀请",
+ "%1$s has declined your invitation" : "%1$s 已拒绝了您的邀请",
+ "%1$s has responded to your invitation" : "%1$s 已回应了您的邀请",
+ "Invitation updated: %1$s" : "邀请已更新:%1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s 已更新事件“%2$s”",
"Invitation: %1$s" : "邀请:%1$s",
- "Invitation" : "邀请",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s 想邀请您加入“%2$s”",
+ "Organizer:" : "组织者:",
+ "Attendees:" : "参加者:",
"Title:" : "标题:",
- "Time:" : "时间:",
+ "When:" : "时间:",
"Location:" : "地区:",
"Link:" : "链接:",
- "Organizer:" : "组织者:",
- "Attendees:" : "与会者:",
+ "Occurring:" : "发生:",
"Accept" : "接受",
"Decline" : "拒绝",
"More options …" : "更多选项",
"More options at %s" : "在%s的更多选项",
+ "Monday" : "周一",
+ "Tuesday" : "周二",
+ "Wednesday" : "周三",
+ "Thursday" : "周四",
+ "Friday" : "周五",
+ "Saturday" : "周六",
+ "Sunday" : "周日",
+ "January" : "一月",
+ "February" : "二月",
+ "March" : "三月",
+ "April" : "四月",
+ "May" : "五月",
+ "June" : "六月",
+ "July" : "七月",
+ "August" : "八月",
+ "September" : "九月",
+ "October" : "十月",
+ "November" : "十一月",
+ "December" : "十二月",
+ "First" : "第一",
+ "Second" : "第二",
+ "Third" : "第三",
+ "Fourth" : "第四",
+ "Fifth" : "第五",
+ "Last" : "最后一个",
+ "Second Last" : "倒数第二",
+ "Third Last" : "倒数第三",
+ "Fourth Last" : "倒数第四",
+ "Fifth Last" : "倒数第五",
"Contacts" : "联系人",
"{actor} created address book {addressbook}" : "{actor} 创建了通讯录 {addressbook}",
"You created address book {addressbook}" : "你创建了通讯录 {addressbook}",
@@ -108,52 +220,119 @@ OC.L10N.register(
"{actor} updated contact {card} in address book {addressbook}" : "{actor} 更新了通讯录 {addressbook} 中的联系人 {card} ",
"You updated contact {card} in address book {addressbook}" : "你更新了通讯录 {addressbook} 中的联系人 {card} ",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "一名 <strong>联系人</strong>或一个<strong>通讯录</strong>被更改了",
- "System is in maintenance mode." : "系统处于维护模式 ",
+ "Accounts" : "账号",
+ "System address book which holds all accounts" : "包含所有账号的系统通讯录",
+ "File is not updatable: %1$s" : "无法更新文件:%1$s",
+ "Failed to get storage for file" : "无法获取文件的存储空间",
+ "Could not write to final file, canceled by hook" : "无法写入最终文件,操作被插件取消",
+ "Could not write file contents" : "无法写入文件内容",
+ "_%n byte_::_%n bytes_" : ["%n字节"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "将文件复制到目标位置时发生错误(已复制:%1$s,预期大小:%2$s)",
+ "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." : "预期文件大小为 %1$s,实际从 Nextcloud 客户端读入并写入 Nextcloud 存储空间的大小为 %2$s。可能是发送端发生了网络问题,或者是服务器写入存储设备时发生错误。",
+ "Could not rename part file to final file, canceled by hook" : "无法将部分文件重命名为最终文件,操作被插件取消",
+ "Could not rename part file to final file" : "无法将部分文件重命名为最终文件",
+ "Failed to check file size: %1$s" : "检查文件大小失败:%1$s",
+ "Could not open file: %1$s, file does seem to exist" : "无法打开文件 %1$s,文件似乎不存在",
+ "Could not open file: %1$s, file doesn't seem to exist" : "无法打开文件 %1$s,文件似乎不存在",
+ "Encryption not ready: %1$s" : "加密不可用:%1$s",
+ "Failed to open file: %1$s" : "打开文件失败:%1$s",
+ "Failed to unlink: %1$s" : "解除链接失败:%1$s",
+ "Failed to write file contents: %1$s" : "写入文件内容失败:%1$s",
+ "File not found: %1$s" : "找不到文件:%1$s",
+ "Invalid target path" : "目标路径无效",
+ "System is in maintenance mode." : "系统处于维护模式。",
"Upgrade needed" : "需要升级",
- "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "您的%s 需要配置使用HTTPS以在iOS/macOS中使用CalDAV和CardDAV。",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "您的 %s 需要配置使用 HTTPS 以在 iOS/macOS 中使用 CalDAV 和 CardDAV。",
"Configures a CalDAV account" : "配置一个 CalDAV 账号",
"Configures a CardDAV account" : "配置一个 CardDAV 账号",
"Events" : "事件",
- "Tasks" : "任务",
"Untitled task" : "无标题任务",
"Completed on %s" : "已完成 %s",
- "Due on %s by %s" : "到期于%s ,在%s之前",
- "Due on %s" : "到期于%s",
+ "Due on %s by %s" : "到期于 %s,在 %s 之前",
+ "Due on %s" : "到期于 %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "欢迎使用 Nextcloud 日历!\n\n这是一个示例事件——探索使用 Nextcloud 日历进行规划的灵活性,进行任何您想要的编辑!\n\n使用 Nextcloud 日历,您可以:\n- 轻松创建、编辑和管理事件。\n- 创建多个日历并与队友、朋友或家人共享。\n- 查看空闲时间并向他人显示您的忙碌时间。\n- 通过 CalDAV 与应用和设备无缝集成。\n- 自定义您的体验:安排重复事件、调整通知和其他设置。",
+ "Example event - open me!" : "示例事件——打开我!",
+ "System Address Book" : "系统通讯录",
+ "The system address book contains contact information for all users in your instance." : "系统通讯录包含实例中所有用户的联系人信息。",
+ "Enable System Address Book" : "启用系统通讯录",
+ "DAV system address book" : "DAV 系统通讯录",
+ "No outstanding DAV system address book sync." : "书书未完成 DAV 系统通讯录的同步",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV 系统通讯录同步失败,这可能是因为你的服务器实例有超过1000名用户,或是因为出现了错误。请手动运行指令 “occ dav:sync-system-addressbook” 继续该操作。",
+ "WebDAV endpoint" : "WebDAV 端点",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "无法检查您的 Web 服务器是否已正确设置以允许通过 WebDAV 进行文件同步。 请手动检查。",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "您的网页服务器没有正确设置允许文件同步,因为 WebDAV 接口看起来无法正常工作。",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "您的 Web 服务器已正确设置为允许通过 WebDAV 进行文件同步。",
+ "Migrated calendar (%1$s)" : "迁移的日历(%1$s)",
+ "Calendars including events, details and attendees" : "日历,包括事件、详情和参加者",
"Contacts and groups" : "联系人和群组",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV端点",
- "Availability" : "可用性",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "如果您配置了工作时间,其他用户在预订会议时就会了解您何时不在办公室。",
+ "Absence saved" : "缺席已保存",
+ "Failed to save your absence settings" : "未能保存您的离开状态设置",
+ "Absence cleared" : "缺席已清除",
+ "Failed to clear your absence settings" : "未能清除您的离开状态设置",
+ "First day" : "第一天",
+ "Last day (inclusive)" : "最后一天(含)",
+ "Out of office replacement (optional)" : "外出时工作替代人员(可选)",
+ "Name of the replacement" : "替代人员名称",
+ "No results." : "没有结果。",
+ "Start typing." : "开始输入",
+ "Short absence status" : "短暂离开状态",
+ "Long absence Message" : "长期离开信息",
+ "Save" : "保存",
+ "Disable absence" : "禁用离开状态",
+ "Failed to load availability" : "可用时间段加载失败",
+ "Saved availability" : "可用时间已保存",
+ "Failed to save availability" : "可用时间保存失败",
"Time zone:" : "时区:",
"to" : "到",
"Delete slot" : "删除插槽",
"No working hours set" : "尚未设置工作时间",
"Add slot" : "添加插槽",
- "Monday" : "周一",
- "Tuesday" : "周二",
- "Wednesday" : "周三",
- "Thursday" : "周四",
- "Friday" : "周五",
- "Saturday" : "周六",
- "Sunday" : "周日",
- "Save" : "保存",
+ "Weekdays" : "工作日",
+ "Pick a start time for {dayName}" : "选择 {dayName} 的开始时间",
+ "Pick a end time for {dayName}" : "选择 {dayName} 的结束时间",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "不在工作时间内时,自动将用户状态设置为“勿扰”并静音所有通知。",
+ "Cancel" : "取消",
+ "Import" : "导入",
+ "Error while saving settings" : "保存设置时出错",
+ "Contact reset successfully" : "联系人重置成功",
+ "Error while resetting contact" : "重置联系人时出错",
+ "Contact imported successfully" : "联系人导入成功",
+ "Error while importing contact" : "导入联系人时出错",
+ "Import contact" : "导入联系人",
+ "Reset to default" : "重置为默认设置",
+ "Import contacts" : "导入联系人",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "导入新的 .vcf 文件将删除现有的默认联系人,并用新联系人替换。是否要继续?",
+ "Failed to save example event creation setting" : "无法保存示例事件创建设置",
+ "Failed to upload the example event" : "无法上传示例事件",
+ "Custom example event was saved successfully" : "已成功保存自定义示例事件",
+ "Failed to delete the custom example event" : "无法删除自定义示例事件",
+ "Custom example event was deleted successfully" : "已成功删除自定义示例事件",
+ "Import calendar event" : "导入日历事件",
+ "Uploading a new event will overwrite the existing one." : "上传新事件将覆盖现有事件。",
+ "Upload event" : "上传事件",
+ "Availability" : "工作时间",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "如果你配置了工作时间,其他人在预订会议时会看到你何时不在办公室。",
+ "Absence" : "离开",
+ "Configure your next absence period." : "配置你的下一次离开时段",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "也安装{calendarappstoreopen}日历应用{linkclose},或者{calendardocopen}连接您的桌面和移动端同步日历 ↗{linkclose}。",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "请确保正确设置{emailopen}邮件服务器{linkclose}。",
"Calendar server" : "日历服务器",
- "Send invitations to attendees" : "向与会者发送邀请",
+ "Send invitations to attendees" : "向参加者发送邀请",
"Automatically generate a birthday calendar" : "自动生成生日日历",
"Birthday calendars will be generated by a background job." : "生日日历将由后台作业生成。",
"Hence they will not be available immediately after enabling but will show up after some time." : "因此,它们在启用后不会立即可用,但会在一段时间后显示出来。",
"Send notifications for events" : "发送事件通知",
"Notifications are sent via background jobs, so these must occur often enough." : "通知将通过后台任务发送,所以任务的频率要足够高。",
+ "Send reminder notifications to calendar sharees as well" : "同时向日历共享者发送提醒通知",
+ "Reminders are always sent to organizers and attendees." : "始终向组织者和参加者发送提醒。",
"Enable notifications for events via push" : "启用推送事件通知",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "也安装 {calendarappstoreopen}日历应用{linkclose},或者 {calendardocopen}连接您的桌面和移动端同步日历 ↗{linkclose}。",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "请确保正确设置 {emailopen}邮件服务器{linkclose}。",
+ "Example content" : "示例内容",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "示例内容用于展示 Nextcloud 的功能。默认内容随 Nextcloud 一起提供,可以用自定义内容替换。",
"There was an error updating your attendance status." : "更新您的出席状态时出错。",
"Please contact the organizer directly." : "请直接联系组织者。",
"Are you accepting the invitation?" : "您是否接受邀请?",
"Tentative" : "暂定",
- "Number of guests" : "客人数目",
- "Comment" : "备注",
- "Your attendance was updated successfully." : "您的出席状态更新成功。",
- "Calendar and tasks" : "日历和任务"
+ "Your attendance was updated successfully." : "您的出席状态更新成功。"
},
"nplurals=1; plural=0;");
diff --git a/apps/dav/l10n/zh_CN.json b/apps/dav/l10n/zh_CN.json
index 6ebc5361977..03ca61d40ab 100644
--- a/apps/dav/l10n/zh_CN.json
+++ b/apps/dav/l10n/zh_CN.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "日历",
- "Todos" : "待办事项",
+ "Tasks" : "任务",
"Personal" : "个人",
"{actor} created calendar {calendar}" : "{actor} 创建了日历 {calendar}",
"You created calendar {calendar}" : "您创建的日历 {calendar}",
@@ -23,64 +23,176 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} 通过组 {group} 共享了日历 {calendar}",
"You unshared calendar {calendar} from group {group}" : "群组 {group} 取消了你共享的日历 {calendar}",
"{actor} unshared calendar {calendar} from group {group}" : "群组 {group} 取消了 {actor} 共享的日历 {calendar}",
+ "Untitled event" : "未命名事件",
"{actor} created event {event} in calendar {calendar}" : "{actor} 在日历 {calendar} 中创建了事件 {event}",
"You created event {event} in calendar {calendar}" : "您在日历 {calendar} 中创建了事件 {event}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} 在日历 {calendar} 中删除了事件 {event}",
"You deleted event {event} from calendar {calendar}" : "您在日历 {calendar} 中删除了事件 {event}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} 在日历 {calendar} 中更新了事件 {event}",
"You updated event {event} in calendar {calendar}" : "您在日历 {calendar} 中更新了事件 {event}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} 将事件 {event} 从日历 {sourceCalendar} 移动至日历 {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "您已将事件 {event} 从日历 {sourceCalendar} 移动至日历 {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} 还原了事件 {event},它位于日历 {calendar}",
"You restored event {event} of calendar {calendar}" : "你还原了事件 {event},它位于日历 {calendar}",
"Busy" : "忙碌",
- "{actor} created todo {todo} in list {calendar}" : "{actor} 在列表 {calendar} 中创建了待办事项 {todo}",
- "You created todo {todo} in list {calendar}" : "您在列表 {calendar} 中创建了待办事项 {todo}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} 在列表 {calendar} 中删除了待办事项 {todo}",
- "You deleted todo {todo} from list {calendar}" : "您在列表 {calendar} 中删除了待办事项 {todo}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} 在列表 {calendar} 中更新了待办事项 {todo}",
- "You updated todo {todo} in list {calendar}" : "您在列表 {calendar} 中更新了待办事项 {todo}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} 在列表 {calendar} 中解决了待办事项 {todo}",
- "You solved todo {todo} in list {calendar}" : "您在列表 {calendar} 中解决了待办事项 {todo}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} 在列表 {calendar} 中重新打开了待办事项 {todo}",
- "You reopened todo {todo} in list {calendar}" : "您在列表 {calendar} 中重新打开了待办事项 {todo}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} 在清单 {calendar} 中创建了待办事项 {todo}",
+ "You created to-do {todo} in list {calendar}" : "您已在清单 {calendar} 中创建了待办事项 {todo}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} 从列表 {calendar} 中删除了待办事项 {todo}",
+ "You deleted to-do {todo} from list {calendar}" : "你从列表 {calendar} 中删除了待办事项 {todo}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} 更新了列表 {calendar} 中的待办事项 {todo}",
+ "You updated to-do {todo} in list {calendar}" : "你更新了列表 {calendar} 中的待办事项 {todo}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} 解决了列表 {calendar} 中的待办事项 {todo}",
+ "You solved to-do {todo} in list {calendar}" : "你解决了列表 {calendar} 中的待办事项 {todo}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} 重新开启了列表 {calendar} 中的待办事项 {todo}",
+ "You reopened to-do {todo} in list {calendar}" : "你重新开启了列表 {calendar} 中的待办事项 {todo}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} 将待办事项 {todo} 从列表 {sourceCalendar} 移动到列表 {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "你将待办事项 {todo} 从列表 {sourceCalendar} 移动到列表 {targetCalendar}",
"Calendar, contacts and tasks" : "日历、联系人和任务",
"A <strong>calendar</strong> was modified" : "<strong>日历</strong>已经修改",
"A calendar <strong>event</strong> was modified" : "日历中<strong>事件</strong>已经修改",
- "A calendar <strong>todo</strong> was modified" : "列表中<strong>待办事项</strong>已经修改",
+ "A calendar <strong>to-do</strong> was modified" : "日历中的<strong>待办事项</strong>已修改",
"Contact birthdays" : "联系人生日",
"Death of %s" : "%s 的忌日",
+ "Untitled calendar" : "未命名的日历",
"Calendar:" : "日历:",
"Date:" : "日期:",
"Where:" : "地点:",
"Description:" : "描述:",
- "Untitled event" : "未命名事件",
"_%n year_::_%n years_" : ["%n 年"],
"_%n month_::_%n months_" : ["%n 月"],
"_%n day_::_%n days_" : ["%n 天"],
"_%n hour_::_%n hours_" : ["%n 小时"],
"_%n minute_::_%n minutes_" : ["%n 分钟"],
- "%s (in %s)" : "%s (在 %s)",
- "%s (%s ago)" : "%s (%s 前)",
+ "%s (in %s)" : "%s(在 %s)",
+ "%s (%s ago)" : "%s(%s 前)",
"Calendar: %s" : "日历:%s",
"Date: %s" : "日期:%s",
"Description: %s" : "描述:%s",
"Where: %s" : "地点:%s",
"%1$s via %2$s" : "%1$s 通过 %2$s",
+ "In the past on %1$s for the entire day" : "过去全天 %1$s ",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["在 %n 分钟后全天 %1$s"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["在 %n 小时后全天 %1$s"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["在 %n 天后全天 %1$s"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["在 %n 周后全天 %1$s"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["在 %n 个月后全天 %1$s"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["在 %n 年后全天 %1$s"],
+ "In the past on %1$s between %2$s - %3$s" : "过去 %2$s - %3$s %1$s",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["在 %2$s - %3$s %n 分钟后 %1$s"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["在 %2$s - %3$s %n 小时后 %1$s"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["在 %2$s - %3$s %n 天后 %1$s"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["在 %2$s - %3$s %n 周后 %1$s"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["在 %2$s - %3$s %n 个月后 %1$s"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["在 %2$s - %3$s %n 年后 %1$s"],
+ "Could not generate when statement" : "无法生成 when 语句",
+ "Every Day for the entire day" : "每天全天",
+ "Every Day for the entire day until %1$s" : "每天全天,直到 %1$s",
+ "Every Day between %1$s - %2$s" : "在 %1$s - %2$s 每天",
+ "Every Day between %1$s - %2$s until %3$s" : "在 %1$s - %2$s 每天,直到 %3$s",
+ "Every %1$d Days for the entire day" : "每 %1$d 天全天",
+ "Every %1$d Days for the entire day until %2$s" : "每 %1$d 天全天,直到 %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "在 %2$s - %3$s 每 %1$d 天",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "在 %2$s - %3$s 每 %1$d 天,直到 %4$s",
+ "Could not generate event recurrence statement" : "无法生成事件重复语句",
+ "Every Week on %1$s for the entire day" : "每周全天 %1$s",
+ "Every Week on %1$s for the entire day until %2$s" : "每周全天 %1$s,直到 %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "在 %2$s - %3$s 每周 %1$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "在 %2$s - %3$s 每周 %1$s,直到 %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "每 %1$d 周全天 %2$s",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "每 %1$d 周全天 %2$s,直到 %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "在 %3$s - %4$s 每 %1$d 周 %2$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "在 %3$s - %4$s 每 %1$d 周 %2$s,直到 %5$s",
+ "Every Month on the %1$s for the entire day" : "每个月全天 %1$s",
+ "Every Month on the %1$s for the entire day until %2$s" : "每个月全天 %1$s,直到 %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "在 %2$s - %3$s 每个月 %1$s",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "在 %2$s - %3$s 每个月 %1$s,直到 %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "每 %1$d 个月全天 %2$s",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "每 %1$d 个月全天 %2$s,直到 %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "在 %3$s - %4$s 每 %1$d 个月 %2$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "在 %3$s - %4$s 每 %1$d 个月 %2$s,直到 %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "每年 %1$s 全天 %2$s",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "每年 %1$s 全天 %2$s,直到 %3$s ",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "在 %3$s - %4$s 每年 %1$s %2$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "在 %3$s - %4$s 每年 %1$s %2$s,直到 %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "每 %1$d 年 %2$s 全天 %3$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "每 %1$d 年 %2$s 全天 %3$s,直到 %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "在 %4$s - %5$s 每 %1$d 年 %2$s %3$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "在 %4$s - %5$s 每 %1$d 年 %2$s %3$s,直到 %6$s",
+ "On specific dates for the entire day until %1$s" : "在特定日期全天,直到 %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "在 %1$s - %2$s 特定日期,直到 %3$s",
+ "In the past on %1$s" : "在过去 %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["%n 分钟后 %1$s"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["%n 小时后 %1$s"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["%n 天后 %1$s"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["%n 周后 %1$s"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["%n 个月后 %1$s"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["%n 年后 %1$s"],
+ "In the past on %1$s then on %2$s" : "过去 %1$s,然后 %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["%n 分钟后 %1$s,然后 %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["%n 小时后 %1$s,然后 %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["%n 天后 %1$s,然后 %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["%n 周后 %1$s,然后 %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["%n 个月后 %1$s,然后 %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["%n 年后 %1$s,然后 %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "在过去 %1$s,然后 %2$s 和 %3$s",
+ "_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_" : ["%n 分钟后 %1$s,然后 %2$s 和 %3$s"],
+ "_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_" : ["%n 小时后 %1$s,然后 %2$s 和 %3$s"],
+ "_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_" : ["%n 天后 %1$s,然后 %2$s 和 %3$s"],
+ "_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_" : ["%n 周后 %1$s,然后 %2$s 和 %3$s"],
+ "_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_" : ["%n 个月后 %1$s,然后 %2$s 和 %3$s"],
+ "_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_" : ["%n 年后 %1$s,然后 %2$s 和 %3$s"],
+ "Could not generate next recurrence statement" : "无法生成下一个重复语句",
"Cancelled: %1$s" : "已取消:%1$s",
- "Invitation canceled" : "邀请已取消",
+ "\"%1$s\" has been canceled" : "“%1$s”已取消",
"Re: %1$s" : "回复:%1$s",
- "Invitation updated" : "邀请已更新",
+ "%1$s has accepted your invitation" : "%1$s 已接受您的邀请",
+ "%1$s has tentatively accepted your invitation" : "%1$s 暂时接受了您的邀请",
+ "%1$s has declined your invitation" : "%1$s 已拒绝了您的邀请",
+ "%1$s has responded to your invitation" : "%1$s 已回应了您的邀请",
+ "Invitation updated: %1$s" : "邀请已更新:%1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s 已更新事件“%2$s”",
"Invitation: %1$s" : "邀请:%1$s",
- "Invitation" : "邀请",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s 想邀请您加入“%2$s”",
+ "Organizer:" : "组织者:",
+ "Attendees:" : "参加者:",
"Title:" : "标题:",
- "Time:" : "时间:",
+ "When:" : "时间:",
"Location:" : "地区:",
"Link:" : "链接:",
- "Organizer:" : "组织者:",
- "Attendees:" : "与会者:",
+ "Occurring:" : "发生:",
"Accept" : "接受",
"Decline" : "拒绝",
"More options …" : "更多选项",
"More options at %s" : "在%s的更多选项",
+ "Monday" : "周一",
+ "Tuesday" : "周二",
+ "Wednesday" : "周三",
+ "Thursday" : "周四",
+ "Friday" : "周五",
+ "Saturday" : "周六",
+ "Sunday" : "周日",
+ "January" : "一月",
+ "February" : "二月",
+ "March" : "三月",
+ "April" : "四月",
+ "May" : "五月",
+ "June" : "六月",
+ "July" : "七月",
+ "August" : "八月",
+ "September" : "九月",
+ "October" : "十月",
+ "November" : "十一月",
+ "December" : "十二月",
+ "First" : "第一",
+ "Second" : "第二",
+ "Third" : "第三",
+ "Fourth" : "第四",
+ "Fifth" : "第五",
+ "Last" : "最后一个",
+ "Second Last" : "倒数第二",
+ "Third Last" : "倒数第三",
+ "Fourth Last" : "倒数第四",
+ "Fifth Last" : "倒数第五",
"Contacts" : "联系人",
"{actor} created address book {addressbook}" : "{actor} 创建了通讯录 {addressbook}",
"You created address book {addressbook}" : "你创建了通讯录 {addressbook}",
@@ -106,52 +218,119 @@
"{actor} updated contact {card} in address book {addressbook}" : "{actor} 更新了通讯录 {addressbook} 中的联系人 {card} ",
"You updated contact {card} in address book {addressbook}" : "你更新了通讯录 {addressbook} 中的联系人 {card} ",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "一名 <strong>联系人</strong>或一个<strong>通讯录</strong>被更改了",
- "System is in maintenance mode." : "系统处于维护模式 ",
+ "Accounts" : "账号",
+ "System address book which holds all accounts" : "包含所有账号的系统通讯录",
+ "File is not updatable: %1$s" : "无法更新文件:%1$s",
+ "Failed to get storage for file" : "无法获取文件的存储空间",
+ "Could not write to final file, canceled by hook" : "无法写入最终文件,操作被插件取消",
+ "Could not write file contents" : "无法写入文件内容",
+ "_%n byte_::_%n bytes_" : ["%n字节"],
+ "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "将文件复制到目标位置时发生错误(已复制:%1$s,预期大小:%2$s)",
+ "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." : "预期文件大小为 %1$s,实际从 Nextcloud 客户端读入并写入 Nextcloud 存储空间的大小为 %2$s。可能是发送端发生了网络问题,或者是服务器写入存储设备时发生错误。",
+ "Could not rename part file to final file, canceled by hook" : "无法将部分文件重命名为最终文件,操作被插件取消",
+ "Could not rename part file to final file" : "无法将部分文件重命名为最终文件",
+ "Failed to check file size: %1$s" : "检查文件大小失败:%1$s",
+ "Could not open file: %1$s, file does seem to exist" : "无法打开文件 %1$s,文件似乎不存在",
+ "Could not open file: %1$s, file doesn't seem to exist" : "无法打开文件 %1$s,文件似乎不存在",
+ "Encryption not ready: %1$s" : "加密不可用:%1$s",
+ "Failed to open file: %1$s" : "打开文件失败:%1$s",
+ "Failed to unlink: %1$s" : "解除链接失败:%1$s",
+ "Failed to write file contents: %1$s" : "写入文件内容失败:%1$s",
+ "File not found: %1$s" : "找不到文件:%1$s",
+ "Invalid target path" : "目标路径无效",
+ "System is in maintenance mode." : "系统处于维护模式。",
"Upgrade needed" : "需要升级",
- "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "您的%s 需要配置使用HTTPS以在iOS/macOS中使用CalDAV和CardDAV。",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "您的 %s 需要配置使用 HTTPS 以在 iOS/macOS 中使用 CalDAV 和 CardDAV。",
"Configures a CalDAV account" : "配置一个 CalDAV 账号",
"Configures a CardDAV account" : "配置一个 CardDAV 账号",
"Events" : "事件",
- "Tasks" : "任务",
"Untitled task" : "无标题任务",
"Completed on %s" : "已完成 %s",
- "Due on %s by %s" : "到期于%s ,在%s之前",
- "Due on %s" : "到期于%s",
+ "Due on %s by %s" : "到期于 %s,在 %s 之前",
+ "Due on %s" : "到期于 %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "欢迎使用 Nextcloud 日历!\n\n这是一个示例事件——探索使用 Nextcloud 日历进行规划的灵活性,进行任何您想要的编辑!\n\n使用 Nextcloud 日历,您可以:\n- 轻松创建、编辑和管理事件。\n- 创建多个日历并与队友、朋友或家人共享。\n- 查看空闲时间并向他人显示您的忙碌时间。\n- 通过 CalDAV 与应用和设备无缝集成。\n- 自定义您的体验:安排重复事件、调整通知和其他设置。",
+ "Example event - open me!" : "示例事件——打开我!",
+ "System Address Book" : "系统通讯录",
+ "The system address book contains contact information for all users in your instance." : "系统通讯录包含实例中所有用户的联系人信息。",
+ "Enable System Address Book" : "启用系统通讯录",
+ "DAV system address book" : "DAV 系统通讯录",
+ "No outstanding DAV system address book sync." : "书书未完成 DAV 系统通讯录的同步",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV 系统通讯录同步失败,这可能是因为你的服务器实例有超过1000名用户,或是因为出现了错误。请手动运行指令 “occ dav:sync-system-addressbook” 继续该操作。",
+ "WebDAV endpoint" : "WebDAV 端点",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "无法检查您的 Web 服务器是否已正确设置以允许通过 WebDAV 进行文件同步。 请手动检查。",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "您的网页服务器没有正确设置允许文件同步,因为 WebDAV 接口看起来无法正常工作。",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "您的 Web 服务器已正确设置为允许通过 WebDAV 进行文件同步。",
+ "Migrated calendar (%1$s)" : "迁移的日历(%1$s)",
+ "Calendars including events, details and attendees" : "日历,包括事件、详情和参加者",
"Contacts and groups" : "联系人和群组",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV端点",
- "Availability" : "可用性",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "如果您配置了工作时间,其他用户在预订会议时就会了解您何时不在办公室。",
+ "Absence saved" : "缺席已保存",
+ "Failed to save your absence settings" : "未能保存您的离开状态设置",
+ "Absence cleared" : "缺席已清除",
+ "Failed to clear your absence settings" : "未能清除您的离开状态设置",
+ "First day" : "第一天",
+ "Last day (inclusive)" : "最后一天(含)",
+ "Out of office replacement (optional)" : "外出时工作替代人员(可选)",
+ "Name of the replacement" : "替代人员名称",
+ "No results." : "没有结果。",
+ "Start typing." : "开始输入",
+ "Short absence status" : "短暂离开状态",
+ "Long absence Message" : "长期离开信息",
+ "Save" : "保存",
+ "Disable absence" : "禁用离开状态",
+ "Failed to load availability" : "可用时间段加载失败",
+ "Saved availability" : "可用时间已保存",
+ "Failed to save availability" : "可用时间保存失败",
"Time zone:" : "时区:",
"to" : "到",
"Delete slot" : "删除插槽",
"No working hours set" : "尚未设置工作时间",
"Add slot" : "添加插槽",
- "Monday" : "周一",
- "Tuesday" : "周二",
- "Wednesday" : "周三",
- "Thursday" : "周四",
- "Friday" : "周五",
- "Saturday" : "周六",
- "Sunday" : "周日",
- "Save" : "保存",
+ "Weekdays" : "工作日",
+ "Pick a start time for {dayName}" : "选择 {dayName} 的开始时间",
+ "Pick a end time for {dayName}" : "选择 {dayName} 的结束时间",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "不在工作时间内时,自动将用户状态设置为“勿扰”并静音所有通知。",
+ "Cancel" : "取消",
+ "Import" : "导入",
+ "Error while saving settings" : "保存设置时出错",
+ "Contact reset successfully" : "联系人重置成功",
+ "Error while resetting contact" : "重置联系人时出错",
+ "Contact imported successfully" : "联系人导入成功",
+ "Error while importing contact" : "导入联系人时出错",
+ "Import contact" : "导入联系人",
+ "Reset to default" : "重置为默认设置",
+ "Import contacts" : "导入联系人",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "导入新的 .vcf 文件将删除现有的默认联系人,并用新联系人替换。是否要继续?",
+ "Failed to save example event creation setting" : "无法保存示例事件创建设置",
+ "Failed to upload the example event" : "无法上传示例事件",
+ "Custom example event was saved successfully" : "已成功保存自定义示例事件",
+ "Failed to delete the custom example event" : "无法删除自定义示例事件",
+ "Custom example event was deleted successfully" : "已成功删除自定义示例事件",
+ "Import calendar event" : "导入日历事件",
+ "Uploading a new event will overwrite the existing one." : "上传新事件将覆盖现有事件。",
+ "Upload event" : "上传事件",
+ "Availability" : "工作时间",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "如果你配置了工作时间,其他人在预订会议时会看到你何时不在办公室。",
+ "Absence" : "离开",
+ "Configure your next absence period." : "配置你的下一次离开时段",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "也安装{calendarappstoreopen}日历应用{linkclose},或者{calendardocopen}连接您的桌面和移动端同步日历 ↗{linkclose}。",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "请确保正确设置{emailopen}邮件服务器{linkclose}。",
"Calendar server" : "日历服务器",
- "Send invitations to attendees" : "向与会者发送邀请",
+ "Send invitations to attendees" : "向参加者发送邀请",
"Automatically generate a birthday calendar" : "自动生成生日日历",
"Birthday calendars will be generated by a background job." : "生日日历将由后台作业生成。",
"Hence they will not be available immediately after enabling but will show up after some time." : "因此,它们在启用后不会立即可用,但会在一段时间后显示出来。",
"Send notifications for events" : "发送事件通知",
"Notifications are sent via background jobs, so these must occur often enough." : "通知将通过后台任务发送,所以任务的频率要足够高。",
+ "Send reminder notifications to calendar sharees as well" : "同时向日历共享者发送提醒通知",
+ "Reminders are always sent to organizers and attendees." : "始终向组织者和参加者发送提醒。",
"Enable notifications for events via push" : "启用推送事件通知",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "也安装 {calendarappstoreopen}日历应用{linkclose},或者 {calendardocopen}连接您的桌面和移动端同步日历 ↗{linkclose}。",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "请确保正确设置 {emailopen}邮件服务器{linkclose}。",
+ "Example content" : "示例内容",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "示例内容用于展示 Nextcloud 的功能。默认内容随 Nextcloud 一起提供,可以用自定义内容替换。",
"There was an error updating your attendance status." : "更新您的出席状态时出错。",
"Please contact the organizer directly." : "请直接联系组织者。",
"Are you accepting the invitation?" : "您是否接受邀请?",
"Tentative" : "暂定",
- "Number of guests" : "客人数目",
- "Comment" : "备注",
- "Your attendance was updated successfully." : "您的出席状态更新成功。",
- "Calendar and tasks" : "日历和任务"
+ "Your attendance was updated successfully." : "您的出席状态更新成功。"
},"pluralForm" :"nplurals=1; plural=0;"
} \ No newline at end of file
diff --git a/apps/dav/l10n/zh_HK.js b/apps/dav/l10n/zh_HK.js
index 1c4723c39bd..2f93aacd742 100644
--- a/apps/dav/l10n/zh_HK.js
+++ b/apps/dav/l10n/zh_HK.js
@@ -2,7 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "日曆",
- "Todos" : "待辦事項",
+ "Tasks" : "任務",
"Personal" : "個人",
"{actor} created calendar {calendar}" : "{actor} 建立了日曆 {calendar}",
"You created calendar {calendar}" : "您建立了日曆 {calendar}",
@@ -25,36 +25,41 @@ OC.L10N.register(
"{actor} shared calendar {calendar} with group {group}" : "{actor} 與群組 {group} 分享了日曆 {calendar}",
"You unshared calendar {calendar} from group {group}" : "您已停止與群組 {group} 分享日曆 {calendar}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} 從 {group} 群組中取消了分享日曆 {calendar}",
+ "Untitled event" : "無標題活動",
"{actor} created event {event} in calendar {calendar}" : "{actor} 新增了日曆 {calendar} 中的活動 {event}",
"You created event {event} in calendar {calendar}" : "您新增了日曆 {calendar} 中的活動 {event}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} 從日曆 {calendar} 中刪除了活動 {event}",
"You deleted event {event} from calendar {calendar}" : "您從日曆 {calendar} 中刪除了活動 {event}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} 更新了日曆 {calendar} 中的活動 {event}",
"You updated event {event} in calendar {calendar}" : "您更新了日曆 {calendar} 中的活動 {event}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} 將活動 {event} 從日曆 {sourceCalendar} 移到日曆 {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "您將活動 {event} 從日曆 {sourceCalendar} 移到日曆 {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} 復原了日曆 {calendar} 的活動 {event}",
"You restored event {event} of calendar {calendar}" : "您復原了日曆 {calendar} 的活動 {event}",
"Busy" : "忙碌中",
- "{actor} created todo {todo} in list {calendar}" : "{actor} 在任務列表 {calendar} 中建立了待辦事項 {todo}",
- "You created todo {todo} in list {calendar}" : "您在任務列表 {calendar} 中建立了待辦事項 {todo}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} 從任務列表 {calendar} 中刪除了待辦事項 {todo}",
- "You deleted todo {todo} from list {calendar}" : "您從任務列表 {calendar} 中刪除了待辦事項 {todo}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} 從任務列表 {calendar} 中更新了待辦事項 {todo}",
- "You updated todo {todo} in list {calendar}" : "您在任務列表 {calendar} 中更新了待辦事項 {todo}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} 解决了任務列表 {calendar} 中的代辦事項 {todo}",
- "You solved todo {todo} in list {calendar}" : "您解决了任務列表 {calendar} 中的代辦事項 {todo}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} 重新開啟了任務列表 {calendar} 中的代辦事項 {todo}",
- "You reopened todo {todo} in list {calendar}" : "你重新開啟了 {calendar} 清單中的代辦事項 {todo}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} 在任務清單 {calendar} 中建立了待辦事項 {todo}",
+ "You created to-do {todo} in list {calendar}" : "您在任務清單 {calendar} 中建立了待辦事項 {todo}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} 從任務清單 {calendar} 中刪除了待辦事項 {todo}",
+ "You deleted to-do {todo} from list {calendar}" : "您從任務清單 {calendar} 中刪除了待辦事項 {todo}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} 從任務清單 {calendar} 中更新了待辦事項 {todo}",
+ "You updated to-do {todo} in list {calendar}" : "您在任務清單 {calendar} 中更新了待辦事項 {todo}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} 解决了任務清單 {calendar} 中的代辦事項 {todo}",
+ "You solved to-do {todo} in list {calendar}" : "您解决了任務清單 {calendar} 中的代辦事項 {todo}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} 重新開啟了任務清單 {calendar} 中的代辦事項 {todo}",
+ "You reopened to-do {todo} in list {calendar}" : "你重新開啟了任務清單 {calendar} 中的代辦事項 {todo}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} 將待辦事項 {todo} 從清單 {sourceCalendar} 移到清單 {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "您將待辦事項 {todo} 從清單 {sourceCalendar} 移到清單 {targetCalendar}",
"Calendar, contacts and tasks" : "日曆、聯絡人和任務",
"A <strong>calendar</strong> was modified" : "<strong>日曆</strong>被修改",
"A calendar <strong>event</strong> was modified" : "日曆<strong>活動</strong>被修改",
- "A calendar <strong>todo</strong> was modified" : "日曆<strong>代辦事項</strong>被修改",
+ "A calendar <strong>to-do</strong> was modified" : "日曆<strong>代辦事項</strong>被修改",
"Contact birthdays" : "聯絡人生日",
"Death of %s" : "%s之卒",
+ "Untitled calendar" : "未命名日曆",
"Calendar:" : "日曆:",
"Date:" : "日期:",
"Where:" : "地點:",
"Description:" : "描述:",
- "Untitled event" : "無標題活動",
"_%n year_::_%n years_" : ["%n 年"],
"_%n month_::_%n months_" : ["%n 月"],
"_%n day_::_%n days_" : ["%n 天"],
@@ -67,22 +72,129 @@ OC.L10N.register(
"Description: %s" : "描述:%s",
"Where: %s" : "地點:%s",
"%1$s via %2$s" : "%1$s 由 %2$s",
+ "In the past on %1$s for the entire day" : "在過去 %1$s 的整天",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["將在 %n 分鐘之後於 %1$s 整天進行"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["將在 %n 小時之後於 %1$s 整天進行"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["將在 %n 天之後於 %1$s 整天進行"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["將在 %n 星期之後於 %1$s 整天進行"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["將在 %n 個月之後於 %1$s 整天進行"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["將在 %n 年之後於 %1$s 整天進行"],
+ "In the past on %1$s between %2$s - %3$s" : "於已經過去的 %1$s %2$s - %3$s 之間進行",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["將在 %n 分鐘之後於 %1$s %2$s - %3$s 之間進行"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["將在 %n 小時之後於 %1$s %2$s - %3$s 之間進行"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["將在 %n 天之後於 %1$s %2$s - %3$s 之間進行"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["將在 %n 星期之後於 %1$s %2$s - %3$s 之間進行"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["將在 %n 個月之後於 %1$s %2$s - %3$s 之間進行"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["將在 %n 年之後於 %1$s %2$s - %3$s 之間進行"],
+ "Could not generate when statement" : "無法產生 when 陳述",
+ "Every Day for the entire day" : "每天一整天",
+ "Every Day for the entire day until %1$s" : "每天一整天,直至 %1$s",
+ "Every Day between %1$s - %2$s" : "每天於 %1$s - %2$s 之間",
+ "Every Day between %1$s - %2$s until %3$s" : "每天於 %1$s - %2$s 之間,直至 %3$s",
+ "Every %1$d Days for the entire day" : "每 %1$d 天整天",
+ "Every %1$d Days for the entire day until %2$s" : "每 %1$d 天整天,直至 %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "每 %1$d 天於 %2$s - %3$s 之間",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "每 %1$d 天於 %2$s - %3$s 之間,直至 %4$s",
+ "Could not generate event recurrence statement" : "無法產生事件重複陳述",
+ "Every Week on %1$s for the entire day" : "每週於 %1$s 整天",
+ "Every Week on %1$s for the entire day until %2$s" : "每週於 %1$s 整天,直到 %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "每週於 %1$s,在 %2$s - %3$s 之間",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "每週於 %1$s,在 %2$s - %3$s 之間,直到 %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "每 %1$d 週於 %2$s 整天",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "每 %1$d 週於 %2$s 整天,直至 %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "每 %1$d 週於 %2$s,在 %3$s - %4$s 之間",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "每 %1$d 週於 %2$s,在 %3$s - %4$s 之間,直至 %5$s",
+ "Every Month on the %1$s for the entire day" : "每個月於 %1$s 整天",
+ "Every Month on the %1$s for the entire day until %2$s" : "每個月於 %1$s 整天,直至 %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "每個月於 %1$s,在 %2$s - %3$s 之間",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "每個月於 %1$s,在 %2$s - %3$s 之間,直至 %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "每 %1$d 個月於 %2$s 整天",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "每 %1$d 個月於 %2$s 整天,直至 %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "每 %1$d 個月於 %2$s,在 %3$s - %4$s 之間",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "每 %1$d 個月於 %2$s,在 %3$s - %4$s 之間,直至 %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "每年於 %1$s 的 %2$s 整天",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "每年於 %1$s 的 %2$s 整天,直至 %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "每年於 %1$s 的 %2$s,在 %3$s - %4$s 之間",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "每年於 %1$s 的 %2$s,在 %3$s - %4$s 之間,直至 %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "每 %1$d 年於 %2$s 的 %3$s 整天",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "每 %1$d 年於 %2$s 的 %3$s 整天,直至 %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "每 %1$d 年於 %2$s 的 %3$s,在 %4$s - %5$s 之間",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "每 %1$d 年於 %2$s 的 %3$s,在 %4$s - %5$s 之間,直至 %6$s",
+ "On specific dates for the entire day until %1$s" : "在特定日期的整天,直至 %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "在 %1$s - %2$s 之間的特定日期,直至 %3$s",
+ "In the past on %1$s" : "於已經過去的 %1$s 進行",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["將在 %n 分鐘之後於 %1$s 進行"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["將在 %n 小時之後於 %1$s 進行"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["將在 %n 天之後於 %1$s 進行"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["將在 %n 星期之後於 %1$s 進行"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["將在 %n 個月之後於 %1$s 進行"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["將在 %n 年之後於 %1$s 進行"],
+ "In the past on %1$s then on %2$s" : "在過去的 %1$s,然後在 %2$s 進行",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["將在 %n 分鐘之後於 %1$s 進行,然後在 %2$s 再次進行"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["將在 %n 小時之後於 %1$s 進行,然後在 %2$s 再次進行"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["將在 %n 天之後於 %1$s 進行,然後在 %2$s 再次進行"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["將在 %n 星期之後於 %1$s 進行,然後在 %2$s 再次進行"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["將在 %n 個月之後於 %1$s 進行,然後在 %2$s 再次進行"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["將在 %n 年之後於 %1$s 進行,然後在 %2$s 再次進行"],
+ "In the past on %1$s then on %2$s and %3$s" : "在過去的 %1$s 進行,然後在 %2$s 及 %3$s 再次進行",
+ "_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_" : ["將在 %n 分鐘之後於 %1$s 進行,然後在 %2$s 及 %3$s再次進行"],
+ "_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_" : ["將在 %n 小時之後於 %1$s 進行,然後在 %2$s 及 %3$s再次進行"],
+ "_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_" : ["將在 %n 天之後於 %1$s 進行,然後在 %2$s 及 %3$s再次進行"],
+ "_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_" : ["將在 %n 星期之後於 %1$s 進行,然後在 %2$s 及 %3$s再次進行"],
+ "_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_" : ["將在 %n 個月之後於 %1$s 進行,然後在 %2$s 及 %3$s再次進行"],
+ "_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_" : ["將在 %n 年之後於 %1$s 進行,然後在 %2$s 及 %3$s再次進行"],
+ "Could not generate next recurrence statement" : "無法產生下一個重複語句",
"Cancelled: %1$s" : "已取消:%1$s",
- "Invitation canceled" : "邀請被取消了",
+ "\"%1$s\" has been canceled" : "\"%1$s\" 已被取消",
"Re: %1$s" : "關於: %1$s",
- "Invitation updated" : "邀請已更新",
+ "%1$s has accepted your invitation" : "%1$s 已接受您的邀請",
+ "%1$s has tentatively accepted your invitation" : "%1$s 暫時接受了您的邀請",
+ "%1$s has declined your invitation" : "%1$s 已婉拒您的邀請",
+ "%1$s has responded to your invitation" : "%1$s 已回應您的邀請",
+ "Invitation updated: %1$s" : "邀請已更新︰%1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s 已更新了活動 \"%2$s\"",
"Invitation: %1$s" : "邀請:%1$s",
- "Invitation" : "邀請",
- "Title:" : "標題:",
- "Time:" : "時間:",
- "Location:" : "地點:",
- "Link:" : "連結:",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s 想邀請您加入“%2$s”",
"Organizer:" : "主辦單位:",
"Attendees:" : "參加者:",
+ "Title:" : "標題:",
+ "When:" : "時間:",
+ "Location:" : "位置:",
+ "Link:" : "連結:",
+ "Occurring:" : "發生:",
"Accept" : "接受",
"Decline" : "拒絕",
"More options …" : "更多選項 ...",
"More options at %s" : "%s有更多選項",
+ "Monday" : "星期一",
+ "Tuesday" : "星期二",
+ "Wednesday" : "星期三",
+ "Thursday" : "星期四",
+ "Friday" : "星期五",
+ "Saturday" : "星期六",
+ "Sunday" : "星期日",
+ "January" : "一月",
+ "February" : "二月",
+ "March" : "三月",
+ "April" : "四月",
+ "May" : "五月",
+ "June" : "六月",
+ "July" : "七月",
+ "August" : "八月",
+ "September" : "九月",
+ "October" : "十月",
+ "November" : "十一月",
+ "December" : "十二月",
+ "First" : "首頁",
+ "Second" : "第二",
+ "Third" : "第三",
+ "Fourth" : "第四",
+ "Fifth" : "第五",
+ "Last" : "最後",
+ "Second Last" : "尾二",
+ "Third Last" : "尾三",
+ "Fourth Last" : "尾四",
+ "Fifth Last" : "尾五",
"Contacts" : "聯絡人",
"{actor} created address book {addressbook}" : "{actor} 創建了通訊錄 {addressbook} ",
"You created address book {addressbook}" : "您創建了通訊錄 {addressbook} ",
@@ -108,7 +220,10 @@ OC.L10N.register(
"{actor} updated contact {card} in address book {addressbook}" : "{actor} 更新了通訊錄 {addressbook} 中的聯絡人 {card}",
"You updated contact {card} in address book {addressbook}" : "您更新了通訊錄 {addressbook} 中的聯絡人 {card}",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "聯絡人或通訊錄被修改",
+ "Accounts" : "帳戶",
+ "System address book which holds all accounts" : "包含所有帳戶的系統通訊錄",
"File is not updatable: %1$s" : "檔案無法更新:%1$s",
+ "Failed to get storage for file" : "無法取得檔案儲存空間",
"Could not write to final file, canceled by hook" : "無法寫入最終檔案,被掛勾取消",
"Could not write file contents" : "無法寫入檔案內容",
"_%n byte_::_%n bytes_" : ["%n 位元組"],
@@ -117,64 +232,107 @@ OC.L10N.register(
"Could not rename part file to final file, canceled by hook" : "無法將部份檔案重新命名為最終檔案,被掛勾取消",
"Could not rename part file to final file" : "無法將部份檔案重新命名為最終檔案",
"Failed to check file size: %1$s" : "檢查檔案大小失敗:%1$s",
- "Could not open file" : "無法開啟檔案",
+ "Could not open file: %1$s, file does seem to exist" : "無法開啟檔案:%1$s,檔案似乎存在",
+ "Could not open file: %1$s, file doesn't seem to exist" : "無法開啟檔案:%1$s,檔案似乎不存在",
"Encryption not ready: %1$s" : "尚未準備好加密:%1$s",
"Failed to open file: %1$s" : "開啟檔案失敗:%1$s",
"Failed to unlink: %1$s" : "解除連結失敗:%1$s",
- "Invalid chunk name" : "無效的區塊名稱",
- "Could not rename part file assembled from chunks" : "無法重新命名從區塊組合成的部份檔案",
"Failed to write file contents: %1$s" : "寫入檔案內容失敗:%1$s",
"File not found: %1$s" : "找不到檔案:%1$s",
+ "Invalid target path" : "無效的目標路徑",
"System is in maintenance mode." : "系統處於維護模式。",
"Upgrade needed" : "需要升級",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "您的 %s 必須要設定 HTTPS ,才能在 iOS/macOS 上使用 CalDAV 和 CardDAV",
"Configures a CalDAV account" : "設定一個 CalDAV 帳號",
"Configures a CardDAV account" : "設定一個 CardDAV 帳號",
"Events" : "活動",
- "Tasks" : "任務",
"Untitled task" : "無標題任務",
"Completed on %s" : "完成於 %s",
"Due on %s by %s" : "完成日期為 %s %s",
"Due on %s" : "完成日期 %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "歡迎使用 Nextcloud 日曆!\n\n這是範例事件 - 使用 Nextcloud 日曆進行任何編輯,探索規劃的彈性!\n\n使用 Nextcloud 日曆,您可以:\n- 毫不費力地建立、編輯與管理活動。\n- 建立多個日曆,並與同事、朋友或家人分享。\n- 檢查可得性,並向他人顯示您的忙碌時間。\n- 透過 CalDAV 與應用程式與裝置無縫整合。\n- 自訂您的體驗:排定定期活動、調整通知與其他設定。",
+ "Example event - open me!" : "範例活動 - 打開我!",
+ "System Address Book" : "系統通訊錄",
+ "The system address book contains contact information for all users in your instance." : "系統通訊錄包含您實例中的所有聯絡人資訊。",
+ "Enable System Address Book" : "啟用系統通訊錄",
+ "DAV system address book" : "DAV 系統通訊錄",
+ "No outstanding DAV system address book sync." : "沒有未完成的 DAV 系統通訊錄同步。",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV 系統通訊錄同步尚未執行,因為您的實例有超過 1000 個用戶,或是因為遇到錯誤。請透過 \"occ dav:sync-system-addressbook\" 手動執行。",
+ "WebDAV endpoint" : "WebDAV 端點",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "無法檢查您的網頁伺服器是否正確設置以允許透過 WebDAV 進行檔案同步。請手動進行檢查。",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "WebDAV 界面似乎為故障狀態,導致您的網頁伺服器無法提供檔案同步功能。",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "您的網頁伺服器已正確設定為允許透過 WebDAV 進行檔案同步。",
"Migrated calendar (%1$s)" : "遷移的日曆(%1$s)",
"Calendars including events, details and attendees" : "日曆,包括活動、詳細信息和與會者",
"Contacts and groups" : "聯絡人和群組",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV 端點",
- "Availability" : "可得性",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "若您設定了您的工作時間,其他使用者在預約會議時就會知道您何時不在辦公室。",
+ "Absence saved" : "缺席時段已儲存",
+ "Failed to save your absence settings" : "無法保存您的缺席設定",
+ "Absence cleared" : "缺席時段已清除",
+ "Failed to clear your absence settings" : "無法清除您的缺席設定",
+ "First day" : "第一天",
+ "Last day (inclusive)" : "最後一天(含)",
+ "Out of office replacement (optional)" : "外出時的替代人員(非強制的)",
+ "Name of the replacement" : "替代人員姓名",
+ "No results." : "沒有結果。",
+ "Start typing." : "開始輸入。",
+ "Short absence status" : "短暫缺席狀態",
+ "Long absence Message" : "長期缺席訊息",
+ "Save" : "保存",
+ "Disable absence" : "停用缺席",
+ "Failed to load availability" : "加載空閒時間失敗",
+ "Saved availability" : "已保存空閒時間",
+ "Failed to save availability" : "未能保存空閒時間",
"Time zone:" : "時區:",
"to" : "至",
"Delete slot" : "刪除欄位",
"No working hours set" : "尚未設置工作時間",
"Add slot" : "新增欄位",
- "Monday" : "星期一",
- "Tuesday" : "星期二",
- "Wednesday" : "星期三",
- "Thursday" : "星期四",
- "Friday" : "星期五",
- "Saturday" : "星期六",
- "Sunday" : "星期日",
- "Save" : "儲存",
+ "Weekdays" : "平日",
+ "Pick a start time for {dayName}" : "為 {dayName} 挑選開始時間",
+ "Pick a end time for {dayName}" : "為 {dayName} 挑選結束時間",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "在忙碌時,自動將用戶狀態設定為「請勿打擾」以靜音所有通知。",
+ "Cancel" : "取消",
+ "Import" : "導入",
+ "Error while saving settings" : "儲存設定時發生錯誤",
+ "Contact reset successfully" : "聯絡人重設成功",
+ "Error while resetting contact" : "重設聯絡人時發生錯誤",
+ "Contact imported successfully" : "成功導入了聯絡人",
+ "Error while importing contact" : "導入聯絡人時發生錯誤",
+ "Import contact" : "導入聯絡人",
+ "Reset to default" : "恢復預設值",
+ "Import contacts" : "導入聯絡人",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "導入新的 .vcf 檔案將刪除現有的默認聯絡人並將其替換為新的聯絡人。您想繼續嗎?",
+ "Failed to save example event creation setting" : "儲存範例事件建立設定失敗",
+ "Failed to upload the example event" : "上傳範例事件失敗",
+ "Custom example event was saved successfully" : "已成功儲存自訂範例事件",
+ "Failed to delete the custom example event" : "刪除自訂範例事件失敗",
+ "Custom example event was deleted successfully" : "已成功刪除自訂範例事件",
+ "Import calendar event" : "導入日曆活動",
+ "Uploading a new event will overwrite the existing one." : "上傳新活動將會覆寫原有的",
+ "Upload event" : "上傳活動",
+ "Availability" : "空閒時間",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "若您設定了您的工作時間,其他人仕在預約會議時就會知道您何時不在辦公室。",
+ "Absence" : "缺席",
+ "Configure your next absence period." : "設定您的下一個缺席時段。",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "也安裝 {calendarappstoreopen}日曆應用程式{linkclose}或安裝{calendardocopen}以連接您的桌面電腦和流動裝置進行同步↗{linkclose}。",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "請確保正確設置{emailopen}電郵伺服器{linkclose}。",
"Calendar server" : "日曆伺服器",
"Send invitations to attendees" : "發送邀請函給參加者",
- "Automatically generate a birthday calendar" : "自動建立生日日曆",
+ "Automatically generate a birthday calendar" : "自動生成生日日曆",
"Birthday calendars will be generated by a background job." : "生日日曆將由後台作業生成。",
- "Hence they will not be available immediately after enabling but will show up after some time." : "因此,它們在啟用後不會立即可用,但會在一段時間後顯示出来。",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "因此,啟用後不會立即可用,但會在一段時間後顯示。",
"Send notifications for events" : "發送活動通知",
"Notifications are sent via background jobs, so these must occur often enough." : "通告將透過後台任務發送,所以任務的頻率要足夠高。",
"Send reminder notifications to calendar sharees as well" : "也向共享日曆者傳送提醒通告",
"Reminders are always sent to organizers and attendees." : "一律傳送提醒通知給舉辦者與參與者。",
"Enable notifications for events via push" : "啟用推送活動通知",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "也安裝 {calendarappstoreopen}日曆應用程式{linkclose}或安裝{calendardocopen}以連接您的桌面電腦和流動裝置進行同步↗{linkclose}。",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "請確保正確設置{emailopen}電郵伺服器{linkclose}。",
+ "Example content" : "範例內容",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "範例內容用來展示 Nextcloud 的功能。Nextcloud 隨附預設內容,可由自訂內容取代。",
"There was an error updating your attendance status." : "更新您的參與狀況時發生錯誤",
"Please contact the organizer directly." : "請直接聯繫絡主辦人",
"Are you accepting the invitation?" : "接受邀請嗎?",
"Tentative" : "暫定",
- "Number of guests" : "訪客数目",
- "Comment" : "留言",
- "Your attendance was updated successfully." : "您的參與狀況成功更新",
- "Calendar and tasks" : "日曆和任務"
+ "Your attendance was updated successfully." : "您的參與狀況成功更新"
},
"nplurals=1; plural=0;");
diff --git a/apps/dav/l10n/zh_HK.json b/apps/dav/l10n/zh_HK.json
index ab9f1a4588d..c02aff5c85f 100644
--- a/apps/dav/l10n/zh_HK.json
+++ b/apps/dav/l10n/zh_HK.json
@@ -1,6 +1,6 @@
{ "translations": {
"Calendar" : "日曆",
- "Todos" : "待辦事項",
+ "Tasks" : "任務",
"Personal" : "個人",
"{actor} created calendar {calendar}" : "{actor} 建立了日曆 {calendar}",
"You created calendar {calendar}" : "您建立了日曆 {calendar}",
@@ -23,36 +23,41 @@
"{actor} shared calendar {calendar} with group {group}" : "{actor} 與群組 {group} 分享了日曆 {calendar}",
"You unshared calendar {calendar} from group {group}" : "您已停止與群組 {group} 分享日曆 {calendar}",
"{actor} unshared calendar {calendar} from group {group}" : "{actor} 從 {group} 群組中取消了分享日曆 {calendar}",
+ "Untitled event" : "無標題活動",
"{actor} created event {event} in calendar {calendar}" : "{actor} 新增了日曆 {calendar} 中的活動 {event}",
"You created event {event} in calendar {calendar}" : "您新增了日曆 {calendar} 中的活動 {event}",
"{actor} deleted event {event} from calendar {calendar}" : "{actor} 從日曆 {calendar} 中刪除了活動 {event}",
"You deleted event {event} from calendar {calendar}" : "您從日曆 {calendar} 中刪除了活動 {event}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} 更新了日曆 {calendar} 中的活動 {event}",
"You updated event {event} in calendar {calendar}" : "您更新了日曆 {calendar} 中的活動 {event}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} 將活動 {event} 從日曆 {sourceCalendar} 移到日曆 {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "您將活動 {event} 從日曆 {sourceCalendar} 移到日曆 {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} 復原了日曆 {calendar} 的活動 {event}",
"You restored event {event} of calendar {calendar}" : "您復原了日曆 {calendar} 的活動 {event}",
"Busy" : "忙碌中",
- "{actor} created todo {todo} in list {calendar}" : "{actor} 在任務列表 {calendar} 中建立了待辦事項 {todo}",
- "You created todo {todo} in list {calendar}" : "您在任務列表 {calendar} 中建立了待辦事項 {todo}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} 從任務列表 {calendar} 中刪除了待辦事項 {todo}",
- "You deleted todo {todo} from list {calendar}" : "您從任務列表 {calendar} 中刪除了待辦事項 {todo}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} 從任務列表 {calendar} 中更新了待辦事項 {todo}",
- "You updated todo {todo} in list {calendar}" : "您在任務列表 {calendar} 中更新了待辦事項 {todo}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} 解决了任務列表 {calendar} 中的代辦事項 {todo}",
- "You solved todo {todo} in list {calendar}" : "您解决了任務列表 {calendar} 中的代辦事項 {todo}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} 重新開啟了任務列表 {calendar} 中的代辦事項 {todo}",
- "You reopened todo {todo} in list {calendar}" : "你重新開啟了 {calendar} 清單中的代辦事項 {todo}",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} 在任務清單 {calendar} 中建立了待辦事項 {todo}",
+ "You created to-do {todo} in list {calendar}" : "您在任務清單 {calendar} 中建立了待辦事項 {todo}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} 從任務清單 {calendar} 中刪除了待辦事項 {todo}",
+ "You deleted to-do {todo} from list {calendar}" : "您從任務清單 {calendar} 中刪除了待辦事項 {todo}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} 從任務清單 {calendar} 中更新了待辦事項 {todo}",
+ "You updated to-do {todo} in list {calendar}" : "您在任務清單 {calendar} 中更新了待辦事項 {todo}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} 解决了任務清單 {calendar} 中的代辦事項 {todo}",
+ "You solved to-do {todo} in list {calendar}" : "您解决了任務清單 {calendar} 中的代辦事項 {todo}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} 重新開啟了任務清單 {calendar} 中的代辦事項 {todo}",
+ "You reopened to-do {todo} in list {calendar}" : "你重新開啟了任務清單 {calendar} 中的代辦事項 {todo}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} 將待辦事項 {todo} 從清單 {sourceCalendar} 移到清單 {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "您將待辦事項 {todo} 從清單 {sourceCalendar} 移到清單 {targetCalendar}",
"Calendar, contacts and tasks" : "日曆、聯絡人和任務",
"A <strong>calendar</strong> was modified" : "<strong>日曆</strong>被修改",
"A calendar <strong>event</strong> was modified" : "日曆<strong>活動</strong>被修改",
- "A calendar <strong>todo</strong> was modified" : "日曆<strong>代辦事項</strong>被修改",
+ "A calendar <strong>to-do</strong> was modified" : "日曆<strong>代辦事項</strong>被修改",
"Contact birthdays" : "聯絡人生日",
"Death of %s" : "%s之卒",
+ "Untitled calendar" : "未命名日曆",
"Calendar:" : "日曆:",
"Date:" : "日期:",
"Where:" : "地點:",
"Description:" : "描述:",
- "Untitled event" : "無標題活動",
"_%n year_::_%n years_" : ["%n 年"],
"_%n month_::_%n months_" : ["%n 月"],
"_%n day_::_%n days_" : ["%n 天"],
@@ -65,22 +70,129 @@
"Description: %s" : "描述:%s",
"Where: %s" : "地點:%s",
"%1$s via %2$s" : "%1$s 由 %2$s",
+ "In the past on %1$s for the entire day" : "在過去 %1$s 的整天",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["將在 %n 分鐘之後於 %1$s 整天進行"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["將在 %n 小時之後於 %1$s 整天進行"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["將在 %n 天之後於 %1$s 整天進行"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["將在 %n 星期之後於 %1$s 整天進行"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["將在 %n 個月之後於 %1$s 整天進行"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["將在 %n 年之後於 %1$s 整天進行"],
+ "In the past on %1$s between %2$s - %3$s" : "於已經過去的 %1$s %2$s - %3$s 之間進行",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["將在 %n 分鐘之後於 %1$s %2$s - %3$s 之間進行"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["將在 %n 小時之後於 %1$s %2$s - %3$s 之間進行"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["將在 %n 天之後於 %1$s %2$s - %3$s 之間進行"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["將在 %n 星期之後於 %1$s %2$s - %3$s 之間進行"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["將在 %n 個月之後於 %1$s %2$s - %3$s 之間進行"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["將在 %n 年之後於 %1$s %2$s - %3$s 之間進行"],
+ "Could not generate when statement" : "無法產生 when 陳述",
+ "Every Day for the entire day" : "每天一整天",
+ "Every Day for the entire day until %1$s" : "每天一整天,直至 %1$s",
+ "Every Day between %1$s - %2$s" : "每天於 %1$s - %2$s 之間",
+ "Every Day between %1$s - %2$s until %3$s" : "每天於 %1$s - %2$s 之間,直至 %3$s",
+ "Every %1$d Days for the entire day" : "每 %1$d 天整天",
+ "Every %1$d Days for the entire day until %2$s" : "每 %1$d 天整天,直至 %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "每 %1$d 天於 %2$s - %3$s 之間",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "每 %1$d 天於 %2$s - %3$s 之間,直至 %4$s",
+ "Could not generate event recurrence statement" : "無法產生事件重複陳述",
+ "Every Week on %1$s for the entire day" : "每週於 %1$s 整天",
+ "Every Week on %1$s for the entire day until %2$s" : "每週於 %1$s 整天,直到 %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "每週於 %1$s,在 %2$s - %3$s 之間",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "每週於 %1$s,在 %2$s - %3$s 之間,直到 %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "每 %1$d 週於 %2$s 整天",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "每 %1$d 週於 %2$s 整天,直至 %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "每 %1$d 週於 %2$s,在 %3$s - %4$s 之間",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "每 %1$d 週於 %2$s,在 %3$s - %4$s 之間,直至 %5$s",
+ "Every Month on the %1$s for the entire day" : "每個月於 %1$s 整天",
+ "Every Month on the %1$s for the entire day until %2$s" : "每個月於 %1$s 整天,直至 %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "每個月於 %1$s,在 %2$s - %3$s 之間",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "每個月於 %1$s,在 %2$s - %3$s 之間,直至 %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "每 %1$d 個月於 %2$s 整天",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "每 %1$d 個月於 %2$s 整天,直至 %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "每 %1$d 個月於 %2$s,在 %3$s - %4$s 之間",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "每 %1$d 個月於 %2$s,在 %3$s - %4$s 之間,直至 %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "每年於 %1$s 的 %2$s 整天",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "每年於 %1$s 的 %2$s 整天,直至 %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "每年於 %1$s 的 %2$s,在 %3$s - %4$s 之間",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "每年於 %1$s 的 %2$s,在 %3$s - %4$s 之間,直至 %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "每 %1$d 年於 %2$s 的 %3$s 整天",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "每 %1$d 年於 %2$s 的 %3$s 整天,直至 %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "每 %1$d 年於 %2$s 的 %3$s,在 %4$s - %5$s 之間",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "每 %1$d 年於 %2$s 的 %3$s,在 %4$s - %5$s 之間,直至 %6$s",
+ "On specific dates for the entire day until %1$s" : "在特定日期的整天,直至 %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "在 %1$s - %2$s 之間的特定日期,直至 %3$s",
+ "In the past on %1$s" : "於已經過去的 %1$s 進行",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["將在 %n 分鐘之後於 %1$s 進行"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["將在 %n 小時之後於 %1$s 進行"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["將在 %n 天之後於 %1$s 進行"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["將在 %n 星期之後於 %1$s 進行"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["將在 %n 個月之後於 %1$s 進行"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["將在 %n 年之後於 %1$s 進行"],
+ "In the past on %1$s then on %2$s" : "在過去的 %1$s,然後在 %2$s 進行",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["將在 %n 分鐘之後於 %1$s 進行,然後在 %2$s 再次進行"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["將在 %n 小時之後於 %1$s 進行,然後在 %2$s 再次進行"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["將在 %n 天之後於 %1$s 進行,然後在 %2$s 再次進行"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["將在 %n 星期之後於 %1$s 進行,然後在 %2$s 再次進行"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["將在 %n 個月之後於 %1$s 進行,然後在 %2$s 再次進行"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["將在 %n 年之後於 %1$s 進行,然後在 %2$s 再次進行"],
+ "In the past on %1$s then on %2$s and %3$s" : "在過去的 %1$s 進行,然後在 %2$s 及 %3$s 再次進行",
+ "_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_" : ["將在 %n 分鐘之後於 %1$s 進行,然後在 %2$s 及 %3$s再次進行"],
+ "_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_" : ["將在 %n 小時之後於 %1$s 進行,然後在 %2$s 及 %3$s再次進行"],
+ "_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_" : ["將在 %n 天之後於 %1$s 進行,然後在 %2$s 及 %3$s再次進行"],
+ "_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_" : ["將在 %n 星期之後於 %1$s 進行,然後在 %2$s 及 %3$s再次進行"],
+ "_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_" : ["將在 %n 個月之後於 %1$s 進行,然後在 %2$s 及 %3$s再次進行"],
+ "_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_" : ["將在 %n 年之後於 %1$s 進行,然後在 %2$s 及 %3$s再次進行"],
+ "Could not generate next recurrence statement" : "無法產生下一個重複語句",
"Cancelled: %1$s" : "已取消:%1$s",
- "Invitation canceled" : "邀請被取消了",
+ "\"%1$s\" has been canceled" : "\"%1$s\" 已被取消",
"Re: %1$s" : "關於: %1$s",
- "Invitation updated" : "邀請已更新",
+ "%1$s has accepted your invitation" : "%1$s 已接受您的邀請",
+ "%1$s has tentatively accepted your invitation" : "%1$s 暫時接受了您的邀請",
+ "%1$s has declined your invitation" : "%1$s 已婉拒您的邀請",
+ "%1$s has responded to your invitation" : "%1$s 已回應您的邀請",
+ "Invitation updated: %1$s" : "邀請已更新︰%1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s 已更新了活動 \"%2$s\"",
"Invitation: %1$s" : "邀請:%1$s",
- "Invitation" : "邀請",
- "Title:" : "標題:",
- "Time:" : "時間:",
- "Location:" : "地點:",
- "Link:" : "連結:",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s 想邀請您加入“%2$s”",
"Organizer:" : "主辦單位:",
"Attendees:" : "參加者:",
+ "Title:" : "標題:",
+ "When:" : "時間:",
+ "Location:" : "位置:",
+ "Link:" : "連結:",
+ "Occurring:" : "發生:",
"Accept" : "接受",
"Decline" : "拒絕",
"More options …" : "更多選項 ...",
"More options at %s" : "%s有更多選項",
+ "Monday" : "星期一",
+ "Tuesday" : "星期二",
+ "Wednesday" : "星期三",
+ "Thursday" : "星期四",
+ "Friday" : "星期五",
+ "Saturday" : "星期六",
+ "Sunday" : "星期日",
+ "January" : "一月",
+ "February" : "二月",
+ "March" : "三月",
+ "April" : "四月",
+ "May" : "五月",
+ "June" : "六月",
+ "July" : "七月",
+ "August" : "八月",
+ "September" : "九月",
+ "October" : "十月",
+ "November" : "十一月",
+ "December" : "十二月",
+ "First" : "首頁",
+ "Second" : "第二",
+ "Third" : "第三",
+ "Fourth" : "第四",
+ "Fifth" : "第五",
+ "Last" : "最後",
+ "Second Last" : "尾二",
+ "Third Last" : "尾三",
+ "Fourth Last" : "尾四",
+ "Fifth Last" : "尾五",
"Contacts" : "聯絡人",
"{actor} created address book {addressbook}" : "{actor} 創建了通訊錄 {addressbook} ",
"You created address book {addressbook}" : "您創建了通訊錄 {addressbook} ",
@@ -106,7 +218,10 @@
"{actor} updated contact {card} in address book {addressbook}" : "{actor} 更新了通訊錄 {addressbook} 中的聯絡人 {card}",
"You updated contact {card} in address book {addressbook}" : "您更新了通訊錄 {addressbook} 中的聯絡人 {card}",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "聯絡人或通訊錄被修改",
+ "Accounts" : "帳戶",
+ "System address book which holds all accounts" : "包含所有帳戶的系統通訊錄",
"File is not updatable: %1$s" : "檔案無法更新:%1$s",
+ "Failed to get storage for file" : "無法取得檔案儲存空間",
"Could not write to final file, canceled by hook" : "無法寫入最終檔案,被掛勾取消",
"Could not write file contents" : "無法寫入檔案內容",
"_%n byte_::_%n bytes_" : ["%n 位元組"],
@@ -115,64 +230,107 @@
"Could not rename part file to final file, canceled by hook" : "無法將部份檔案重新命名為最終檔案,被掛勾取消",
"Could not rename part file to final file" : "無法將部份檔案重新命名為最終檔案",
"Failed to check file size: %1$s" : "檢查檔案大小失敗:%1$s",
- "Could not open file" : "無法開啟檔案",
+ "Could not open file: %1$s, file does seem to exist" : "無法開啟檔案:%1$s,檔案似乎存在",
+ "Could not open file: %1$s, file doesn't seem to exist" : "無法開啟檔案:%1$s,檔案似乎不存在",
"Encryption not ready: %1$s" : "尚未準備好加密:%1$s",
"Failed to open file: %1$s" : "開啟檔案失敗:%1$s",
"Failed to unlink: %1$s" : "解除連結失敗:%1$s",
- "Invalid chunk name" : "無效的區塊名稱",
- "Could not rename part file assembled from chunks" : "無法重新命名從區塊組合成的部份檔案",
"Failed to write file contents: %1$s" : "寫入檔案內容失敗:%1$s",
"File not found: %1$s" : "找不到檔案:%1$s",
+ "Invalid target path" : "無效的目標路徑",
"System is in maintenance mode." : "系統處於維護模式。",
"Upgrade needed" : "需要升級",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "您的 %s 必須要設定 HTTPS ,才能在 iOS/macOS 上使用 CalDAV 和 CardDAV",
"Configures a CalDAV account" : "設定一個 CalDAV 帳號",
"Configures a CardDAV account" : "設定一個 CardDAV 帳號",
"Events" : "活動",
- "Tasks" : "任務",
"Untitled task" : "無標題任務",
"Completed on %s" : "完成於 %s",
"Due on %s by %s" : "完成日期為 %s %s",
"Due on %s" : "完成日期 %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "歡迎使用 Nextcloud 日曆!\n\n這是範例事件 - 使用 Nextcloud 日曆進行任何編輯,探索規劃的彈性!\n\n使用 Nextcloud 日曆,您可以:\n- 毫不費力地建立、編輯與管理活動。\n- 建立多個日曆,並與同事、朋友或家人分享。\n- 檢查可得性,並向他人顯示您的忙碌時間。\n- 透過 CalDAV 與應用程式與裝置無縫整合。\n- 自訂您的體驗:排定定期活動、調整通知與其他設定。",
+ "Example event - open me!" : "範例活動 - 打開我!",
+ "System Address Book" : "系統通訊錄",
+ "The system address book contains contact information for all users in your instance." : "系統通訊錄包含您實例中的所有聯絡人資訊。",
+ "Enable System Address Book" : "啟用系統通訊錄",
+ "DAV system address book" : "DAV 系統通訊錄",
+ "No outstanding DAV system address book sync." : "沒有未完成的 DAV 系統通訊錄同步。",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV 系統通訊錄同步尚未執行,因為您的實例有超過 1000 個用戶,或是因為遇到錯誤。請透過 \"occ dav:sync-system-addressbook\" 手動執行。",
+ "WebDAV endpoint" : "WebDAV 端點",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "無法檢查您的網頁伺服器是否正確設置以允許透過 WebDAV 進行檔案同步。請手動進行檢查。",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "WebDAV 界面似乎為故障狀態,導致您的網頁伺服器無法提供檔案同步功能。",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "您的網頁伺服器已正確設定為允許透過 WebDAV 進行檔案同步。",
"Migrated calendar (%1$s)" : "遷移的日曆(%1$s)",
"Calendars including events, details and attendees" : "日曆,包括活動、詳細信息和與會者",
"Contacts and groups" : "聯絡人和群組",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV 端點",
- "Availability" : "可得性",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "若您設定了您的工作時間,其他使用者在預約會議時就會知道您何時不在辦公室。",
+ "Absence saved" : "缺席時段已儲存",
+ "Failed to save your absence settings" : "無法保存您的缺席設定",
+ "Absence cleared" : "缺席時段已清除",
+ "Failed to clear your absence settings" : "無法清除您的缺席設定",
+ "First day" : "第一天",
+ "Last day (inclusive)" : "最後一天(含)",
+ "Out of office replacement (optional)" : "外出時的替代人員(非強制的)",
+ "Name of the replacement" : "替代人員姓名",
+ "No results." : "沒有結果。",
+ "Start typing." : "開始輸入。",
+ "Short absence status" : "短暫缺席狀態",
+ "Long absence Message" : "長期缺席訊息",
+ "Save" : "保存",
+ "Disable absence" : "停用缺席",
+ "Failed to load availability" : "加載空閒時間失敗",
+ "Saved availability" : "已保存空閒時間",
+ "Failed to save availability" : "未能保存空閒時間",
"Time zone:" : "時區:",
"to" : "至",
"Delete slot" : "刪除欄位",
"No working hours set" : "尚未設置工作時間",
"Add slot" : "新增欄位",
- "Monday" : "星期一",
- "Tuesday" : "星期二",
- "Wednesday" : "星期三",
- "Thursday" : "星期四",
- "Friday" : "星期五",
- "Saturday" : "星期六",
- "Sunday" : "星期日",
- "Save" : "儲存",
+ "Weekdays" : "平日",
+ "Pick a start time for {dayName}" : "為 {dayName} 挑選開始時間",
+ "Pick a end time for {dayName}" : "為 {dayName} 挑選結束時間",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "在忙碌時,自動將用戶狀態設定為「請勿打擾」以靜音所有通知。",
+ "Cancel" : "取消",
+ "Import" : "導入",
+ "Error while saving settings" : "儲存設定時發生錯誤",
+ "Contact reset successfully" : "聯絡人重設成功",
+ "Error while resetting contact" : "重設聯絡人時發生錯誤",
+ "Contact imported successfully" : "成功導入了聯絡人",
+ "Error while importing contact" : "導入聯絡人時發生錯誤",
+ "Import contact" : "導入聯絡人",
+ "Reset to default" : "恢復預設值",
+ "Import contacts" : "導入聯絡人",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "導入新的 .vcf 檔案將刪除現有的默認聯絡人並將其替換為新的聯絡人。您想繼續嗎?",
+ "Failed to save example event creation setting" : "儲存範例事件建立設定失敗",
+ "Failed to upload the example event" : "上傳範例事件失敗",
+ "Custom example event was saved successfully" : "已成功儲存自訂範例事件",
+ "Failed to delete the custom example event" : "刪除自訂範例事件失敗",
+ "Custom example event was deleted successfully" : "已成功刪除自訂範例事件",
+ "Import calendar event" : "導入日曆活動",
+ "Uploading a new event will overwrite the existing one." : "上傳新活動將會覆寫原有的",
+ "Upload event" : "上傳活動",
+ "Availability" : "空閒時間",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "若您設定了您的工作時間,其他人仕在預約會議時就會知道您何時不在辦公室。",
+ "Absence" : "缺席",
+ "Configure your next absence period." : "設定您的下一個缺席時段。",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "也安裝 {calendarappstoreopen}日曆應用程式{linkclose}或安裝{calendardocopen}以連接您的桌面電腦和流動裝置進行同步↗{linkclose}。",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "請確保正確設置{emailopen}電郵伺服器{linkclose}。",
"Calendar server" : "日曆伺服器",
"Send invitations to attendees" : "發送邀請函給參加者",
- "Automatically generate a birthday calendar" : "自動建立生日日曆",
+ "Automatically generate a birthday calendar" : "自動生成生日日曆",
"Birthday calendars will be generated by a background job." : "生日日曆將由後台作業生成。",
- "Hence they will not be available immediately after enabling but will show up after some time." : "因此,它們在啟用後不會立即可用,但會在一段時間後顯示出来。",
+ "Hence they will not be available immediately after enabling but will show up after some time." : "因此,啟用後不會立即可用,但會在一段時間後顯示。",
"Send notifications for events" : "發送活動通知",
"Notifications are sent via background jobs, so these must occur often enough." : "通告將透過後台任務發送,所以任務的頻率要足夠高。",
"Send reminder notifications to calendar sharees as well" : "也向共享日曆者傳送提醒通告",
"Reminders are always sent to organizers and attendees." : "一律傳送提醒通知給舉辦者與參與者。",
"Enable notifications for events via push" : "啟用推送活動通知",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "也安裝 {calendarappstoreopen}日曆應用程式{linkclose}或安裝{calendardocopen}以連接您的桌面電腦和流動裝置進行同步↗{linkclose}。",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "請確保正確設置{emailopen}電郵伺服器{linkclose}。",
+ "Example content" : "範例內容",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "範例內容用來展示 Nextcloud 的功能。Nextcloud 隨附預設內容,可由自訂內容取代。",
"There was an error updating your attendance status." : "更新您的參與狀況時發生錯誤",
"Please contact the organizer directly." : "請直接聯繫絡主辦人",
"Are you accepting the invitation?" : "接受邀請嗎?",
"Tentative" : "暫定",
- "Number of guests" : "訪客数目",
- "Comment" : "留言",
- "Your attendance was updated successfully." : "您的參與狀況成功更新",
- "Calendar and tasks" : "日曆和任務"
+ "Your attendance was updated successfully." : "您的參與狀況成功更新"
},"pluralForm" :"nplurals=1; plural=0;"
} \ No newline at end of file
diff --git a/apps/dav/l10n/zh_TW.js b/apps/dav/l10n/zh_TW.js
index 414eaad4c01..56784c958bd 100644
--- a/apps/dav/l10n/zh_TW.js
+++ b/apps/dav/l10n/zh_TW.js
@@ -1,88 +1,200 @@
OC.L10N.register(
"dav",
{
- "Calendar" : "日曆",
- "Todos" : "待辦事項",
+ "Calendar" : "行事曆",
+ "Tasks" : "工作項目",
"Personal" : "個人",
- "{actor} created calendar {calendar}" : "{actor} 建立了日曆 {calendar}",
- "You created calendar {calendar}" : "您建立了日曆 {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} 刪除了日曆 {calendar}",
- "You deleted calendar {calendar}" : "您刪除了日曆 {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} 更新了日曆 {calendar}",
- "You updated calendar {calendar}" : "您更新了日曆 {calendar}",
- "{actor} restored calendar {calendar}" : "{actor} 復原了日曆 {calendar}",
- "You restored calendar {calendar}" : "您復原了日曆 {calendar}",
- "You shared calendar {calendar} as public link" : "您將 {calendar} 日曆以公開連結分享",
+ "{actor} created calendar {calendar}" : "{actor} 建立了行事曆 {calendar}",
+ "You created calendar {calendar}" : "您建立了行事曆 {calendar}",
+ "{actor} deleted calendar {calendar}" : "{actor} 刪除了行事曆 {calendar}",
+ "You deleted calendar {calendar}" : "您刪除了行事曆 {calendar}",
+ "{actor} updated calendar {calendar}" : "{actor} 更新了行事曆 {calendar}",
+ "You updated calendar {calendar}" : "您更新了行事曆 {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} 復原了行事曆 {calendar}",
+ "You restored calendar {calendar}" : "您復原了行事曆 {calendar}",
+ "You shared calendar {calendar} as public link" : "您已將 {calendar} 行事曆以公開連結分享",
"You removed public link for calendar {calendar}" : "您刪除了 {calendar} 的公開分享連結",
- "{actor} shared calendar {calendar} with you" : "{actor} 與您分享了 {calendar} 日曆",
- "You shared calendar {calendar} with {user}" : "您與 {user} 分享了 {calendar} 日曆",
- "{actor} shared calendar {calendar} with {user}" : "{actor} 與 {user} 分享了日曆 {calendar}",
- "{actor} unshared calendar {calendar} from you" : "{actor} 停止與您分享日曆 {calendar}",
- "You unshared calendar {calendar} from {user}" : "您停止與 {user} 分享日曆 {calendar}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} 停止與 {user} 分享日曆 {calendar}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} 停止與他們自己分享日曆 {calendar}",
- "You shared calendar {calendar} with group {group}" : "您與群組 {group} 分享了日曆 {calendar}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} 與群組 {group} 分享了日曆 {calendar}",
- "You unshared calendar {calendar} from group {group}" : "您已停止與群組 {group} 分享日曆 {calendar}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} 已停止與群組 {group} 分享日曆 {calendar}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} 在日曆 {calendar} 中建立了 {event} 活動",
- "You created event {event} in calendar {calendar}" : "您在日曆 {calendar} 中建立了 {event} 活動",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} 在日曆 {calendar} 中刪除了 {event} 活動",
- "You deleted event {event} from calendar {calendar}" : "您在日曆 {calendar} 中刪除了 {event} 活動",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} 在日曆 {calendar} 中更新了 {event} 活動",
- "You updated event {event} in calendar {calendar}" : "您在日曆 {calendar} 中刪除了 {event} 活動",
- "{actor} restored event {event} of calendar {calendar}" : "{actor} 復原了日曆 {calendar} 中的活動 {event}",
- "You restored event {event} of calendar {calendar}" : "您復原了日曆 {calendar} 中的活動 {event}",
+ "{actor} shared calendar {calendar} with you" : "{actor} 與您分享了 {calendar} 行事曆",
+ "You shared calendar {calendar} with {user}" : "您與 {user} 分享了 {calendar} 行事曆",
+ "{actor} shared calendar {calendar} with {user}" : "{actor} 與 {user} 分享了行事曆 {calendar}",
+ "{actor} unshared calendar {calendar} from you" : "{actor} 已停止與您分享行事曆 {calendar}",
+ "You unshared calendar {calendar} from {user}" : "您已停止與 {user} 分享行事曆 {calendar}",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor} 已停止與 {user} 分享行事曆 {calendar}",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} 已停止與自己分享行事曆 {calendar}",
+ "You shared calendar {calendar} with group {group}" : "您已與群組 {group} 分享了行事曆 {calendar}",
+ "{actor} shared calendar {calendar} with group {group}" : "{actor} 已與群組 {group} 分享了行事曆 {calendar}",
+ "You unshared calendar {calendar} from group {group}" : "您已停止與群組 {group} 分享行事曆 {calendar}",
+ "{actor} unshared calendar {calendar} from group {group}" : "{actor} 已停止與群組 {group} 分享行事曆 {calendar}",
+ "Untitled event" : "未命名行程",
+ "{actor} created event {event} in calendar {calendar}" : "{actor} 在行事曆 {calendar} 中建立了行程 {event}",
+ "You created event {event} in calendar {calendar}" : "您在行事曆 {calendar} 中建立了行程 {event}",
+ "{actor} deleted event {event} from calendar {calendar}" : "{actor} 在行事曆 {calendar} 中刪除了行程 {event}",
+ "You deleted event {event} from calendar {calendar}" : "您在行事曆 {calendar} 中刪除了行程 {event}",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} 在行事曆 {calendar} 中更新了行程 {event}",
+ "You updated event {event} in calendar {calendar}" : "您在行事曆 {calendar} 中刪除了行程 {event}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} 將行程 {event} 從行事曆 {sourceCalendar} 移動至行事曆 {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "您將行程 {event} 從行事曆 {sourceCalendar} 移動至行事曆 {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} 復原了行事曆 {calendar} 中的行程 {event}",
+ "You restored event {event} of calendar {calendar}" : "您復原了行事曆 {calendar} 中的行程 {event}",
"Busy" : "忙碌",
- "{actor} created todo {todo} in list {calendar}" : "{actor} 在列表 {calendar} 中建立了待辦事項 {todo}",
- "You created todo {todo} in list {calendar}" : "您在列表 {calendar} 中建立了待辦事項 {todo}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} 在列表 {calendar} 中刪除了待辦事項 {todo}",
- "You deleted todo {todo} from list {calendar}" : "您在列表 {calendar} 中刪除了待辦事項 {todo}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} 在列表 {calendar} 中更新了待辦事項 {todo}",
- "You updated todo {todo} in list {calendar}" : "您在列表 {calendar} 中更新了待辦事項 {todo}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} 在列表 {calendar} 中解決了待辦事項 {todo}",
- "You solved todo {todo} in list {calendar}" : "您在列表 {calendar} 中解決了待辦事項 {todo}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} 在列表 {calendar} 中重新開啟了待辦事項 {todo}",
- "You reopened todo {todo} in list {calendar}" : "您在列表 {calendar} 中重新開啟了待辦事項 {todo}",
- "Calendar, contacts and tasks" : "日曆、通訊錄與工作項目",
- "A <strong>calendar</strong> was modified" : "一個<strong>日曆</strong>被更動",
- "A calendar <strong>event</strong> was modified" : "一個日曆<strong>活動</strong>被更動",
- "A calendar <strong>todo</strong> was modified" : "一個日曆<strong>待辦事項</strong>被更動",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} 在清單 {calendar} 中建立了待辦事項 {todo}",
+ "You created to-do {todo} in list {calendar}" : "您在清單 {calendar} 中建立了待辦事項 {todo}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} 從清單 {calendar} 中刪除了待辦事項 {todo}",
+ "You deleted to-do {todo} from list {calendar}" : "您從清單 {calendar} 中刪除了待辦事項 {todo}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} 在清單 {calendar} 中更新了待辦事項 {todo}",
+ "You updated to-do {todo} in list {calendar}" : "您在清單 {calendar} 中更新了待辦事項 {todo}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} 在清單 {calendar} 中解決了待辦事項 {todo}",
+ "You solved to-do {todo} in list {calendar}" : "您在清單 {calendar} 中解決了待辦事項 {todo}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} 在清單 {calendar} 中重新開啟了待辦事項 {todo}",
+ "You reopened to-do {todo} in list {calendar}" : "您在清單 {calendar} 中重新開啟了待辦事項 {todo}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} 將待辦事項 {todo} 從清單 {sourceCalendar} 移動到清單 {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "您將待辦事項 {todo} 從清單 {sourceCalendar} 移動到清單 {targetCalendar}",
+ "Calendar, contacts and tasks" : "行事曆、聯絡人與工作項目",
+ "A <strong>calendar</strong> was modified" : "一個<strong>行事曆</strong>被更動",
+ "A calendar <strong>event</strong> was modified" : "一個行事曆<strong>行程</strong>被更動",
+ "A calendar <strong>to-do</strong> was modified" : "已修改一個行事曆<strong>待辦事項</strong>",
"Contact birthdays" : "聯絡人生日",
"Death of %s" : "%s 逝世",
- "Calendar:" : "日曆:",
+ "Untitled calendar" : "未命名行事曆",
+ "Calendar:" : "行事曆:",
"Date:" : "日期:",
"Where:" : "地點:",
"Description:" : "描述:",
- "Untitled event" : "未命名活動",
- "_%n year_::_%n years_" : ["%n年"],
- "_%n month_::_%n months_" : ["%n月"],
- "_%n day_::_%n days_" : ["%n天"],
- "_%n hour_::_%n hours_" : ["%n小時"],
- "_%n minute_::_%n minutes_" : ["%n分鐘"],
- "%s (in %s)" : "%s(在 %s)",
- "%s (%s ago)" : "%s(%s 前)",
- "Calendar: %s" : "日曆:%s",
+ "_%n year_::_%n years_" : ["%n 年"],
+ "_%n month_::_%n months_" : ["%n 月"],
+ "_%n day_::_%n days_" : ["%n 天"],
+ "_%n hour_::_%n hours_" : ["%n 小時"],
+ "_%n minute_::_%n minutes_" : ["%n 分鐘"],
+ "%s (in %s)" : "%s(還有 %s)",
+ "%s (%s ago)" : "%s(%s之前)",
+ "Calendar: %s" : "行事曆:%s",
"Date: %s" : "日期:%s",
"Description: %s" : "描述:%s",
"Where: %s" : "地點:%s",
- "%1$s via %2$s" : "%1$s 由 %2$s",
+ "%1$s via %2$s" : "%1$s 經由 %2$s",
+ "In the past on %1$s for the entire day" : "在過去 %1$s 的整天",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["在 %1$s 的整天內,%n 分鐘"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["在 %1$s 的整天內,%n 小時"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["在 %1$s 的整天內,%n 天"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["在 %1$s 的整天內,%n 週"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["在 %1$s 的整天內,%n 分鐘"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["在 %1$s 的整天內,%n 年"],
+ "In the past on %1$s between %2$s - %3$s" : "在過去的 %1$s,在 %2$s - %3$s 之間",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["於 %1$s 的 %n 分鐘內,在 %2$s - %3$s 之間"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["於 %1$s 的 %n 小時內,在 %2$s - %3$s 之間"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["於 %1$s 的 %n 天內,在 %2$s - %3$s 之間"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["於 %1$s 的 %n 週內,在 %2$s - %3$s 之間"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["於 %1$s 的 %n 月內,在 %2$s - %3$s 之間"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["於 %1$s 的 %n 年內,在 %2$s - %3$s 之間"],
+ "Could not generate when statement" : "無法產生 when 陳述",
+ "Every Day for the entire day" : "每天一整天",
+ "Every Day for the entire day until %1$s" : "每天一整天,直到 %1$s",
+ "Every Day between %1$s - %2$s" : "每天於 %1$s - %2$s 之間",
+ "Every Day between %1$s - %2$s until %3$s" : "每天於 %1$s - %2$s 之間,直到 %3$s",
+ "Every %1$d Days for the entire day" : "每 %1$d 天整天",
+ "Every %1$d Days for the entire day until %2$s" : "每 %1$d 天整天,直到 %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "每 %1$d 天於 %2$s - %3$s 之間",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "每 %1$d 天於 %2$s - %3$s 之間,直到 %4$s",
+ "Could not generate event recurrence statement" : "無法產生事件重複陳述",
+ "Every Week on %1$s for the entire day" : "每週於 %1$s 整天",
+ "Every Week on %1$s for the entire day until %2$s" : "每週於 %1$s 整天,直到 %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "每週於 %1$s,在 %2$s - %3$s 之間",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "每週於 %1$s,在 %2$s - %3$s 之間,直到 %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "每 %1$d 週於 %2$s 整天",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "每 %1$d 週於 %2$s 整天,直到 %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "每 %1$d 週於 %2$s,在 %3$s - %4$s 之間",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "每 %1$d 週於 %2$s,在 %3$s - %4$s 之間,直到 %5$s",
+ "Every Month on the %1$s for the entire day" : "每個月於 %1$s 整天",
+ "Every Month on the %1$s for the entire day until %2$s" : "每個月於 %1$s 整天,直到 %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "每個月於 %1$s,在 %2$s - %3$s 之間",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "每個月於 %1$s,在 %2$s - %3$s 之間,直到 %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "每 %1$d 個月於 %2$s 整天",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "每 %1$d 個月於 %2$s 整天,直到 %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "每 %1$d 個月於 %2$s,在 %3$s - %4$s 之間",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "每 %1$d 個月於 %2$s,在 %3$s - %4$s 之間,直到 %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "每年於 %1$s 的 %2$s 整天",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "每年於 %1$s 的 %2$s 整天,直到 %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "每年於 %1$s 的 %2$s,在 %3$s - %4$s 之間",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "每年於 %1$s 的 %2$s,在 %3$s - %4$s 之間,直到 %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "每 %1$d 年於 %2$s 的 %3$s 整天",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "每 %1$d 年於 %2$s 的 %3$s 整天,直到 %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "每 %1$d 年於 %2$s 的 %3$s,在 %4$s - %5$s 之間",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "每 %1$d 年於 %2$s 的 %3$s,在 %4$s - %5$s 之間,直到 %6$s",
+ "On specific dates for the entire day until %1$s" : "在特定日期的整天,直到 %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "在 %1$s - %2$s 之間的特定日期,直到 %3$s",
+ "In the past on %1$s" : "過去在 %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["將在 %n 分鐘之後於 %1$s 進行"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["將在 %n 小時之後於 %1$s 進行"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["將在 %n 天之後於 %1$s 進行"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["將在 %n 週之後於 %1$s 進行"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["將在 %n 個月之後於 %1$s 進行"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["將在 %n 年之後於 %1$s 進行"],
+ "In the past on %1$s then on %2$s" : "在過去的 %1$s,然後在 %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["在 %1$s 分鐘內,然後在 %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["在 %1$s 小時內,然後在 %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["在 %1$s 天內,然後在 %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["在 %1$s 週內,然後在 %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["在 %1$s 月內,然後在 %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["在 %1$s 年內,然後在 %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "在過去的 %1$s,然後在 %2$s 與 %3$s",
+ "_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_" : ["於 %1$s 的 %n 分鐘內,然後在 %2$s 與 %3$s"],
+ "_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_" : ["於 %1$s 的 %n 小時內,然後在 %2$s 與 %3$s"],
+ "_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_" : ["於 %1$s 的 %n 天內,然後在 %2$s 與 %3$s"],
+ "_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_" : ["於 %1$s 的 %n 週內,然後在 %2$s 與 %3$s"],
+ "_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_" : ["於 %1$s 的 %n 月內,然後在 %2$s 與 %3$s"],
+ "_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_" : ["於 %1$s 的 %n 年內,然後在 %2$s 與 %3$s"],
+ "Could not generate next recurrence statement" : "無法產生下一個重複陳述",
"Cancelled: %1$s" : "已取消:%1$s",
- "Invitation canceled" : "邀請已取消",
+ "\"%1$s\" has been canceled" : "「%1$s」已取消",
"Re: %1$s" : "回覆:%1$s",
- "Invitation updated" : "邀請已更新",
+ "%1$s has accepted your invitation" : "%1$s 已接受您的邀請",
+ "%1$s has tentatively accepted your invitation" : "%1$s 考慮接受了您的邀請",
+ "%1$s has declined your invitation" : "%1$s 已婉拒您的邀請",
+ "%1$s has responded to your invitation" : "%1$s 已回應您的邀請",
+ "Invitation updated: %1$s" : "邀請已更新:%1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s 已更新行程:「%2$s」",
"Invitation: %1$s" : "邀請:%1$s",
- "Invitation" : "邀請",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s 想邀請您參與「%2$s」",
+ "Organizer:" : "主辦者:",
+ "Attendees:" : "參與者:",
"Title:" : "標題:",
- "Time:" : "時間:",
+ "When:" : "時間:",
"Location:" : "地點:",
"Link:" : "連結:",
- "Organizer:" : "組織者:",
- "Attendees:" : "參與者:",
+ "Occurring:" : "發生:",
"Accept" : "接受",
"Decline" : "拒絕",
- "More options …" : "更多選項……",
+ "More options …" : "更多選項…",
"More options at %s" : "%s 有更多選項",
+ "Monday" : "週一",
+ "Tuesday" : "週二",
+ "Wednesday" : "週三",
+ "Thursday" : "週四",
+ "Friday" : "週五",
+ "Saturday" : "週六",
+ "Sunday" : "週日",
+ "January" : "一月",
+ "February" : "二月",
+ "March" : "三月",
+ "April" : "四月",
+ "May" : "五月",
+ "June" : "六月",
+ "July" : "七月",
+ "August" : "八月",
+ "September" : "九月",
+ "October" : "十月",
+ "November" : "十一月",
+ "December" : "十二月",
+ "First" : "第一個",
+ "Second" : "第二",
+ "Third" : "第三",
+ "Fourth" : "第四",
+ "Fifth" : "第五",
+ "Last" : "最後",
+ "Second Last" : "倒數第二",
+ "Third Last" : "倒數第三",
+ "Fourth Last" : "倒數第四",
+ "Fifth Last" : "倒數第五",
"Contacts" : "聯絡人",
"{actor} created address book {addressbook}" : "{actor} 建立了通訊錄 {addressbook}",
"You created address book {addressbook}" : "您建立了通訊錄 {addressbook}",
@@ -93,88 +205,134 @@ OC.L10N.register(
"{actor} shared address book {addressbook} with you" : "{actor} 與您分享了通訊錄 {addressbook}",
"You shared address book {addressbook} with {user}" : "您與 {user} 分享了通訊錄 {addressbook}",
"{actor} shared address book {addressbook} with {user}" : "{actor} 與 {user} 分享了通訊錄 {addressbook}",
- "{actor} unshared address book {addressbook} from you" : "{actor} 取消與您分享通訊錄 {addressbook}",
- "You unshared address book {addressbook} from {user}" : "您取消與 {user} 分享的通訊錄 {addressbook}",
- "{actor} unshared address book {addressbook} from {user}" : "{actor} 取消與 {user} 分享通訊錄 {addressbook}",
- "{actor} unshared address book {addressbook} from themselves" : "{actor} 取消與他們自己分享的通訊錄 {addressbook}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} 已取消與您分享通訊錄 {addressbook}",
+ "You unshared address book {addressbook} from {user}" : "您已取消與 {user} 分享通訊錄 {addressbook}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} 已取消與 {user} 分享通訊錄 {addressbook}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} 已取消與自己分享通訊錄 {addressbook}",
"You shared address book {addressbook} with group {group}" : "您與群組 {group} 分享了通訊錄 {addressbook}",
"{actor} shared address book {addressbook} with group {group}" : "{actor} 與群組 {group} 分享了通訊錄 {addressbook}",
- "You unshared address book {addressbook} from group {group}" : "您取消與群組 {group} 分享的通訊錄 {addressbook}",
- "{actor} unshared address book {addressbook} from group {group}" : "{actor} 取消與群組 {group} 分享的通訊錄 {addressbook}",
- "{actor} created contact {card} in address book {addressbook}" : "{actor} 在通訊錄 {addressbook} 中建立了聯絡人 {card}",
- "You created contact {card} in address book {addressbook}" : "您在通訊錄 {addressbook} 中建立了聯絡人 {card}",
+ "You unshared address book {addressbook} from group {group}" : "您已取消與群組 {group} 分享通訊錄 {addressbook}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} 已取消與群組 {group} 分享通訊錄 {addressbook}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} 在通訊錄 {addressbook} 建立了聯絡人 {card}",
+ "You created contact {card} in address book {addressbook}" : "您在通訊錄 {addressbook} 建立了聯絡人 {card}",
"{actor} deleted contact {card} from address book {addressbook}" : "{actor} 從通訊錄 {addressbook} 刪除了聯絡人 {card}",
"You deleted contact {card} from address book {addressbook}" : "您從通訊錄 {addressbook} 刪除了聯絡人 {card}",
- "{actor} updated contact {card} in address book {addressbook}" : "{actor} 在通訊錄 {addressbook} 中更新了聯絡人 {card}",
- "You updated contact {card} in address book {addressbook}" : "您在通訊錄 {addressbook} 中更新了聯絡人 {card}",
- "A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>聯絡人</strong>或<strong>通訊錄</strong>已被修改",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} 在通訊錄 {addressbook} 更新了聯絡人 {card}",
+ "You updated contact {card} in address book {addressbook}" : "您在通訊錄 {addressbook} 更新了聯絡人 {card}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>聯絡人</strong>或<strong>通訊錄</strong>已修改",
+ "Accounts" : "帳號",
+ "System address book which holds all accounts" : "包含所有帳號的系統通訊錄",
"File is not updatable: %1$s" : "檔案無法更新:%1$s",
- "Could not write to final file, canceled by hook" : "無法寫入最終檔案,被掛勾取消",
+ "Failed to get storage for file" : "無法取得檔案儲存空間",
+ "Could not write to final file, canceled by hook" : "無法寫入最終檔案,被連動取消",
"Could not write file contents" : "無法寫入檔案內容",
"_%n byte_::_%n bytes_" : ["%n 位元組"],
"Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "將檔案複製到目標位置時發生錯誤(已複製:%1$s,預期的檔案大小:%2$s)",
- "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." : "預期的檔案大小為 %1$s,但讀取(從 Nextcloud 客戶端)與寫入(至 Nextcloud 儲存空間)%2$s。可能是傳送端的網路問題或是伺服器端的儲存空間寫入問題。",
- "Could not rename part file to final file, canceled by hook" : "無法將部份檔案重新命名為最終檔案,被掛勾取消",
+ "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." : "預期的檔案大小為 %1$s,但讀取(從 Nextcloud 客戶端)與寫入(至 Nextcloud 儲存空間)%2$s。可能是傳送端的網路問題,或是伺服器端的儲存空間寫入問題。",
+ "Could not rename part file to final file, canceled by hook" : "無法將部份檔案重新命名為最終檔案,被連動取消",
"Could not rename part file to final file" : "無法將部份檔案重新命名為最終檔案",
"Failed to check file size: %1$s" : "檢查檔案大小失敗:%1$s",
- "Could not open file" : "無法開啟檔案",
+ "Could not open file: %1$s, file does seem to exist" : "無法開啟檔案:%1$s,檔案似乎存在",
+ "Could not open file: %1$s, file doesn't seem to exist" : "無法開啟檔案:%1$s,檔案似乎不存在",
"Encryption not ready: %1$s" : "尚未準備好加密:%1$s",
"Failed to open file: %1$s" : "開啟檔案失敗:%1$s",
"Failed to unlink: %1$s" : "解除連結失敗:%1$s",
- "Invalid chunk name" : "無效的區塊名稱",
- "Could not rename part file assembled from chunks" : "無法重新命名從區塊組合成的部份檔案",
"Failed to write file contents: %1$s" : "寫入檔案內容失敗:%1$s",
"File not found: %1$s" : "找不到檔案:%1$s",
+ "Invalid target path" : "無效目標路徑",
"System is in maintenance mode." : "系統處於維護模式。",
"Upgrade needed" : "需要升級",
- "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "您的 %s 必須要設定 HTTPS ,才能在 iOS/macOS 上使用 CalDAV 和 CardDAV。",
- "Configures a CalDAV account" : "設定一個 CalDAV 帳號",
- "Configures a CardDAV account" : "設定一個 CardDAV 帳號",
- "Events" : "活動",
- "Tasks" : "工作項目",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "您的 %s 必須要設定採用 HTTPS ,才能在 iOS/macOS 上使用 CalDAV 和 CardDAV。",
+ "Configures a CalDAV account" : "設定 CalDAV 帳號",
+ "Configures a CardDAV account" : "設定 CardDAV 帳號",
+ "Events" : "行程",
"Untitled task" : "未命名工作項目",
"Completed on %s" : "完成於 %s",
"Due on %s by %s" : "到期於 %s 由 %s",
"Due on %s" : "到期於 %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "歡迎使用 Nextcloud 日曆!\n\n這是範例事件 - 使用 Nextcloud 日曆進行任何編輯,探索規劃的彈性!\n\n使用 Nextcloud 日曆,您可以:\n- 毫不費力地建立、編輯與管理活動。\n- 建立多個日曆,並與同事、朋友或家人分享。\n- 檢查可得性,並向他人顯示您的忙碌時間。\n- 透過 CalDAV 與應用程式與裝置無縫整合。\n- 自訂您的體驗:排定定期活動、調整通知與其他設定。",
+ "Example event - open me!" : "範例活動 - 開啟我!",
+ "System Address Book" : "系統通訊錄",
+ "The system address book contains contact information for all users in your instance." : "系統通訊錄包含您站台中的所有聯絡人資訊。",
+ "Enable System Address Book" : "啟用系統通訊錄",
+ "DAV system address book" : "DAV 系統通訊錄",
+ "No outstanding DAV system address book sync." : "沒有未完成的 DAV 系統通訊錄同步。",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV 系統通訊錄同步尚未執行,因為您的站台有超過 1000 位使用者,或是因為遇到錯誤。請透過呼叫「occ dav:sync-system-addressbook」手動執行同步。",
+ "WebDAV endpoint" : "WebDAV 端點",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "無法檢查您的網路伺服器是否已正確設定以允許透過 WebDAV 進行檔案同步。請手動檢查。",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "WebDAV 介面似乎為故障狀態,導致您的網頁伺服器無法提供檔案同步功能。",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "您的網路伺服器已正確設定為允許透過 WebDAV 進行檔案同步。",
"Migrated calendar (%1$s)" : "已導入的行事曆 (%1$s)",
- "Calendars including events, details and attendees" : "行事曆,包含事件、詳細資訊及參與者",
+ "Calendars including events, details and attendees" : "行事曆,包含行程、詳細資訊、參與者",
"Contacts and groups" : "聯絡人與群組",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV 端點",
- "Availability" : "可用性",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "若您設定了您的工作時間,其他使用者在預約會議時就會知道您何時不在辦公室。",
+ "Absence saved" : "不在時段已儲存",
+ "Failed to save your absence settings" : "儲存您的不在設定失敗",
+ "Absence cleared" : "不在時段已清除",
+ "Failed to clear your absence settings" : "清除您的不在設定失敗",
+ "First day" : "第一天",
+ "Last day (inclusive)" : "最後一天(含)",
+ "Out of office replacement (optional)" : "不在辦公室取代(選擇性)",
+ "Name of the replacement" : "取代名稱",
+ "No results." : "沒有結果。",
+ "Start typing." : "開始輸入。",
+ "Short absence status" : "暫時不在狀態",
+ "Long absence Message" : "長期不在訊息",
+ "Save" : "儲存",
+ "Disable absence" : "停用不在",
+ "Failed to load availability" : "空閒時間載入失敗",
+ "Saved availability" : "已儲存空閒時間",
+ "Failed to save availability" : "儲存空閒時間失敗",
"Time zone:" : "時區:",
"to" : "到",
- "Delete slot" : "刪除欄位",
+ "Delete slot" : "刪除時段",
"No working hours set" : "未設定工作時間",
- "Add slot" : "新增欄位",
- "Monday" : "週一",
- "Tuesday" : "週二",
- "Wednesday" : "週三",
- "Thursday" : "週四",
- "Friday" : "週五",
- "Saturday" : "週六",
- "Sunday" : "週日",
- "Save" : "儲存",
- "Calendar server" : "日曆伺服器",
- "Send invitations to attendees" : "發送邀請函給參加者",
- "Automatically generate a birthday calendar" : "自動生成生日日曆",
- "Birthday calendars will be generated by a background job." : "生日日曆將由背景作業生成。",
+ "Add slot" : "新增時段",
+ "Weekdays" : "週間",
+ "Pick a start time for {dayName}" : "為 {dayName} 挑選開始時間",
+ "Pick a end time for {dayName}" : "為 {dayName} 挑選結束時間",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "在空閒時間以外,自動將使用者狀態設定為「請勿打擾」以靜音所有通知。",
+ "Cancel" : "取消",
+ "Import" : "匯入",
+ "Error while saving settings" : "儲存設定時發生錯誤",
+ "Contact reset successfully" : "成功重設聯絡人",
+ "Error while resetting contact" : "重設聯絡人時發生錯誤",
+ "Contact imported successfully" : "成功匯入聯絡人",
+ "Error while importing contact" : "匯入聯絡人時發生錯誤",
+ "Import contact" : "匯入聯絡人",
+ "Reset to default" : "重設為預設值",
+ "Import contacts" : "匯入聯絡人",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "匯入新的 .vcf 檔案將會刪除現有的預設聯絡人並將其取代為新的聯絡人。您想要繼續嗎?",
+ "Failed to save example event creation setting" : "儲存範例事件建立設定失敗",
+ "Failed to upload the example event" : "上傳範例事件失敗",
+ "Custom example event was saved successfully" : "已成功儲存自訂範例事件",
+ "Failed to delete the custom example event" : "刪除自訂範例事件失敗",
+ "Custom example event was deleted successfully" : "已成功刪除自訂範例事件",
+ "Import calendar event" : "匯入日曆事件",
+ "Uploading a new event will overwrite the existing one." : "上傳新事件將會覆寫原有的",
+ "Upload event" : "上傳事件",
+ "Availability" : "空閒時間",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "若您設定了您的工作時間,其他使用者在預約會議時就會知道您何時不在辦公室。",
+ "Absence" : "不在",
+ "Configure your next absence period." : "設定您下一段不在的時間。",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "也可安裝{calendarappstoreopen}行事曆應用程式{linkclose},或{calendardocopen}連結您的電腦或行動裝置來同步 ↗{linkclose}。",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "請確定您有正確設定{emailopen}電子郵件伺服器{linkclose}。",
+ "Calendar server" : "行事曆伺服器",
+ "Send invitations to attendees" : "發送邀請函給參與者",
+ "Automatically generate a birthday calendar" : "自動生成生日行事曆",
+ "Birthday calendars will be generated by a background job." : "生日行事曆將由背景作業生成。",
"Hence they will not be available immediately after enabling but will show up after some time." : "因此,啟用後不會立即可用,但會在一段時間後顯示。",
- "Send notifications for events" : "傳送活動通知",
+ "Send notifications for events" : "傳送行程通知",
"Notifications are sent via background jobs, so these must occur often enough." : "通知會透過背景作業傳送,因此這些會經常發生。",
"Send reminder notifications to calendar sharees as well" : "也向共享行事曆傳送提醒通知",
- "Reminders are always sent to organizers and attendees." : "一律傳送提醒給舉辦者與參與者。",
- "Enable notifications for events via push" : "啟用推播活動通知",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "也安裝{calendarappstoreopen}日曆應用程式{linkclose},或{calendardocopen}連結您的電腦或行動裝置以供同步 ↗{linkclose}。",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "請確定您正確設定了{emailopen}電子郵件伺服器{linkclose}。",
+ "Reminders are always sent to organizers and attendees." : "一律傳送提醒給主辦者與參與者。",
+ "Enable notifications for events via push" : "啟用行程通知推播",
+ "Example content" : "範例內容",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "範例內容用來展示 Nextcloud 的功能。Nextcloud 隨附預設內容,可由自訂內容取代。",
"There was an error updating your attendance status." : "更新您的參與狀態時發生錯誤。",
- "Please contact the organizer directly." : "請直接聯絡主辦人。",
- "Are you accepting the invitation?" : "您接受邀請嗎?",
- "Tentative" : "暫定",
- "Number of guests" : "訪客數量",
- "Comment" : "留言",
- "Your attendance was updated successfully." : "您的參與狀態成功更新。",
- "Calendar and tasks" : "日曆與工作項目"
+ "Please contact the organizer directly." : "請直接聯絡主辦者。",
+ "Are you accepting the invitation?" : "您要接受邀請嗎?",
+ "Tentative" : "考慮接受",
+ "Your attendance was updated successfully." : "您的參與狀態已成功更新。"
},
"nplurals=1; plural=0;");
diff --git a/apps/dav/l10n/zh_TW.json b/apps/dav/l10n/zh_TW.json
index 29dd4133fc5..bfd58f4a453 100644
--- a/apps/dav/l10n/zh_TW.json
+++ b/apps/dav/l10n/zh_TW.json
@@ -1,86 +1,198 @@
{ "translations": {
- "Calendar" : "日曆",
- "Todos" : "待辦事項",
+ "Calendar" : "行事曆",
+ "Tasks" : "工作項目",
"Personal" : "個人",
- "{actor} created calendar {calendar}" : "{actor} 建立了日曆 {calendar}",
- "You created calendar {calendar}" : "您建立了日曆 {calendar}",
- "{actor} deleted calendar {calendar}" : "{actor} 刪除了日曆 {calendar}",
- "You deleted calendar {calendar}" : "您刪除了日曆 {calendar}",
- "{actor} updated calendar {calendar}" : "{actor} 更新了日曆 {calendar}",
- "You updated calendar {calendar}" : "您更新了日曆 {calendar}",
- "{actor} restored calendar {calendar}" : "{actor} 復原了日曆 {calendar}",
- "You restored calendar {calendar}" : "您復原了日曆 {calendar}",
- "You shared calendar {calendar} as public link" : "您將 {calendar} 日曆以公開連結分享",
+ "{actor} created calendar {calendar}" : "{actor} 建立了行事曆 {calendar}",
+ "You created calendar {calendar}" : "您建立了行事曆 {calendar}",
+ "{actor} deleted calendar {calendar}" : "{actor} 刪除了行事曆 {calendar}",
+ "You deleted calendar {calendar}" : "您刪除了行事曆 {calendar}",
+ "{actor} updated calendar {calendar}" : "{actor} 更新了行事曆 {calendar}",
+ "You updated calendar {calendar}" : "您更新了行事曆 {calendar}",
+ "{actor} restored calendar {calendar}" : "{actor} 復原了行事曆 {calendar}",
+ "You restored calendar {calendar}" : "您復原了行事曆 {calendar}",
+ "You shared calendar {calendar} as public link" : "您已將 {calendar} 行事曆以公開連結分享",
"You removed public link for calendar {calendar}" : "您刪除了 {calendar} 的公開分享連結",
- "{actor} shared calendar {calendar} with you" : "{actor} 與您分享了 {calendar} 日曆",
- "You shared calendar {calendar} with {user}" : "您與 {user} 分享了 {calendar} 日曆",
- "{actor} shared calendar {calendar} with {user}" : "{actor} 與 {user} 分享了日曆 {calendar}",
- "{actor} unshared calendar {calendar} from you" : "{actor} 停止與您分享日曆 {calendar}",
- "You unshared calendar {calendar} from {user}" : "您停止與 {user} 分享日曆 {calendar}",
- "{actor} unshared calendar {calendar} from {user}" : "{actor} 停止與 {user} 分享日曆 {calendar}",
- "{actor} unshared calendar {calendar} from themselves" : "{actor} 停止與他們自己分享日曆 {calendar}",
- "You shared calendar {calendar} with group {group}" : "您與群組 {group} 分享了日曆 {calendar}",
- "{actor} shared calendar {calendar} with group {group}" : "{actor} 與群組 {group} 分享了日曆 {calendar}",
- "You unshared calendar {calendar} from group {group}" : "您已停止與群組 {group} 分享日曆 {calendar}",
- "{actor} unshared calendar {calendar} from group {group}" : "{actor} 已停止與群組 {group} 分享日曆 {calendar}",
- "{actor} created event {event} in calendar {calendar}" : "{actor} 在日曆 {calendar} 中建立了 {event} 活動",
- "You created event {event} in calendar {calendar}" : "您在日曆 {calendar} 中建立了 {event} 活動",
- "{actor} deleted event {event} from calendar {calendar}" : "{actor} 在日曆 {calendar} 中刪除了 {event} 活動",
- "You deleted event {event} from calendar {calendar}" : "您在日曆 {calendar} 中刪除了 {event} 活動",
- "{actor} updated event {event} in calendar {calendar}" : "{actor} 在日曆 {calendar} 中更新了 {event} 活動",
- "You updated event {event} in calendar {calendar}" : "您在日曆 {calendar} 中刪除了 {event} 活動",
- "{actor} restored event {event} of calendar {calendar}" : "{actor} 復原了日曆 {calendar} 中的活動 {event}",
- "You restored event {event} of calendar {calendar}" : "您復原了日曆 {calendar} 中的活動 {event}",
+ "{actor} shared calendar {calendar} with you" : "{actor} 與您分享了 {calendar} 行事曆",
+ "You shared calendar {calendar} with {user}" : "您與 {user} 分享了 {calendar} 行事曆",
+ "{actor} shared calendar {calendar} with {user}" : "{actor} 與 {user} 分享了行事曆 {calendar}",
+ "{actor} unshared calendar {calendar} from you" : "{actor} 已停止與您分享行事曆 {calendar}",
+ "You unshared calendar {calendar} from {user}" : "您已停止與 {user} 分享行事曆 {calendar}",
+ "{actor} unshared calendar {calendar} from {user}" : "{actor} 已停止與 {user} 分享行事曆 {calendar}",
+ "{actor} unshared calendar {calendar} from themselves" : "{actor} 已停止與自己分享行事曆 {calendar}",
+ "You shared calendar {calendar} with group {group}" : "您已與群組 {group} 分享了行事曆 {calendar}",
+ "{actor} shared calendar {calendar} with group {group}" : "{actor} 已與群組 {group} 分享了行事曆 {calendar}",
+ "You unshared calendar {calendar} from group {group}" : "您已停止與群組 {group} 分享行事曆 {calendar}",
+ "{actor} unshared calendar {calendar} from group {group}" : "{actor} 已停止與群組 {group} 分享行事曆 {calendar}",
+ "Untitled event" : "未命名行程",
+ "{actor} created event {event} in calendar {calendar}" : "{actor} 在行事曆 {calendar} 中建立了行程 {event}",
+ "You created event {event} in calendar {calendar}" : "您在行事曆 {calendar} 中建立了行程 {event}",
+ "{actor} deleted event {event} from calendar {calendar}" : "{actor} 在行事曆 {calendar} 中刪除了行程 {event}",
+ "You deleted event {event} from calendar {calendar}" : "您在行事曆 {calendar} 中刪除了行程 {event}",
+ "{actor} updated event {event} in calendar {calendar}" : "{actor} 在行事曆 {calendar} 中更新了行程 {event}",
+ "You updated event {event} in calendar {calendar}" : "您在行事曆 {calendar} 中刪除了行程 {event}",
+ "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} 將行程 {event} 從行事曆 {sourceCalendar} 移動至行事曆 {targetCalendar}",
+ "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "您將行程 {event} 從行事曆 {sourceCalendar} 移動至行事曆 {targetCalendar}",
+ "{actor} restored event {event} of calendar {calendar}" : "{actor} 復原了行事曆 {calendar} 中的行程 {event}",
+ "You restored event {event} of calendar {calendar}" : "您復原了行事曆 {calendar} 中的行程 {event}",
"Busy" : "忙碌",
- "{actor} created todo {todo} in list {calendar}" : "{actor} 在列表 {calendar} 中建立了待辦事項 {todo}",
- "You created todo {todo} in list {calendar}" : "您在列表 {calendar} 中建立了待辦事項 {todo}",
- "{actor} deleted todo {todo} from list {calendar}" : "{actor} 在列表 {calendar} 中刪除了待辦事項 {todo}",
- "You deleted todo {todo} from list {calendar}" : "您在列表 {calendar} 中刪除了待辦事項 {todo}",
- "{actor} updated todo {todo} in list {calendar}" : "{actor} 在列表 {calendar} 中更新了待辦事項 {todo}",
- "You updated todo {todo} in list {calendar}" : "您在列表 {calendar} 中更新了待辦事項 {todo}",
- "{actor} solved todo {todo} in list {calendar}" : "{actor} 在列表 {calendar} 中解決了待辦事項 {todo}",
- "You solved todo {todo} in list {calendar}" : "您在列表 {calendar} 中解決了待辦事項 {todo}",
- "{actor} reopened todo {todo} in list {calendar}" : "{actor} 在列表 {calendar} 中重新開啟了待辦事項 {todo}",
- "You reopened todo {todo} in list {calendar}" : "您在列表 {calendar} 中重新開啟了待辦事項 {todo}",
- "Calendar, contacts and tasks" : "日曆、通訊錄與工作項目",
- "A <strong>calendar</strong> was modified" : "一個<strong>日曆</strong>被更動",
- "A calendar <strong>event</strong> was modified" : "一個日曆<strong>活動</strong>被更動",
- "A calendar <strong>todo</strong> was modified" : "一個日曆<strong>待辦事項</strong>被更動",
+ "{actor} created to-do {todo} in list {calendar}" : "{actor} 在清單 {calendar} 中建立了待辦事項 {todo}",
+ "You created to-do {todo} in list {calendar}" : "您在清單 {calendar} 中建立了待辦事項 {todo}",
+ "{actor} deleted to-do {todo} from list {calendar}" : "{actor} 從清單 {calendar} 中刪除了待辦事項 {todo}",
+ "You deleted to-do {todo} from list {calendar}" : "您從清單 {calendar} 中刪除了待辦事項 {todo}",
+ "{actor} updated to-do {todo} in list {calendar}" : "{actor} 在清單 {calendar} 中更新了待辦事項 {todo}",
+ "You updated to-do {todo} in list {calendar}" : "您在清單 {calendar} 中更新了待辦事項 {todo}",
+ "{actor} solved to-do {todo} in list {calendar}" : "{actor} 在清單 {calendar} 中解決了待辦事項 {todo}",
+ "You solved to-do {todo} in list {calendar}" : "您在清單 {calendar} 中解決了待辦事項 {todo}",
+ "{actor} reopened to-do {todo} in list {calendar}" : "{actor} 在清單 {calendar} 中重新開啟了待辦事項 {todo}",
+ "You reopened to-do {todo} in list {calendar}" : "您在清單 {calendar} 中重新開啟了待辦事項 {todo}",
+ "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} 將待辦事項 {todo} 從清單 {sourceCalendar} 移動到清單 {targetCalendar}",
+ "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "您將待辦事項 {todo} 從清單 {sourceCalendar} 移動到清單 {targetCalendar}",
+ "Calendar, contacts and tasks" : "行事曆、聯絡人與工作項目",
+ "A <strong>calendar</strong> was modified" : "一個<strong>行事曆</strong>被更動",
+ "A calendar <strong>event</strong> was modified" : "一個行事曆<strong>行程</strong>被更動",
+ "A calendar <strong>to-do</strong> was modified" : "已修改一個行事曆<strong>待辦事項</strong>",
"Contact birthdays" : "聯絡人生日",
"Death of %s" : "%s 逝世",
- "Calendar:" : "日曆:",
+ "Untitled calendar" : "未命名行事曆",
+ "Calendar:" : "行事曆:",
"Date:" : "日期:",
"Where:" : "地點:",
"Description:" : "描述:",
- "Untitled event" : "未命名活動",
- "_%n year_::_%n years_" : ["%n年"],
- "_%n month_::_%n months_" : ["%n月"],
- "_%n day_::_%n days_" : ["%n天"],
- "_%n hour_::_%n hours_" : ["%n小時"],
- "_%n minute_::_%n minutes_" : ["%n分鐘"],
- "%s (in %s)" : "%s(在 %s)",
- "%s (%s ago)" : "%s(%s 前)",
- "Calendar: %s" : "日曆:%s",
+ "_%n year_::_%n years_" : ["%n 年"],
+ "_%n month_::_%n months_" : ["%n 月"],
+ "_%n day_::_%n days_" : ["%n 天"],
+ "_%n hour_::_%n hours_" : ["%n 小時"],
+ "_%n minute_::_%n minutes_" : ["%n 分鐘"],
+ "%s (in %s)" : "%s(還有 %s)",
+ "%s (%s ago)" : "%s(%s之前)",
+ "Calendar: %s" : "行事曆:%s",
"Date: %s" : "日期:%s",
"Description: %s" : "描述:%s",
"Where: %s" : "地點:%s",
- "%1$s via %2$s" : "%1$s 由 %2$s",
+ "%1$s via %2$s" : "%1$s 經由 %2$s",
+ "In the past on %1$s for the entire day" : "在過去 %1$s 的整天",
+ "_In a minute on %1$s for the entire day_::_In %n minutes on %1$s for the entire day_" : ["在 %1$s 的整天內,%n 分鐘"],
+ "_In a hour on %1$s for the entire day_::_In %n hours on %1$s for the entire day_" : ["在 %1$s 的整天內,%n 小時"],
+ "_In a day on %1$s for the entire day_::_In %n days on %1$s for the entire day_" : ["在 %1$s 的整天內,%n 天"],
+ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["在 %1$s 的整天內,%n 週"],
+ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["在 %1$s 的整天內,%n 分鐘"],
+ "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["在 %1$s 的整天內,%n 年"],
+ "In the past on %1$s between %2$s - %3$s" : "在過去的 %1$s,在 %2$s - %3$s 之間",
+ "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["於 %1$s 的 %n 分鐘內,在 %2$s - %3$s 之間"],
+ "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["於 %1$s 的 %n 小時內,在 %2$s - %3$s 之間"],
+ "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["於 %1$s 的 %n 天內,在 %2$s - %3$s 之間"],
+ "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["於 %1$s 的 %n 週內,在 %2$s - %3$s 之間"],
+ "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["於 %1$s 的 %n 月內,在 %2$s - %3$s 之間"],
+ "_In a year on %1$s between %2$s - %3$s_::_In %n years on %1$s between %2$s - %3$s_" : ["於 %1$s 的 %n 年內,在 %2$s - %3$s 之間"],
+ "Could not generate when statement" : "無法產生 when 陳述",
+ "Every Day for the entire day" : "每天一整天",
+ "Every Day for the entire day until %1$s" : "每天一整天,直到 %1$s",
+ "Every Day between %1$s - %2$s" : "每天於 %1$s - %2$s 之間",
+ "Every Day between %1$s - %2$s until %3$s" : "每天於 %1$s - %2$s 之間,直到 %3$s",
+ "Every %1$d Days for the entire day" : "每 %1$d 天整天",
+ "Every %1$d Days for the entire day until %2$s" : "每 %1$d 天整天,直到 %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "每 %1$d 天於 %2$s - %3$s 之間",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "每 %1$d 天於 %2$s - %3$s 之間,直到 %4$s",
+ "Could not generate event recurrence statement" : "無法產生事件重複陳述",
+ "Every Week on %1$s for the entire day" : "每週於 %1$s 整天",
+ "Every Week on %1$s for the entire day until %2$s" : "每週於 %1$s 整天,直到 %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "每週於 %1$s,在 %2$s - %3$s 之間",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "每週於 %1$s,在 %2$s - %3$s 之間,直到 %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "每 %1$d 週於 %2$s 整天",
+ "Every %1$d Weeks on %2$s for the entire day until %3$s" : "每 %1$d 週於 %2$s 整天,直到 %3$s",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s" : "每 %1$d 週於 %2$s,在 %3$s - %4$s 之間",
+ "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "每 %1$d 週於 %2$s,在 %3$s - %4$s 之間,直到 %5$s",
+ "Every Month on the %1$s for the entire day" : "每個月於 %1$s 整天",
+ "Every Month on the %1$s for the entire day until %2$s" : "每個月於 %1$s 整天,直到 %2$s",
+ "Every Month on the %1$s between %2$s - %3$s" : "每個月於 %1$s,在 %2$s - %3$s 之間",
+ "Every Month on the %1$s between %2$s - %3$s until %4$s" : "每個月於 %1$s,在 %2$s - %3$s 之間,直到 %4$s",
+ "Every %1$d Months on the %2$s for the entire day" : "每 %1$d 個月於 %2$s 整天",
+ "Every %1$d Months on the %2$s for the entire day until %3$s" : "每 %1$d 個月於 %2$s 整天,直到 %3$s",
+ "Every %1$d Months on the %2$s between %3$s - %4$s" : "每 %1$d 個月於 %2$s,在 %3$s - %4$s 之間",
+ "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "每 %1$d 個月於 %2$s,在 %3$s - %4$s 之間,直到 %5$s",
+ "Every Year in %1$s on the %2$s for the entire day" : "每年於 %1$s 的 %2$s 整天",
+ "Every Year in %1$s on the %2$s for the entire day until %3$s" : "每年於 %1$s 的 %2$s 整天,直到 %3$s",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s" : "每年於 %1$s 的 %2$s,在 %3$s - %4$s 之間",
+ "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "每年於 %1$s 的 %2$s,在 %3$s - %4$s 之間,直到 %5$s",
+ "Every %1$d Years in %2$s on the %3$s for the entire day" : "每 %1$d 年於 %2$s 的 %3$s 整天",
+ "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "每 %1$d 年於 %2$s 的 %3$s 整天,直到 %4$s",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "每 %1$d 年於 %2$s 的 %3$s,在 %4$s - %5$s 之間",
+ "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "每 %1$d 年於 %2$s 的 %3$s,在 %4$s - %5$s 之間,直到 %6$s",
+ "On specific dates for the entire day until %1$s" : "在特定日期的整天,直到 %1$s",
+ "On specific dates between %1$s - %2$s until %3$s" : "在 %1$s - %2$s 之間的特定日期,直到 %3$s",
+ "In the past on %1$s" : "過去在 %1$s",
+ "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["將在 %n 分鐘之後於 %1$s 進行"],
+ "_In a hour on %1$s_::_In %n hours on %1$s_" : ["將在 %n 小時之後於 %1$s 進行"],
+ "_In a day on %1$s_::_In %n days on %1$s_" : ["將在 %n 天之後於 %1$s 進行"],
+ "_In a week on %1$s_::_In %n weeks on %1$s_" : ["將在 %n 週之後於 %1$s 進行"],
+ "_In a month on %1$s_::_In %n months on %1$s_" : ["將在 %n 個月之後於 %1$s 進行"],
+ "_In a year on %1$s_::_In %n years on %1$s_" : ["將在 %n 年之後於 %1$s 進行"],
+ "In the past on %1$s then on %2$s" : "在過去的 %1$s,然後在 %2$s",
+ "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["在 %1$s 分鐘內,然後在 %2$s"],
+ "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["在 %1$s 小時內,然後在 %2$s"],
+ "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["在 %1$s 天內,然後在 %2$s"],
+ "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["在 %1$s 週內,然後在 %2$s"],
+ "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["在 %1$s 月內,然後在 %2$s"],
+ "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["在 %1$s 年內,然後在 %2$s"],
+ "In the past on %1$s then on %2$s and %3$s" : "在過去的 %1$s,然後在 %2$s 與 %3$s",
+ "_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_" : ["於 %1$s 的 %n 分鐘內,然後在 %2$s 與 %3$s"],
+ "_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_" : ["於 %1$s 的 %n 小時內,然後在 %2$s 與 %3$s"],
+ "_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_" : ["於 %1$s 的 %n 天內,然後在 %2$s 與 %3$s"],
+ "_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_" : ["於 %1$s 的 %n 週內,然後在 %2$s 與 %3$s"],
+ "_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_" : ["於 %1$s 的 %n 月內,然後在 %2$s 與 %3$s"],
+ "_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_" : ["於 %1$s 的 %n 年內,然後在 %2$s 與 %3$s"],
+ "Could not generate next recurrence statement" : "無法產生下一個重複陳述",
"Cancelled: %1$s" : "已取消:%1$s",
- "Invitation canceled" : "邀請已取消",
+ "\"%1$s\" has been canceled" : "「%1$s」已取消",
"Re: %1$s" : "回覆:%1$s",
- "Invitation updated" : "邀請已更新",
+ "%1$s has accepted your invitation" : "%1$s 已接受您的邀請",
+ "%1$s has tentatively accepted your invitation" : "%1$s 考慮接受了您的邀請",
+ "%1$s has declined your invitation" : "%1$s 已婉拒您的邀請",
+ "%1$s has responded to your invitation" : "%1$s 已回應您的邀請",
+ "Invitation updated: %1$s" : "邀請已更新:%1$s",
+ "%1$s updated the event \"%2$s\"" : "%1$s 已更新行程:「%2$s」",
"Invitation: %1$s" : "邀請:%1$s",
- "Invitation" : "邀請",
+ "%1$s would like to invite you to \"%2$s\"" : "%1$s 想邀請您參與「%2$s」",
+ "Organizer:" : "主辦者:",
+ "Attendees:" : "參與者:",
"Title:" : "標題:",
- "Time:" : "時間:",
+ "When:" : "時間:",
"Location:" : "地點:",
"Link:" : "連結:",
- "Organizer:" : "組織者:",
- "Attendees:" : "參與者:",
+ "Occurring:" : "發生:",
"Accept" : "接受",
"Decline" : "拒絕",
- "More options …" : "更多選項……",
+ "More options …" : "更多選項…",
"More options at %s" : "%s 有更多選項",
+ "Monday" : "週一",
+ "Tuesday" : "週二",
+ "Wednesday" : "週三",
+ "Thursday" : "週四",
+ "Friday" : "週五",
+ "Saturday" : "週六",
+ "Sunday" : "週日",
+ "January" : "一月",
+ "February" : "二月",
+ "March" : "三月",
+ "April" : "四月",
+ "May" : "五月",
+ "June" : "六月",
+ "July" : "七月",
+ "August" : "八月",
+ "September" : "九月",
+ "October" : "十月",
+ "November" : "十一月",
+ "December" : "十二月",
+ "First" : "第一個",
+ "Second" : "第二",
+ "Third" : "第三",
+ "Fourth" : "第四",
+ "Fifth" : "第五",
+ "Last" : "最後",
+ "Second Last" : "倒數第二",
+ "Third Last" : "倒數第三",
+ "Fourth Last" : "倒數第四",
+ "Fifth Last" : "倒數第五",
"Contacts" : "聯絡人",
"{actor} created address book {addressbook}" : "{actor} 建立了通訊錄 {addressbook}",
"You created address book {addressbook}" : "您建立了通訊錄 {addressbook}",
@@ -91,88 +203,134 @@
"{actor} shared address book {addressbook} with you" : "{actor} 與您分享了通訊錄 {addressbook}",
"You shared address book {addressbook} with {user}" : "您與 {user} 分享了通訊錄 {addressbook}",
"{actor} shared address book {addressbook} with {user}" : "{actor} 與 {user} 分享了通訊錄 {addressbook}",
- "{actor} unshared address book {addressbook} from you" : "{actor} 取消與您分享通訊錄 {addressbook}",
- "You unshared address book {addressbook} from {user}" : "您取消與 {user} 分享的通訊錄 {addressbook}",
- "{actor} unshared address book {addressbook} from {user}" : "{actor} 取消與 {user} 分享通訊錄 {addressbook}",
- "{actor} unshared address book {addressbook} from themselves" : "{actor} 取消與他們自己分享的通訊錄 {addressbook}",
+ "{actor} unshared address book {addressbook} from you" : "{actor} 已取消與您分享通訊錄 {addressbook}",
+ "You unshared address book {addressbook} from {user}" : "您已取消與 {user} 分享通訊錄 {addressbook}",
+ "{actor} unshared address book {addressbook} from {user}" : "{actor} 已取消與 {user} 分享通訊錄 {addressbook}",
+ "{actor} unshared address book {addressbook} from themselves" : "{actor} 已取消與自己分享通訊錄 {addressbook}",
"You shared address book {addressbook} with group {group}" : "您與群組 {group} 分享了通訊錄 {addressbook}",
"{actor} shared address book {addressbook} with group {group}" : "{actor} 與群組 {group} 分享了通訊錄 {addressbook}",
- "You unshared address book {addressbook} from group {group}" : "您取消與群組 {group} 分享的通訊錄 {addressbook}",
- "{actor} unshared address book {addressbook} from group {group}" : "{actor} 取消與群組 {group} 分享的通訊錄 {addressbook}",
- "{actor} created contact {card} in address book {addressbook}" : "{actor} 在通訊錄 {addressbook} 中建立了聯絡人 {card}",
- "You created contact {card} in address book {addressbook}" : "您在通訊錄 {addressbook} 中建立了聯絡人 {card}",
+ "You unshared address book {addressbook} from group {group}" : "您已取消與群組 {group} 分享通訊錄 {addressbook}",
+ "{actor} unshared address book {addressbook} from group {group}" : "{actor} 已取消與群組 {group} 分享通訊錄 {addressbook}",
+ "{actor} created contact {card} in address book {addressbook}" : "{actor} 在通訊錄 {addressbook} 建立了聯絡人 {card}",
+ "You created contact {card} in address book {addressbook}" : "您在通訊錄 {addressbook} 建立了聯絡人 {card}",
"{actor} deleted contact {card} from address book {addressbook}" : "{actor} 從通訊錄 {addressbook} 刪除了聯絡人 {card}",
"You deleted contact {card} from address book {addressbook}" : "您從通訊錄 {addressbook} 刪除了聯絡人 {card}",
- "{actor} updated contact {card} in address book {addressbook}" : "{actor} 在通訊錄 {addressbook} 中更新了聯絡人 {card}",
- "You updated contact {card} in address book {addressbook}" : "您在通訊錄 {addressbook} 中更新了聯絡人 {card}",
- "A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>聯絡人</strong>或<strong>通訊錄</strong>已被修改",
+ "{actor} updated contact {card} in address book {addressbook}" : "{actor} 在通訊錄 {addressbook} 更新了聯絡人 {card}",
+ "You updated contact {card} in address book {addressbook}" : "您在通訊錄 {addressbook} 更新了聯絡人 {card}",
+ "A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>聯絡人</strong>或<strong>通訊錄</strong>已修改",
+ "Accounts" : "帳號",
+ "System address book which holds all accounts" : "包含所有帳號的系統通訊錄",
"File is not updatable: %1$s" : "檔案無法更新:%1$s",
- "Could not write to final file, canceled by hook" : "無法寫入最終檔案,被掛勾取消",
+ "Failed to get storage for file" : "無法取得檔案儲存空間",
+ "Could not write to final file, canceled by hook" : "無法寫入最終檔案,被連動取消",
"Could not write file contents" : "無法寫入檔案內容",
"_%n byte_::_%n bytes_" : ["%n 位元組"],
"Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "將檔案複製到目標位置時發生錯誤(已複製:%1$s,預期的檔案大小:%2$s)",
- "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." : "預期的檔案大小為 %1$s,但讀取(從 Nextcloud 客戶端)與寫入(至 Nextcloud 儲存空間)%2$s。可能是傳送端的網路問題或是伺服器端的儲存空間寫入問題。",
- "Could not rename part file to final file, canceled by hook" : "無法將部份檔案重新命名為最終檔案,被掛勾取消",
+ "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." : "預期的檔案大小為 %1$s,但讀取(從 Nextcloud 客戶端)與寫入(至 Nextcloud 儲存空間)%2$s。可能是傳送端的網路問題,或是伺服器端的儲存空間寫入問題。",
+ "Could not rename part file to final file, canceled by hook" : "無法將部份檔案重新命名為最終檔案,被連動取消",
"Could not rename part file to final file" : "無法將部份檔案重新命名為最終檔案",
"Failed to check file size: %1$s" : "檢查檔案大小失敗:%1$s",
- "Could not open file" : "無法開啟檔案",
+ "Could not open file: %1$s, file does seem to exist" : "無法開啟檔案:%1$s,檔案似乎存在",
+ "Could not open file: %1$s, file doesn't seem to exist" : "無法開啟檔案:%1$s,檔案似乎不存在",
"Encryption not ready: %1$s" : "尚未準備好加密:%1$s",
"Failed to open file: %1$s" : "開啟檔案失敗:%1$s",
"Failed to unlink: %1$s" : "解除連結失敗:%1$s",
- "Invalid chunk name" : "無效的區塊名稱",
- "Could not rename part file assembled from chunks" : "無法重新命名從區塊組合成的部份檔案",
"Failed to write file contents: %1$s" : "寫入檔案內容失敗:%1$s",
"File not found: %1$s" : "找不到檔案:%1$s",
+ "Invalid target path" : "無效目標路徑",
"System is in maintenance mode." : "系統處於維護模式。",
"Upgrade needed" : "需要升級",
- "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "您的 %s 必須要設定 HTTPS ,才能在 iOS/macOS 上使用 CalDAV 和 CardDAV。",
- "Configures a CalDAV account" : "設定一個 CalDAV 帳號",
- "Configures a CardDAV account" : "設定一個 CardDAV 帳號",
- "Events" : "活動",
- "Tasks" : "工作項目",
+ "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "您的 %s 必須要設定採用 HTTPS ,才能在 iOS/macOS 上使用 CalDAV 和 CardDAV。",
+ "Configures a CalDAV account" : "設定 CalDAV 帳號",
+ "Configures a CardDAV account" : "設定 CardDAV 帳號",
+ "Events" : "行程",
"Untitled task" : "未命名工作項目",
"Completed on %s" : "完成於 %s",
"Due on %s by %s" : "到期於 %s 由 %s",
"Due on %s" : "到期於 %s",
+ "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "歡迎使用 Nextcloud 日曆!\n\n這是範例事件 - 使用 Nextcloud 日曆進行任何編輯,探索規劃的彈性!\n\n使用 Nextcloud 日曆,您可以:\n- 毫不費力地建立、編輯與管理活動。\n- 建立多個日曆,並與同事、朋友或家人分享。\n- 檢查可得性,並向他人顯示您的忙碌時間。\n- 透過 CalDAV 與應用程式與裝置無縫整合。\n- 自訂您的體驗:排定定期活動、調整通知與其他設定。",
+ "Example event - open me!" : "範例活動 - 開啟我!",
+ "System Address Book" : "系統通訊錄",
+ "The system address book contains contact information for all users in your instance." : "系統通訊錄包含您站台中的所有聯絡人資訊。",
+ "Enable System Address Book" : "啟用系統通訊錄",
+ "DAV system address book" : "DAV 系統通訊錄",
+ "No outstanding DAV system address book sync." : "沒有未完成的 DAV 系統通訊錄同步。",
+ "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV 系統通訊錄同步尚未執行,因為您的站台有超過 1000 位使用者,或是因為遇到錯誤。請透過呼叫「occ dav:sync-system-addressbook」手動執行同步。",
+ "WebDAV endpoint" : "WebDAV 端點",
+ "Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "無法檢查您的網路伺服器是否已正確設定以允許透過 WebDAV 進行檔案同步。請手動檢查。",
+ "Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "WebDAV 介面似乎為故障狀態,導致您的網頁伺服器無法提供檔案同步功能。",
+ "Your web server is properly set up to allow file synchronization over WebDAV." : "您的網路伺服器已正確設定為允許透過 WebDAV 進行檔案同步。",
"Migrated calendar (%1$s)" : "已導入的行事曆 (%1$s)",
- "Calendars including events, details and attendees" : "行事曆,包含事件、詳細資訊及參與者",
+ "Calendars including events, details and attendees" : "行事曆,包含行程、詳細資訊、參與者",
"Contacts and groups" : "聯絡人與群組",
"WebDAV" : "WebDAV",
- "WebDAV endpoint" : "WebDAV 端點",
- "Availability" : "可用性",
- "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "若您設定了您的工作時間,其他使用者在預約會議時就會知道您何時不在辦公室。",
+ "Absence saved" : "不在時段已儲存",
+ "Failed to save your absence settings" : "儲存您的不在設定失敗",
+ "Absence cleared" : "不在時段已清除",
+ "Failed to clear your absence settings" : "清除您的不在設定失敗",
+ "First day" : "第一天",
+ "Last day (inclusive)" : "最後一天(含)",
+ "Out of office replacement (optional)" : "不在辦公室取代(選擇性)",
+ "Name of the replacement" : "取代名稱",
+ "No results." : "沒有結果。",
+ "Start typing." : "開始輸入。",
+ "Short absence status" : "暫時不在狀態",
+ "Long absence Message" : "長期不在訊息",
+ "Save" : "儲存",
+ "Disable absence" : "停用不在",
+ "Failed to load availability" : "空閒時間載入失敗",
+ "Saved availability" : "已儲存空閒時間",
+ "Failed to save availability" : "儲存空閒時間失敗",
"Time zone:" : "時區:",
"to" : "到",
- "Delete slot" : "刪除欄位",
+ "Delete slot" : "刪除時段",
"No working hours set" : "未設定工作時間",
- "Add slot" : "新增欄位",
- "Monday" : "週一",
- "Tuesday" : "週二",
- "Wednesday" : "週三",
- "Thursday" : "週四",
- "Friday" : "週五",
- "Saturday" : "週六",
- "Sunday" : "週日",
- "Save" : "儲存",
- "Calendar server" : "日曆伺服器",
- "Send invitations to attendees" : "發送邀請函給參加者",
- "Automatically generate a birthday calendar" : "自動生成生日日曆",
- "Birthday calendars will be generated by a background job." : "生日日曆將由背景作業生成。",
+ "Add slot" : "新增時段",
+ "Weekdays" : "週間",
+ "Pick a start time for {dayName}" : "為 {dayName} 挑選開始時間",
+ "Pick a end time for {dayName}" : "為 {dayName} 挑選結束時間",
+ "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "在空閒時間以外,自動將使用者狀態設定為「請勿打擾」以靜音所有通知。",
+ "Cancel" : "取消",
+ "Import" : "匯入",
+ "Error while saving settings" : "儲存設定時發生錯誤",
+ "Contact reset successfully" : "成功重設聯絡人",
+ "Error while resetting contact" : "重設聯絡人時發生錯誤",
+ "Contact imported successfully" : "成功匯入聯絡人",
+ "Error while importing contact" : "匯入聯絡人時發生錯誤",
+ "Import contact" : "匯入聯絡人",
+ "Reset to default" : "重設為預設值",
+ "Import contacts" : "匯入聯絡人",
+ "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : "匯入新的 .vcf 檔案將會刪除現有的預設聯絡人並將其取代為新的聯絡人。您想要繼續嗎?",
+ "Failed to save example event creation setting" : "儲存範例事件建立設定失敗",
+ "Failed to upload the example event" : "上傳範例事件失敗",
+ "Custom example event was saved successfully" : "已成功儲存自訂範例事件",
+ "Failed to delete the custom example event" : "刪除自訂範例事件失敗",
+ "Custom example event was deleted successfully" : "已成功刪除自訂範例事件",
+ "Import calendar event" : "匯入日曆事件",
+ "Uploading a new event will overwrite the existing one." : "上傳新事件將會覆寫原有的",
+ "Upload event" : "上傳事件",
+ "Availability" : "空閒時間",
+ "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "若您設定了您的工作時間,其他使用者在預約會議時就會知道您何時不在辦公室。",
+ "Absence" : "不在",
+ "Configure your next absence period." : "設定您下一段不在的時間。",
+ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "也可安裝{calendarappstoreopen}行事曆應用程式{linkclose},或{calendardocopen}連結您的電腦或行動裝置來同步 ↗{linkclose}。",
+ "Please make sure to properly set up {emailopen}the email server{linkclose}." : "請確定您有正確設定{emailopen}電子郵件伺服器{linkclose}。",
+ "Calendar server" : "行事曆伺服器",
+ "Send invitations to attendees" : "發送邀請函給參與者",
+ "Automatically generate a birthday calendar" : "自動生成生日行事曆",
+ "Birthday calendars will be generated by a background job." : "生日行事曆將由背景作業生成。",
"Hence they will not be available immediately after enabling but will show up after some time." : "因此,啟用後不會立即可用,但會在一段時間後顯示。",
- "Send notifications for events" : "傳送活動通知",
+ "Send notifications for events" : "傳送行程通知",
"Notifications are sent via background jobs, so these must occur often enough." : "通知會透過背景作業傳送,因此這些會經常發生。",
"Send reminder notifications to calendar sharees as well" : "也向共享行事曆傳送提醒通知",
- "Reminders are always sent to organizers and attendees." : "一律傳送提醒給舉辦者與參與者。",
- "Enable notifications for events via push" : "啟用推播活動通知",
- "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "也安裝{calendarappstoreopen}日曆應用程式{linkclose},或{calendardocopen}連結您的電腦或行動裝置以供同步 ↗{linkclose}。",
- "Please make sure to properly set up {emailopen}the email server{linkclose}." : "請確定您正確設定了{emailopen}電子郵件伺服器{linkclose}。",
+ "Reminders are always sent to organizers and attendees." : "一律傳送提醒給主辦者與參與者。",
+ "Enable notifications for events via push" : "啟用行程通知推播",
+ "Example content" : "範例內容",
+ "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "範例內容用來展示 Nextcloud 的功能。Nextcloud 隨附預設內容,可由自訂內容取代。",
"There was an error updating your attendance status." : "更新您的參與狀態時發生錯誤。",
- "Please contact the organizer directly." : "請直接聯絡主辦人。",
- "Are you accepting the invitation?" : "您接受邀請嗎?",
- "Tentative" : "暫定",
- "Number of guests" : "訪客數量",
- "Comment" : "留言",
- "Your attendance was updated successfully." : "您的參與狀態成功更新。",
- "Calendar and tasks" : "日曆與工作項目"
+ "Please contact the organizer directly." : "請直接聯絡主辦者。",
+ "Are you accepting the invitation?" : "您要接受邀請嗎?",
+ "Tentative" : "考慮接受",
+ "Your attendance was updated successfully." : "您的參與狀態已成功更新。"
},"pluralForm" :"nplurals=1; plural=0;"
} \ No newline at end of file
diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php
index 8c7f21698a1..9807b585080 100644
--- a/apps/dav/lib/AppInfo/Application.php
+++ b/apps/dav/lib/AppInfo/Application.php
@@ -3,54 +3,23 @@
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 Exception;
-use OCA\DAV\BackgroundJob\UpdateCalendarResourcesRoomsBackgroundJob;
-use OCA\DAV\CalDAV\Activity\Backend;
-use OCA\DAV\CalDAV\BirthdayService;
-use OCA\DAV\CalDAV\CalDavBackend;
+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\Backend as ReminderBackend;
use OCA\DAV\CalDAV\Reminder\NotificationProvider\AudioProvider;
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\CalDAV\WebcalCaching\RefreshWebcalService;
use OCA\DAV\Capabilities;
-use OCA\DAV\CardDAV\CardDavBackend;
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;
@@ -59,42 +28,77 @@ 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\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;
+use OCA\DAV\Events\CalendarUnpublishedEvent;
use OCA\DAV\Events\CalendarUpdatedEvent;
use OCA\DAV\Events\CardCreatedEvent;
use OCA\DAV\Events\CardDeletedEvent;
use OCA\DAV\Events\CardUpdatedEvent;
-use OCA\DAV\HookManager;
+use OCA\DAV\Events\SubscriptionCreatedEvent;
+use OCA\DAV\Events\SubscriptionDeletedEvent;
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;
use OCA\DAV\Listener\CalendarDeletionDefaultUpdaterListener;
use OCA\DAV\Listener\CalendarObjectReminderUpdaterListener;
+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\ILogger;
-use OCP\IServerContainer;
-use OCP\IUser;
+use OCP\DB\Events\AddMissingIndicesEvent;
+use OCP\Federation\Events\TrustedServerRemovedEvent;
+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 Symfony\Component\EventDispatcher\EventDispatcherInterface;
-use Symfony\Component\EventDispatcher\GenericEvent;
+use Psr\Log\LoggerInterface;
use Throwable;
use function is_null;
@@ -107,13 +111,10 @@ class Application extends App implements IBootstrap {
public function register(IRegistrationContext $context): void {
$context->registerServiceAlias('CardDAVSyncService', SyncService::class);
- $context->registerService(PhotoCache::class, function (ContainerInterface $c) {
- /** @var IServerContainer $server */
- $server = $c->get(IServerContainer::class);
-
- return new PhotoCache(
- $server->getAppDataDir('dav-photocache'),
- $c->get(ILogger::class)
+ $context->registerService(AppCalendarPlugin::class, function (ContainerInterface $c) {
+ return new AppCalendarPlugin(
+ $c->get(ICalendarManager::class),
+ $c->get(LoggerInterface::class)
);
});
@@ -132,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);
@@ -149,11 +152,19 @@ class Application extends App implements IBootstrap {
$context->registerEventListener(CalendarObjectUpdatedEvent::class, CalendarObjectReminderUpdaterListener::class);
$context->registerEventListener(CalendarObjectDeletedEvent::class, ActivityUpdaterListener::class);
$context->registerEventListener(CalendarObjectDeletedEvent::class, CalendarObjectReminderUpdaterListener::class);
+ $context->registerEventListener(CalendarObjectMovedEvent::class, ActivityUpdaterListener::class);
+ $context->registerEventListener(CalendarObjectMovedEvent::class, CalendarObjectReminderUpdaterListener::class);
$context->registerEventListener(CalendarObjectMovedToTrashEvent::class, ActivityUpdaterListener::class);
$context->registerEventListener(CalendarObjectMovedToTrashEvent::class, CalendarObjectReminderUpdaterListener::class);
$context->registerEventListener(CalendarObjectRestoredEvent::class, ActivityUpdaterListener::class);
$context->registerEventListener(CalendarObjectRestoredEvent::class, CalendarObjectReminderUpdaterListener::class);
$context->registerEventListener(CalendarShareUpdatedEvent::class, CalendarContactInteractionListener::class);
+ $context->registerEventListener(CalendarPublishedEvent::class, CalendarPublicationListener::class);
+ $context->registerEventListener(CalendarUnpublishedEvent::class, CalendarPublicationListener::class);
+ $context->registerEventListener(CalendarShareUpdatedEvent::class, CalendarShareUpdateListener::class);
+
+ $context->registerEventListener(SubscriptionCreatedEvent::class, SubscriptionListener::class);
+ $context->registerEventListener(SubscriptionDeletedEvent::class, SubscriptionListener::class);
$context->registerEventListener(AddressBookCreatedEvent::class, AddressbookListener::class);
@@ -163,179 +174,60 @@ class Application extends App implements IBootstrap {
$context->registerEventListener(CardCreatedEvent::class, CardListener::class);
$context->registerEventListener(CardDeletedEvent::class, CardListener::class);
$context->registerEventListener(CardUpdatedEvent::class, CardListener::class);
+ $context->registerEventListener(CardCreatedEvent::class, BirthdayListener::class);
+ $context->registerEventListener(CardDeletedEvent::class, BirthdayListener::class);
+ $context->registerEventListener(CardUpdatedEvent::class, BirthdayListener::class);
+ $context->registerEventListener(CardDeletedEvent::class, ClearPhotoCacheListener::class);
+ $context->registerEventListener(CardUpdatedEvent::class, ClearPhotoCacheListener::class);
+ $context->registerEventListener(TrustedServerRemovedEvent::class, TrustedServerRemovedListener::class);
+
+ $context->registerEventListener(BeforePreferenceDeletedEvent::class, UserPreferenceListener::class);
+ $context->registerEventListener(BeforePreferenceSetEvent::class, UserPreferenceListener::class);
+
+ $context->registerEventListener(OutOfOfficeChangedEvent::class, OutOfOfficeListener::class);
+ $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);
- }
-
- public function boot(IBootContext $context): void {
- // Load all dav apps
- \OC_App::loadApps(['dav']);
-
- $context->injectFn([$this, 'registerHooks']);
- $context->injectFn([$this, 'registerContactsManager']);
- $context->injectFn([$this, 'registerCalendarManager']);
- $context->injectFn([$this, 'registerCalendarReminders']);
- }
-
- public function registerHooks(HookManager $hm,
- EventDispatcherInterface $dispatcher,
- IAppContainer $container,
- IServerContainer $serverContainer) {
- $hm->setup();
- // first time login event setup
- $dispatcher->addListener(IUser::class . '::firstLogin', function ($event) use ($hm) {
- if ($event instanceof GenericEvent) {
- $hm->firstLogin($event->getSubject());
- }
- });
-
- $birthdayListener = function ($event) use ($container): void {
- if ($event instanceof GenericEvent) {
- /** @var BirthdayService $b */
- $b = $container->query(BirthdayService::class);
- $b->onCardChanged(
- (int) $event->getArgument('addressBookId'),
- (string) $event->getArgument('cardUri'),
- (string) $event->getArgument('cardData')
- );
- }
- };
-
- $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::createCard', $birthdayListener);
- $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::updateCard', $birthdayListener);
- $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::deleteCard', function ($event) use ($container) {
- if ($event instanceof GenericEvent) {
- /** @var BirthdayService $b */
- $b = $container->query(BirthdayService::class);
- $b->onCardDeleted(
- (int) $event->getArgument('addressBookId'),
- (string) $event->getArgument('cardUri')
- );
- }
- });
-
- $clearPhotoCache = function ($event) use ($container): void {
- if ($event instanceof GenericEvent) {
- /** @var PhotoCache $p */
- $p = $container->query(PhotoCache::class);
- $p->delete(
- $event->getArgument('addressBookId'),
- $event->getArgument('cardUri')
- );
- }
- };
- $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::updateCard', $clearPhotoCache);
- $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::deleteCard', $clearPhotoCache);
-
- $dispatcher->addListener('OC\AccountManager::userUpdated', function (GenericEvent $event) use ($container) {
- $user = $event->getSubject();
- /** @var SyncService $syncService */
- $syncService = $container->query(SyncService::class);
- $syncService->updateUser($user);
- });
+ $context->registerSetupCheck(NeedsSystemAddressBookSync::class);
+ $context->registerSetupCheck(WebdavEndpoint::class);
+ // register admin settings form and listener(s)
+ $context->registerDeclarativeSettings(SystemAddressBookSettings::class);
+ $context->registerEventListener(DeclarativeSettingsGetValueEvent::class, DavAdminSettingsListener::class);
+ $context->registerEventListener(DeclarativeSettingsSetValueEvent::class, DavAdminSettingsListener::class);
- $dispatcher->addListener('\OCA\DAV\CalDAV\CalDavBackend::updateShares', function (GenericEvent $event) use ($container) {
- /** @var Backend $backend */
- $backend = $container->query(Backend::class);
- $backend->onCalendarUpdateShares(
- $event->getArgument('calendarData'),
- $event->getArgument('shares'),
- $event->getArgument('add'),
- $event->getArgument('remove')
- );
-
- // Here we should recalculate if reminders should be sent to new or old sharees
- });
-
- $dispatcher->addListener('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', function (GenericEvent $event) use ($container) {
- /** @var Backend $backend */
- $backend = $container->query(Backend::class);
- $backend->onCalendarPublication(
- $event->getArgument('calendarData'),
- $event->getArgument('public')
- );
- });
-
-
- $dispatcher->addListener('OCP\Federation\TrustedServerEvent::remove',
- function (GenericEvent $event) {
- /** @var CardDavBackend $cardDavBackend */
- $cardDavBackend = \OC::$server->query(CardDavBackend::class);
- $addressBookUri = $event->getSubject();
- $addressBook = $cardDavBackend->getAddressBooksByUri('principals/system/system', $addressBookUri);
- if (!is_null($addressBook)) {
- $cardDavBackend->deleteAddressBook($addressBook['id']);
- }
- }
- );
-
- $dispatcher->addListener('\OCA\DAV\CalDAV\CalDavBackend::createSubscription',
- function (GenericEvent $event) use ($container, $serverContainer) {
- $jobList = $serverContainer->getJobList();
- $subscriptionData = $event->getArgument('subscriptionData');
-
- /**
- * Initial subscription refetch
- *
- * @var RefreshWebcalService $refreshWebcalService
- */
- $refreshWebcalService = $container->query(RefreshWebcalService::class);
- $refreshWebcalService->refreshSubscription(
- (string) $subscriptionData['principaluri'],
- (string) $subscriptionData['uri']
- );
-
- $jobList->add(\OCA\DAV\BackgroundJob\RefreshWebcalJob::class, [
- 'principaluri' => $subscriptionData['principaluri'],
- 'uri' => $subscriptionData['uri']
- ]);
- }
- );
-
- $dispatcher->addListener('\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription',
- function (GenericEvent $event) use ($container, $serverContainer) {
- $jobList = $serverContainer->getJobList();
- $subscriptionData = $event->getArgument('subscriptionData');
-
- $jobList->remove(\OCA\DAV\BackgroundJob\RefreshWebcalJob::class, [
- 'principaluri' => $subscriptionData['principaluri'],
- 'uri' => $subscriptionData['uri']
- ]);
-
- /** @var CalDavBackend $calDavBackend */
- $calDavBackend = $container->get(CalDavBackend::class);
- $calDavBackend->purgeAllCachedEventsForSubscription($subscriptionData['id']);
- /** @var ReminderBackend $calDavBackend */
- $reminderBackend = $container->get(ReminderBackend::class);
- $reminderBackend->cleanRemindersForCalendar((int) $subscriptionData['id']);
- }
- );
+ }
- $eventHandler = function () use ($container, $serverContainer): void {
- try {
- /** @var UpdateCalendarResourcesRoomsBackgroundJob $job */
- $job = $container->query(UpdateCalendarResourcesRoomsBackgroundJob::class);
- $job->run([]);
- $serverContainer->getJobList()->setLastRun($job);
- } catch (Exception $ex) {
- $serverContainer->getLogger()->logException($ex);
- }
- };
+ public function boot(IBootContext $context): void {
+ // Load all dav apps
+ $context->getServerContainer()->get(IAppManager::class)->loadApps(['dav']);
- $dispatcher->addListener('\OCP\Calendar\Resource\ForceRefreshEvent', $eventHandler);
- $dispatcher->addListener('\OCP\Calendar\Room\ForceRefreshEvent', $eventHandler);
+ $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 {
@@ -345,26 +237,25 @@ class Application extends App implements IBootstrap {
}
private function setupContactsProvider(IContactsManager $contactsManager,
- IAppContainer $container,
- string $userID): void {
+ IAppContainer $container,
+ string $userID): void {
/** @var ContactsManager $cm */
$cm = $container->query(ContactsManager::class);
$urlGenerator = $container->getServer()->getURLGenerator();
$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();
+ IAppContainer $container): void {
+ $calendarManager->register(function () use ($container, $calendarManager): void {
+ $user = Server::get(IUserSession::class)->getUser();
if ($user !== null) {
$this->setupCalendarProvider($calendarManager, $container, $user->getUID());
}
@@ -372,20 +263,20 @@ class Application extends App implements IBootstrap {
}
private function setupCalendarProvider(ICalendarManager $calendarManager,
- IAppContainer $container,
- $userId) {
+ IAppContainer $container,
+ $userId) {
$cm = $container->query(CalendarManager::class);
$cm->setupCalendarProvider($calendarManager, $userId);
}
public function registerCalendarReminders(NotificationProviderManager $manager,
- ILogger $logger): void {
+ LoggerInterface $logger): void {
try {
$manager->registerProvider(AudioProvider::class);
$manager->registerProvider(EmailProvider::class);
$manager->registerProvider(PushProvider::class);
} catch (Throwable $ex) {
- $logger->logException($ex);
+ $logger->error($ex->getMessage(), ['exception' => $ex]);
}
}
}
diff --git a/apps/dav/lib/AppInfo/PluginManager.php b/apps/dav/lib/AppInfo/PluginManager.php
index 0b83d6a9205..428547e3f61 100644
--- a/apps/dav/lib/AppInfo/PluginManager.php
+++ b/apps/dav/lib/AppInfo/PluginManager.php
@@ -3,32 +3,14 @@
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;
use OC\ServerContainer;
+use OCA\DAV\CalDAV\AppCalendar\AppCalendarPlugin;
use OCA\DAV\CalDAV\Integration\ICalendarProvider;
use OCA\DAV\CardDAV\Integration\IAddressBookProvider;
use OCP\App\IAppManager;
@@ -46,16 +28,6 @@ use function is_array;
class PluginManager {
/**
- * @var ServerContainer
- */
- private $container;
-
- /**
- * @var IAppManager
- */
- private $appManager;
-
- /**
* App plugins
*
* @var ServerPlugin[]
@@ -92,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,
+ ) {
}
/**
@@ -144,7 +117,9 @@ class PluginManager {
}
$this->populated = true;
- foreach ($this->appManager->getInstalledApps() as $app) {
+ $this->calendarPlugins[] = $this->container->get(AppCalendarPlugin::class);
+
+ 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)) {
@@ -253,7 +228,7 @@ class PluginManager {
private function createClass(string $className): object {
try {
- return $this->container->query($className);
+ return $this->container->get($className);
} catch (QueryException $e) {
if (class_exists($className)) {
return new $className();
diff --git a/apps/dav/lib/Avatars/AvatarHome.php b/apps/dav/lib/Avatars/AvatarHome.php
index ba52daeb2b3..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) {
@@ -59,8 +38,8 @@ class AvatarHome implements ICollection {
public function getChild($name) {
$elements = pathinfo($name);
- $ext = isset($elements['extension']) ? $elements['extension'] : '';
- $size = (int)(isset($elements['filename']) ? $elements['filename'] : '64');
+ $ext = $elements['extension'] ?? '';
+ $size = (int)($elements['filename'] ?? '64');
if (!in_array($ext, ['jpeg', 'png'], true)) {
throw new MethodNotAllowed('File format not allowed');
}
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 3d4e4dd5e6b..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;
@@ -32,7 +13,7 @@ use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
use OCP\BackgroundJob\QueuedJob;
use OCP\IDBConnection;
-use OCP\ILogger;
+use Psr\Log\LoggerInterface;
/**
* Class BuildReminderIndexBackgroundJob
@@ -41,51 +22,28 @@ use OCP\ILogger;
*/
class BuildReminderIndexBackgroundJob extends QueuedJob {
- /** @var IDBConnection */
- private $db;
-
- /** @var ReminderService */
- private $reminderService;
-
- /** @var ILogger */
- private $logger;
-
- /** @var IJobList */
- private $jobList;
-
/** @var ITimeFactory */
private $timeFactory;
/**
* BuildReminderIndexBackgroundJob constructor.
- *
- * @param IDBConnection $db
- * @param ReminderService $reminderService
- * @param ILogger $logger
- * @param IJobList $jobList
- * @param ITimeFactory $timeFactory
*/
- public function __construct(IDBConnection $db,
- ReminderService $reminderService,
- ILogger $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;
}
- /**
- * @param $arguments
- */
- public function run($arguments) {
- $offset = (int) $arguments['offset'];
- $stopAt = (int) $arguments['stopAt'];
+ public function run($argument) {
+ $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);
@@ -115,9 +73,9 @@ class BuildReminderIndexBackgroundJob extends QueuedJob {
->andWhere($query->expr()->gt('id', $query->createNamedParameter($offset)))
->orderBy('id', 'ASC');
- $stmt = $query->execute();
- while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
- $offset = $row['id'];
+ $result = $query->executeQuery();
+ while ($row = $result->fetch(\PDO::FETCH_ASSOC)) {
+ $offset = (int)$row['id'];
if (is_resource($row['calendardata'])) {
$row['calendardata'] = stream_get_contents($row['calendardata']);
}
@@ -126,14 +84,16 @@ class BuildReminderIndexBackgroundJob extends QueuedJob {
try {
$this->reminderService->onCalendarObjectCreate($row);
} catch (\Exception $ex) {
- $this->logger->logException($ex);
+ $this->logger->error($ex->getMessage(), ['exception' => $ex]);
}
if (($this->timeFactory->getTime() - $startTime) > 15) {
+ $result->closeCursor();
return $offset;
}
}
+ $result->closeCursor();
return $stopAt;
}
}
diff --git a/apps/dav/lib/BackgroundJob/CalendarRetentionJob.php b/apps/dav/lib/BackgroundJob/CalendarRetentionJob.php
index b57ed07d5c2..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 ab7dadd8c0b..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,12 +31,11 @@ class EventReminderJob extends TimedJob {
}
/**
- * @param $arg
- * @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($arg):void {
+ public function run($argument):void {
if ($this->config->getAppValue('dav', 'sendEventReminders', 'yes') !== 'yes') {
return;
}
diff --git a/apps/dav/lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php b/apps/dav/lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php
index a338a172d66..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,29 +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;
}
- /**
- * @param array $arguments
- */
- public function run($arguments) {
- $userId = $arguments['userId'];
- $purgeBeforeGenerating = $arguments['purgeBeforeGenerating'] ?? false;
+ 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
new file mode 100644
index 00000000000..cc4fd5dce9d
--- /dev/null
+++ b/apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php
@@ -0,0 +1,75 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\BackgroundJob;
+
+use OCA\DAV\CalDAV\TimezoneService;
+use OCA\DAV\Db\AbsenceMapper;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\QueuedJob;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IUserManager;
+use OCP\User\Events\OutOfOfficeEndedEvent;
+use OCP\User\Events\OutOfOfficeStartedEvent;
+use Psr\Log\LoggerInterface;
+
+class OutOfOfficeEventDispatcherJob extends QueuedJob {
+ public const EVENT_START = 'start';
+ public const EVENT_END = 'end';
+
+ public function __construct(
+ ITimeFactory $time,
+ private AbsenceMapper $absenceMapper,
+ private LoggerInterface $logger,
+ private IEventDispatcher $eventDispatcher,
+ private IUserManager $userManager,
+ private TimezoneService $timezoneService,
+ ) {
+ parent::__construct($time);
+ }
+
+ public function run($argument): void {
+ $id = $argument['id'];
+ $event = $argument['event'];
+
+ try {
+ $absence = $this->absenceMapper->findById($id);
+ } catch (DoesNotExistException|\OCP\DB\Exception $e) {
+ $this->logger->error('Failed to dispatch out-of-office event: ' . $e->getMessage(), [
+ 'exception' => $e,
+ 'argument' => $argument,
+ ]);
+ return;
+ }
+
+ $userId = $absence->getUserId();
+ $user = $this->userManager->get($userId);
+ if ($user === null) {
+ $this->logger->error("Failed to dispatch out-of-office event: User $userId does not exist", [
+ 'argument' => $argument,
+ ]);
+ return;
+ }
+
+ $data = $absence->toOutOufOfficeData(
+ $user,
+ $this->timezoneService->getUserTimezone($userId) ?? $this->timezoneService->getDefaultTimezone(),
+ );
+ if ($event === self::EVENT_START) {
+ $this->eventDispatcher->dispatchTyped(new OutOfOfficeStartedEvent($data));
+ } elseif ($event === self::EVENT_END) {
+ $this->eventDispatcher->dispatchTyped(new OutOfOfficeEndedEvent($data));
+ } else {
+ $this->logger->error("Invalid out-of-office event: $event", [
+ 'argument' => $argument,
+ ]);
+ }
+ }
+}
diff --git a/apps/dav/lib/BackgroundJob/PruneOutdatedSyncTokensJob.php b/apps/dav/lib/BackgroundJob/PruneOutdatedSyncTokensJob.php
new file mode 100644
index 00000000000..8746588acc7
--- /dev/null
+++ b/apps/dav/lib/BackgroundJob/PruneOutdatedSyncTokensJob.php
@@ -0,0 +1,45 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\BackgroundJob;
+
+use OCA\DAV\AppInfo\Application;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\TimedJob;
+use OCP\IConfig;
+use Psr\Log\LoggerInterface;
+
+class PruneOutdatedSyncTokensJob extends TimedJob {
+
+ public function __construct(
+ ITimeFactory $timeFactory,
+ private CalDavBackend $calDavBackend,
+ private CardDavBackend $cardDavBackend,
+ private IConfig $config,
+ private LoggerInterface $logger,
+ ) {
+ parent::__construct($timeFactory);
+ $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;
+
+ $prunedCalendarSyncTokens = $this->calDavBackend->pruneOutdatedSyncTokens($limit, $retention);
+ $prunedAddressBookSyncTokens = $this->cardDavBackend->pruneOutdatedSyncTokens($limit, $retention);
+
+ $this->logger->info('Pruned {calendarSyncTokensNumber} calendar sync tokens and {addressBooksSyncTokensNumber} address book sync tokens', [
+ 'calendarSyncTokensNumber' => $prunedCalendarSyncTokens,
+ 'addressBooksSyncTokensNumber' => $prunedAddressBookSyncTokens
+ ]);
+ }
+}
diff --git a/apps/dav/lib/BackgroundJob/RefreshWebcalJob.php b/apps/dav/lib/BackgroundJob/RefreshWebcalJob.php
index fbb944159fd..e96735ca50b 100644
--- a/apps/dav/lib/BackgroundJob/RefreshWebcalJob.php
+++ b/apps/dav/lib/BackgroundJob/RefreshWebcalJob.php
@@ -3,28 +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>
- * @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;
@@ -34,42 +14,18 @@ use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
use OCP\BackgroundJob\Job;
use OCP\IConfig;
-use OCP\ILogger;
+use Psr\Log\LoggerInterface;
use Sabre\VObject\DateTimeParser;
use Sabre\VObject\InvalidDataException;
class RefreshWebcalJob extends Job {
-
- /**
- * @var RefreshWebcalService
- */
- private $refreshWebcalService;
-
- /**
- * @var IConfig
- */
- private $config;
-
- /** @var ILogger */
- private $logger;
-
- /** @var ITimeFactory */
- private $timeFactory;
-
- /**
- * RefreshWebcalJob constructor.
- *
- * @param RefreshWebcalService $refreshWebcalService
- * @param IConfig $config
- * @param ILogger $logger
- * @param ITimeFactory $timeFactory
- */
- public function __construct(RefreshWebcalService $refreshWebcalService, IConfig $config, ILogger $logger, ITimeFactory $timeFactory) {
+ public function __construct(
+ private RefreshWebcalService $refreshWebcalService,
+ private IConfig $config,
+ private LoggerInterface $logger,
+ ITimeFactory $timeFactory,
+ ) {
parent::__construct($timeFactory);
- $this->refreshWebcalService = $refreshWebcalService;
- $this->config = $config;
- $this->logger = $logger;
- $this->timeFactory = $timeFactory;
}
/**
@@ -77,7 +33,7 @@ class RefreshWebcalJob extends Job {
*
* @inheritdoc
*/
- public function execute(IJobList $jobList, ILogger $logger = null) {
+ public function start(IJobList $jobList): void {
$subscription = $this->refreshWebcalService->getSubscription($this->argument['principaluri'], $this->argument['uri']);
if (!$subscription) {
return;
@@ -85,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'];
@@ -95,17 +51,19 @@ class RefreshWebcalJob extends Job {
/** @var DateInterval $dateInterval */
$dateInterval = DateTimeParser::parseDuration($refreshRate);
} catch (InvalidDataException $ex) {
- $this->logger->logException($ex);
- $this->logger->warning("Subscription $subscriptionId could not be refreshed, refreshrate in database is invalid");
+ $this->logger->error(
+ "Subscription $subscriptionId could not be refreshed, refreshrate in database is invalid",
+ ['exception' => $ex]
+ );
return;
}
$interval = $this->getIntervalFromDateInterval($dateInterval);
- if (($this->timeFactory->getTime() - $this->lastRun) <= $interval) {
+ if (($this->time->getTime() - $this->lastRun) <= $interval) {
return;
}
- parent::execute($jobList, $logger);
+ parent::start($jobList);
}
/**
@@ -146,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 85da81b3b91..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 f7addd58248..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);
- }
-
- /**
- * 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 76906becb54..230cde61578 100644
--- a/apps/dav/lib/BackgroundJob/UploadCleanup.php
+++ b/apps/dav/lib/BackgroundJob/UploadCleanup.php
@@ -3,56 +3,33 @@
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;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
+use Psr\Log\LoggerInterface;
class UploadCleanup extends TimedJob {
-
- /** @var IRootFolder */
- private $rootFolder;
-
- /** @var IJobList */
- private $jobList;
-
- public function __construct(ITimeFactory $time, IRootFolder $rootFolder, IJobList $jobList) {
+ public function __construct(
+ ITimeFactory $time,
+ private IRootFolder $rootFolder,
+ private IJobList $jobList,
+ private LoggerInterface $logger,
+ ) {
parent::__construct($time);
- $this->rootFolder = $rootFolder;
- $this->jobList = $jobList;
// Run once a day
$this->setInterval(60 * 60 * 24);
- $this->setTimeSensitivity(IJob::TIME_INSENSITIVE);
+ $this->setTimeSensitivity(self::TIME_INSENSITIVE);
}
protected function run($argument) {
@@ -64,18 +41,27 @@ class UploadCleanup extends TimedJob {
$userRoot = $userFolder->getParent();
/** @var Folder $uploads */
$uploads = $userRoot->get('uploads');
- /** @var Folder $uploadFolder */
$uploadFolder = $uploads->get($folder);
- } catch (NotFoundException | NoUserException $e) {
+ } catch (NotFoundException|NoUserException $e) {
$this->jobList->remove(self::class, $argument);
return;
}
- $files = $uploadFolder->getDirectoryListing();
-
// Remove if all files have an mtime of more than a day
$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);
+ if ($uploadFolder->getMTime() < $time) {
+ $uploadFolder->delete();
+ }
+ $this->jobList->remove(self::class, $argument);
+ return;
+ }
+
+ /** @var File[] $files */
+ $files = $uploadFolder->getDirectoryListing();
+
// The folder has to be more than a day old
$initial = $uploadFolder->getMTime() < $time;
diff --git a/apps/dav/lib/BackgroundJob/UserStatusAutomation.php b/apps/dav/lib/BackgroundJob/UserStatusAutomation.php
new file mode 100644
index 00000000000..027b3349802
--- /dev/null
+++ b/apps/dav/lib/BackgroundJob/UserStatusAutomation.php
@@ -0,0 +1,243 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\BackgroundJob;
+
+use OCA\DAV\CalDAV\Schedule\Plugin;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
+use OCP\BackgroundJob\TimedJob;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\User\IAvailabilityCoordinator;
+use OCP\User\IOutOfOfficeData;
+use OCP\UserStatus\IManager;
+use OCP\UserStatus\IUserStatus;
+use Psr\Log\LoggerInterface;
+use Sabre\VObject\Component\Available;
+use Sabre\VObject\Component\VAvailability;
+use Sabre\VObject\Reader;
+use Sabre\VObject\Recur\RRuleIterator;
+
+class UserStatusAutomation extends TimedJob {
+ 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,
+ ) {
+ parent::__construct($timeFactory);
+
+ // 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);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function run($argument) {
+ if (!isset($argument['userId'])) {
+ $this->jobList->remove(self::class, $argument);
+ $this->logger->info('Removing invalid ' . self::class . ' background job');
+ return;
+ }
+
+ $userId = $argument['userId'];
+ $user = $this->userManager->get($userId);
+ if ($user === null) {
+ return;
+ }
+
+ $ooo = $this->coordinator->getCurrentOutOfOfficeData($user);
+
+ $continue = $this->processOutOfOfficeData($user, $ooo);
+ if ($continue === false) {
+ return;
+ }
+
+ $property = $this->getAvailabilityFromPropertiesTable($userId);
+ $hasDndForOfficeHours = $this->config->getUserValue($userId, 'dav', 'user_status_automation', 'no') === 'yes';
+
+ if (!$property) {
+ // We found no ooo data and no availability settings, so we need to delete the job because there is no next runtime
+ $this->logger->info('Removing ' . self::class . ' background job for user "' . $userId . '" because the user has no valid availability rules and no OOO data set');
+ $this->jobList->remove(self::class, $argument);
+ $this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND);
+ $this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::DND);
+ return;
+ }
+
+ $this->processAvailability($property, $user->getUID(), $hasDndForOfficeHours);
+ }
+
+ protected function setLastRunToNextToggleTime(string $userId, int $timestamp): void {
+ $query = $this->connection->getQueryBuilder();
+
+ $query->update('jobs')
+ ->set('last_run', $query->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT))
+ ->where($query->expr()->eq('id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT)));
+ $query->executeStatement();
+
+ $this->logger->debug('Updated user status automation last_run to ' . $timestamp . ' for user ' . $userId);
+ }
+
+ /**
+ * @param string $userId
+ * @return false|string
+ */
+ protected function getAvailabilityFromPropertiesTable(string $userId) {
+ $propertyPath = 'calendars/' . $userId . '/inbox';
+ $propertyName = '{' . Plugin::NS_CALDAV . '}calendar-availability';
+
+ $query = $this->connection->getQueryBuilder();
+ $query->select('propertyvalue')
+ ->from('properties')
+ ->where($query->expr()->eq('userid', $query->createNamedParameter($userId)))
+ ->andWhere($query->expr()->eq('propertypath', $query->createNamedParameter($propertyPath)))
+ ->andWhere($query->expr()->eq('propertyname', $query->createNamedParameter($propertyName)))
+ ->setMaxResults(1);
+
+ $result = $query->executeQuery();
+ $property = $result->fetchOne();
+ $result->closeCursor();
+
+ return $property;
+ }
+
+ /**
+ * @param string $property
+ * @param $userId
+ * @param $argument
+ * @return void
+ */
+ private function processAvailability(string $property, string $userId, bool $hasDndForOfficeHours): void {
+ $isCurrentlyAvailable = false;
+ $nextPotentialToggles = [];
+
+ $now = $this->time->getDateTime();
+ $lastMidnight = (clone $now)->setTime(0, 0);
+
+ $vObject = Reader::read($property);
+ foreach ($vObject->getComponents() as $component) {
+ if ($component->name !== 'VAVAILABILITY') {
+ continue;
+ }
+ /** @var VAvailability $component */
+ $availables = $component->getComponents();
+ foreach ($availables as $available) {
+ /** @var Available $available */
+ if ($available->name === 'AVAILABLE') {
+ /** @var \DateTimeImmutable $originalStart */
+ /** @var \DateTimeImmutable $originalEnd */
+ [$originalStart, $originalEnd] = $available->getEffectiveStartEnd();
+
+ // Little shenanigans to fix the automation on the day the rules were adjusted
+ // Otherwise the $originalStart would match rules for Thursdays on a Friday, etc.
+ // So we simply wind back a week and then fastForward to the next occurrence
+ // since today's midnight, which then also accounts for the week days.
+ $effectiveStart = \DateTime::createFromImmutable($originalStart)->sub(new \DateInterval('P7D'));
+ $effectiveEnd = \DateTime::createFromImmutable($originalEnd)->sub(new \DateInterval('P7D'));
+
+ try {
+ $it = new RRuleIterator((string)$available->RRULE, $effectiveStart);
+ $it->fastForward($lastMidnight);
+
+ $startToday = $it->current();
+ if ($startToday && $startToday <= $now) {
+ $duration = $effectiveStart->diff($effectiveEnd);
+ $endToday = $startToday->add($duration);
+ if ($endToday > $now) {
+ // User is currently available
+ // Also queuing the end time as next status toggle
+ $isCurrentlyAvailable = true;
+ $nextPotentialToggles[] = $endToday->getTimestamp();
+ }
+
+ // Availability enabling already done for today,
+ // so jump to the next recurrence to find the next status toggle
+ $it->next();
+ }
+
+ if ($it->current()) {
+ $nextPotentialToggles[] = $it->current()->getTimestamp();
+ }
+ } catch (\Exception $e) {
+ $this->logger->error($e->getMessage(), ['exception' => $e]);
+ }
+ }
+ }
+ }
+
+ if (empty($nextPotentialToggles)) {
+ $this->logger->info('Removing ' . self::class . ' background job for user "' . $userId . '" because the user has no valid availability rules set');
+ $this->jobList->remove(self::class, ['userId' => $userId]);
+ $this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND);
+ return;
+ }
+
+ $nextAutomaticToggle = min($nextPotentialToggles);
+ $this->setLastRunToNextToggleTime($userId, $nextAutomaticToggle - 1);
+
+ if ($isCurrentlyAvailable) {
+ $this->logger->debug('User is currently available, reverting DND status if applicable');
+ $this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND);
+ $this->logger->debug('User status automation ran');
+ return;
+ }
+
+ 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 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)) {
+ // 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);
+ // We need to also run the availability automation
+ return true;
+ }
+
+ 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()) {
+ // 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
+ $this->setLastRunToNextToggleTime($user->getUID(), $ooo->getStartDate());
+ }
+ return true;
+ }
+
+ $this->logger->debug('User is currently in an OOO period, reverting other automated status and setting OOO DND status');
+ $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());
+ return false;
+ }
+}
diff --git a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php
index bb6baf48b56..d4faf3764e1 100644
--- a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php
+++ b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php
@@ -1,47 +1,27 @@
<?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;
+use OCA\DAV\Connector\Sabre\MtimeSanitizer;
+use OCP\AppFramework\Http;
+use OCP\Files\DavUtil;
+use OCP\Files\Folder;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
-use OCP\Files\Folder;
-use OCP\AppFramework\Http;
-use OCA\DAV\Connector\Sabre\MtimeSanitizer;
class BulkUploadPlugin extends ServerPlugin {
-
- /** @var Folder */
- private $userFolder;
-
- /** @var LoggerInterface */
- private $logger;
-
- public function __construct(Folder $userFolder, LoggerInterface $logger) {
- $this->userFolder = $userFolder;
- $this->logger = $logger;
+ public function __construct(
+ private Folder $userFolder,
+ private LoggerInterface $logger,
+ ) {
}
/**
@@ -60,11 +40,11 @@ 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;
}
- $multiPartParser = new MultipartRequestParser($request);
+ $multiPartParser = new MultipartRequestParser($request, $this->logger);
$writtenFiles = [];
while (!$multiPartParser->isAtLastBoundary()) {
@@ -74,7 +54,7 @@ class BulkUploadPlugin extends ServerPlugin {
// Return early if an error occurs during parsing.
$this->logger->error($e->getMessage());
$response->setStatus(Http::STATUS_BAD_REQUEST);
- $response->setBody(json_encode($writtenFiles));
+ $response->setBody(json_encode($writtenFiles, JSON_THROW_ON_ERROR));
return false;
}
@@ -90,22 +70,25 @@ class BulkUploadPlugin extends ServerPlugin {
$node = $this->userFolder->newFile($headers['x-file-path'], $content);
$node->touch($mtime);
+ $node = $this->userFolder->getFirstNodeById($node->getId());
$writtenFiles[$headers['x-file-path']] = [
- "error" => false,
- "etag" => $node->getETag(),
+ '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(),
];
}
}
$response->setStatus(Http::STATUS_OK);
- $response->setBody(json_encode($writtenFiles));
+ $response->setBody(json_encode($writtenFiles, JSON_THROW_ON_ERROR));
return false;
}
diff --git a/apps/dav/lib/BulkUpload/MultipartRequestParser.php b/apps/dav/lib/BulkUpload/MultipartRequestParser.php
index 7554447fc93..50f8cff76ba 100644
--- a/apps/dav/lib/BulkUpload/MultipartRequestParser.php
+++ b/apps/dav/lib/BulkUpload/MultipartRequestParser.php
@@ -1,32 +1,18 @@
<?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;
-use Sabre\HTTP\RequestInterface;
+use OCP\AppFramework\Http;
+use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\LengthRequired;
-use OCP\AppFramework\Http;
+use Sabre\HTTP\RequestInterface;
class MultipartRequestParser {
@@ -34,15 +20,18 @@ class MultipartRequestParser {
private $stream;
/** @var string */
- private $boundary = "";
+ private $boundary = '';
/** @var string */
- private $lastBoundary = "";
+ private $lastBoundary = '';
/**
* @throws BadRequest
*/
- public function __construct(RequestInterface $request) {
+ public function __construct(
+ RequestInterface $request,
+ protected LoggerInterface $logger,
+ ) {
$stream = $request->getBody();
$contentType = $request->getHeader('Content-Type');
@@ -51,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";
}
/**
@@ -69,16 +58,22 @@ 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);
// Remove potential quotes around boundary value.
- if (substr($boundaryValue, 0, 1) == '"' && substr($boundaryValue, -1) == '"') {
+ if (str_starts_with($boundaryValue, '"') && str_ends_with($boundaryValue, '"')) {
$boundaryValue = substr($boundaryValue, 1, -1);
}
@@ -108,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;
@@ -146,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];
}
@@ -158,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));
@@ -179,6 +177,11 @@ class MultipartRequestParser {
throw new Exception('An error occurred while reading headers of a part');
}
+ if (!str_contains($line, ':')) {
+ $this->logger->error('Header missing ":" on bulk request: ' . json_encode($line));
+ throw new Exception('An error occurred while reading headers of a part', Http::STATUS_BAD_REQUEST);
+ }
+
try {
[$key, $value] = explode(':', $line, 2);
$headers[strtolower(trim($key))] = trim($value);
@@ -187,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;
@@ -204,21 +208,19 @@ 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 {
+ $content = stream_get_line($this->stream, $length);
}
- $content = stream_get_line($this->stream, $length);
-
if ($content === false) {
throw new Exception("Fail to read part's content.");
}
- if (feof($this->stream)) {
- throw new Exception("Unexpected EOF while reading stream.");
+ if ($length !== 0 && feof($this->stream)) {
+ throw new Exception('Unexpected EOF while reading stream.');
}
// Read '\r\n'.
@@ -228,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 84ba50b8c37..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;
@@ -34,6 +15,7 @@ use OCP\App\IAppManager;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IUser;
+use OCP\IUserManager;
use OCP\IUserSession;
use Sabre\VObject\Reader;
@@ -44,23 +26,13 @@ use Sabre\VObject\Reader;
*/
class Backend {
- /** @var IActivityManager */
- protected $activityManager;
-
- /** @var IGroupManager */
- protected $groupManager;
-
- /** @var IUserSession */
- protected $userSession;
-
- /** @var IAppManager */
- protected $appManager;
-
- public function __construct(IActivityManager $activityManager, IGroupManager $groupManager, IUserSession $userSession, IAppManager $appManager) {
- $this->activityManager = $activityManager;
- $this->groupManager = $groupManager;
- $this->userSession = $userSession;
- $this->appManager = $appManager;
+ public function __construct(
+ protected IActivityManager $activityManager,
+ protected IGroupManager $groupManager,
+ protected IUserSession $userSession,
+ protected IAppManager $appManager,
+ protected IUserManager $userManager,
+ ) {
}
/**
@@ -119,7 +91,7 @@ class Backend {
* @param array $calendarData
* @param bool $publishStatus
*/
- public function onCalendarPublication(array $calendarData, $publishStatus) {
+ public function onCalendarPublication(array $calendarData, bool $publishStatus): void {
$this->triggerCalendarActivity($publishStatus ? Calendar::SUBJECT_PUBLISH : Calendar::SUBJECT_UNPUBLISH, $calendarData);
}
@@ -148,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);
@@ -165,13 +137,18 @@ class Backend {
}
foreach ($users as $user) {
+ if ($action === Calendar::SUBJECT_DELETE && !$this->userManager->userExists($user)) {
+ // Avoid creating calendar_delete activities for deleted users
+ continue;
+ }
+
$event->setAffectedUser($user)
->setSubject(
$user === $currentUser ? $action . '_self' : $action,
[
'actor' => $currentUser,
'calendar' => [
- 'id' => (int) $calendarData['id'],
+ 'id' => (int)$calendarData['id'],
'uri' => $calendarData['uri'],
'name' => $calendarData['{DAV:}displayname'],
],
@@ -202,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);
@@ -227,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'],
],
@@ -256,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'],
],
@@ -298,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'],
],
@@ -325,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'],
],
@@ -403,7 +380,7 @@ class Backend {
[
'actor' => $event->getAuthor(),
'calendar' => [
- 'id' => (int) $properties['id'],
+ 'id' => (int)$properties['id'],
'uri' => $properties['uri'],
'name' => $properties['{DAV:}displayname'],
],
@@ -438,17 +415,22 @@ class Backend {
$classification = $objectData['classification'] ?? CalDavBackend::CLASSIFICATION_PUBLIC;
$object = $this->getObjectNameAndType($objectData);
+
+ if (!$object) {
+ return;
+ }
+
$action = $action . '_' . $object['type'];
- if ($object['type'] === 'todo' && strpos($action, Event::SUBJECT_OBJECT_UPDATE) === 0 && $object['status'] === 'COMPLETED') {
+ if ($object['type'] === 'todo' && str_starts_with($action, Event::SUBJECT_OBJECT_UPDATE) && $object['status'] === 'COMPLETED') {
$action .= '_completed';
- } elseif ($object['type'] === 'todo' && strpos($action, Event::SUBJECT_OBJECT_UPDATE) === 0 && $object['status'] === 'NEEDS-ACTION') {
+ } elseif ($object['type'] === 'todo' && str_starts_with($action, Event::SUBJECT_OBJECT_UPDATE) && $object['status'] === 'NEEDS-ACTION') {
$action .= '_needs_action';
}
$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);
@@ -465,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'],
],
@@ -476,7 +458,7 @@ class Backend {
],
];
- if ($object['type'] === 'event' && strpos($action, Event::SUBJECT_OBJECT_DELETE) === false && $this->appManager->isEnabledForUser('calendar')) {
+ if ($object['type'] === 'event' && !str_contains($action, Event::SUBJECT_OBJECT_DELETE) && $this->appManager->isEnabledForUser('calendar')) {
$params['object']['link']['object_uri'] = $objectData['uri'];
$params['object']['link']['calendar_uri'] = $calendarData['uri'];
$params['object']['link']['owner'] = $owner;
@@ -494,8 +476,103 @@ class Backend {
}
/**
+ * Creates activities when a calendar object was moved
+ */
+ public function onMovedCalendarObject(array $sourceCalendarData, array $targetCalendarData, array $sourceShares, array $targetShares, array $objectData): void {
+ if (!isset($targetCalendarData['principaluri'])) {
+ return;
+ }
+
+ $sourcePrincipal = explode('/', $sourceCalendarData['principaluri']);
+ $sourceOwner = array_pop($sourcePrincipal);
+
+ $targetPrincipal = explode('/', $targetCalendarData['principaluri']);
+ $targetOwner = array_pop($targetPrincipal);
+
+ if ($sourceOwner !== $targetOwner) {
+ $this->onTouchCalendarObject(
+ Event::SUBJECT_OBJECT_DELETE,
+ $sourceCalendarData,
+ $sourceShares,
+ $objectData
+ );
+ $this->onTouchCalendarObject(
+ Event::SUBJECT_OBJECT_ADD,
+ $targetCalendarData,
+ $targetShares,
+ $objectData
+ );
+ return;
+ }
+
+ $currentUser = $this->userSession->getUser();
+ if ($currentUser instanceof IUser) {
+ $currentUser = $currentUser->getUID();
+ } else {
+ $currentUser = $targetOwner;
+ }
+
+ $classification = $objectData['classification'] ?? CalDavBackend::CLASSIFICATION_PUBLIC;
+ $object = $this->getObjectNameAndType($objectData);
+
+ if (!$object) {
+ return;
+ }
+
+ $event = $this->activityManager->generateEvent();
+ $event->setApp('dav')
+ ->setObject('calendar', (int)$targetCalendarData['id'])
+ ->setType($object['type'] === 'event' ? 'calendar_event' : 'calendar_todo')
+ ->setAuthor($currentUser);
+
+ $users = $this->getUsersForShares(array_intersect($sourceShares, $targetShares));
+ $users[] = $targetOwner;
+
+ // Users for share can return the owner itself if the calendar is published
+ foreach (array_unique($users) as $user) {
+ if ($classification === CalDavBackend::CLASSIFICATION_PRIVATE && $user !== $targetOwner) {
+ // Private events are only shown to the owner
+ continue;
+ }
+
+ $params = [
+ 'actor' => $event->getAuthor(),
+ 'sourceCalendar' => [
+ 'id' => (int)$sourceCalendarData['id'],
+ 'uri' => $sourceCalendarData['uri'],
+ 'name' => $sourceCalendarData['{DAV:}displayname'],
+ ],
+ 'targetCalendar' => [
+ 'id' => (int)$targetCalendarData['id'],
+ 'uri' => $targetCalendarData['uri'],
+ 'name' => $targetCalendarData['{DAV:}displayname'],
+ ],
+ 'object' => [
+ 'id' => $object['id'],
+ 'name' => $classification === CalDavBackend::CLASSIFICATION_CONFIDENTIAL && $user !== $targetOwner ? 'Busy' : $object['name'],
+ 'classified' => $classification === CalDavBackend::CLASSIFICATION_CONFIDENTIAL && $user !== $targetOwner,
+ ],
+ ];
+
+ if ($object['type'] === 'event' && $this->appManager->isEnabledForUser('calendar')) {
+ $params['object']['link']['object_uri'] = $objectData['uri'];
+ $params['object']['link']['calendar_uri'] = $targetCalendarData['uri'];
+ $params['object']['link']['owner'] = $targetOwner;
+ }
+
+ $event->setAffectedUser($user)
+ ->setSubject(
+ $user === $currentUser ? Event::SUBJECT_OBJECT_MOVE . '_' . $object['type'] . '_self' : Event::SUBJECT_OBJECT_MOVE . '_' . $object['type'],
+ $params
+ );
+
+ $this->activityManager->publish($event);
+ }
+ }
+
+ /**
* @param array $objectData
- * @return string[]|bool
+ * @return string[]|false
*/
protected function getObjectNameAndType(array $objectData) {
$vObject = Reader::read($objectData['calendardata']);
@@ -513,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 f727c10befe..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,
+ ) {
}
/**
@@ -52,13 +31,13 @@ class Todo implements IFilter {
* @since 11.0.0
*/
public function getName() {
- return $this->l->t('Todos');
+ return $this->l->t('Tasks');
}
/**
* @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 7f70980a72b..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;
@@ -30,51 +13,26 @@ use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IURLGenerator;
-use OCP\IUser;
use OCP\IUserManager;
abstract class Base implements IProvider {
-
- /** @var IUserManager */
- protected $userManager;
-
- /** @var string[] */
- protected $userDisplayNames = [];
-
- /** @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,
+ ) {
}
- /**
- * @param IEvent $event
- * @param string $subject
- * @param array $parameters
- */
- protected function setSubjects(IEvent $event, $subject, array $parameters) {
- $placeholders = $replacements = [];
- foreach ($parameters as $placeholder => $parameter) {
- $placeholders[] = '{' . $placeholder . '}';
- $replacements[] = $parameter['name'];
- }
-
- $event->setParsedSubject(str_replace($placeholders, $replacements, $subject))
- ->setRichSubject($subject, $parameters);
+ protected function setSubjects(IEvent $event, string $subject, array $parameters): void {
+ $event->setRichSubject($subject, $parameters);
}
/**
@@ -83,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'],
];
}
@@ -107,40 +65,20 @@ abstract class Base implements IProvider {
protected function generateLegacyCalendarParameter($id, $name) {
return [
'type' => 'calendar',
- 'id' => $id,
+ 'id' => (string)$id,
'name' => $name,
];
}
- /**
- * @param string $uid
- * @return array
- */
- protected function generateUserParameter($uid) {
- if (!isset($this->userDisplayNames[$uid])) {
- $this->userDisplayNames[$uid] = $this->getUserDisplayName($uid);
- }
-
+ protected function generateUserParameter(string $uid): array {
return [
'type' => 'user',
'id' => $uid,
- 'name' => $this->userDisplayNames[$uid],
+ 'name' => $this->userManager->getDisplayName($uid) ?? $uid,
];
}
/**
- * @param string $uid
- * @return string
- */
- protected function getUserDisplayName($uid) {
- $user = $this->userManager->get($uid);
- if ($user instanceof IUser) {
- return $user->getDisplayName();
- }
- return $uid;
- }
-
- /**
* @param string $gid
* @return array
*/
diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php b/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php
index daab7806e46..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) {
+ 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 96366f54942..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;
@@ -40,25 +20,14 @@ use OCP\L10N\IFactory;
class Event extends Base {
public const SUBJECT_OBJECT_ADD = 'object_add';
public const SUBJECT_OBJECT_UPDATE = 'object_update';
+ public const SUBJECT_OBJECT_MOVE = 'object_move';
public const SUBJECT_OBJECT_MOVE_TO_TRASH = 'object_move_to_trash';
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
@@ -68,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();
}
@@ -88,23 +61,27 @@ class Event extends Base {
$params = [
'type' => 'calendar-event',
'id' => $eventData['id'],
- 'name' => $eventData['name'],
-
+ 'name' => trim($eventData['name']) !== '' ? $eventData['name'] : $this->l->t('Untitled event'),
];
+
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('/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
}
@@ -117,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) {
+ 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);
@@ -145,6 +122,10 @@ class Event extends Base {
$subject = $this->l->t('{actor} updated event {event} in calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_event_self') {
$subject = $this->l->t('You updated event {event} in calendar {calendar}');
+ } elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE . '_event') {
+ $subject = $this->l->t('{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}');
+ } elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE . '_event_self') {
+ $subject = $this->l->t('You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE_TO_TRASH . '_event') {
$subject = $this->l->t('{actor} deleted event {event} from calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE_TO_TRASH . '_event_self') {
@@ -154,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);
@@ -184,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':
@@ -193,7 +174,25 @@ 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()),
+ ];
+ }
+ }
+
+ if (isset($parameters['sourceCalendar']) && isset($parameters['targetCalendar'])) {
+ switch ($subject) {
+ case self::SUBJECT_OBJECT_MOVE . '_event':
+ return [
+ '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->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->getAffectedUser()),
];
}
}
@@ -210,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 a3ab81e38ae..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) {
+ 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);
@@ -50,27 +33,31 @@ class Todo extends Event {
}
if ($event->getSubject() === self::SUBJECT_OBJECT_ADD . '_todo') {
- $subject = $this->l->t('{actor} created todo {todo} in list {calendar}');
+ $subject = $this->l->t('{actor} created to-do {todo} in list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_ADD . '_todo_self') {
- $subject = $this->l->t('You created todo {todo} in list {calendar}');
+ $subject = $this->l->t('You created to-do {todo} in list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_DELETE . '_todo') {
- $subject = $this->l->t('{actor} deleted todo {todo} from list {calendar}');
+ $subject = $this->l->t('{actor} deleted to-do {todo} from list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_DELETE . '_todo_self') {
- $subject = $this->l->t('You deleted todo {todo} from list {calendar}');
+ $subject = $this->l->t('You deleted to-do {todo} from list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo') {
- $subject = $this->l->t('{actor} updated todo {todo} in list {calendar}');
+ $subject = $this->l->t('{actor} updated to-do {todo} in list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo_self') {
- $subject = $this->l->t('You updated todo {todo} in list {calendar}');
+ $subject = $this->l->t('You updated to-do {todo} in list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo_completed') {
- $subject = $this->l->t('{actor} solved todo {todo} in list {calendar}');
+ $subject = $this->l->t('{actor} solved to-do {todo} in list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo_completed_self') {
- $subject = $this->l->t('You solved todo {todo} in list {calendar}');
+ $subject = $this->l->t('You solved to-do {todo} in list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action') {
- $subject = $this->l->t('{actor} reopened todo {todo} in list {calendar}');
+ $subject = $this->l->t('{actor} reopened to-do {todo} in list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action_self') {
- $subject = $this->l->t('You reopened todo {todo} in list {calendar}');
+ $subject = $this->l->t('You reopened to-do {todo} in list {calendar}');
+ } elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE . '_todo') {
+ $subject = $this->l->t('{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}');
+ } 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);
@@ -100,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':
@@ -109,7 +96,25 @@ 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()),
+ ];
+ }
+ }
+
+ if (isset($parameters['sourceCalendar']) && isset($parameters['targetCalendar'])) {
+ switch ($subject) {
+ case self::SUBJECT_OBJECT_MOVE . '_todo':
+ return [
+ 'actor' => $this->generateUserParameter($parameters['actor']),
+ 'sourceCalendar' => $this->generateCalendarParameter($parameters['sourceCalendar'], $this->l),
+ 'targetCalendar' => $this->generateCalendarParameter($parameters['targetCalendar'], $this->l),
+ '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'], $event->getAffectedUser()),
];
}
}
@@ -128,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':
@@ -137,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 7d27b30c4af..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;
@@ -38,13 +21,13 @@ class Todo extends CalDAVSetting {
* @since 11.0.0
*/
public function getName() {
- return $this->l->t('A calendar <strong>todo</strong> was modified');
+ return $this->l->t('A calendar <strong>to-do</strong> was modified');
}
/**
* @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
new file mode 100644
index 00000000000..87d26324c32
--- /dev/null
+++ b/apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php
@@ -0,0 +1,194 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * 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\Integration\ExternalCalendar;
+use OCA\DAV\CalDAV\Plugin;
+use OCP\Calendar\ICalendar;
+use OCP\Calendar\ICreateFromString;
+use OCP\Constants;
+use Sabre\CalDAV\CalendarQueryValidator;
+use Sabre\CalDAV\ICalendarObject;
+use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\PropPatch;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Reader;
+
+class AppCalendar extends ExternalCalendar {
+ protected ICalendar $calendar;
+
+ public function __construct(
+ string $appId,
+ ICalendar $calendar,
+ protected string $principal,
+ ) {
+ parent::__construct($appId, $calendar->getUri());
+ $this->calendar = $calendar;
+ }
+
+ /**
+ * Return permissions supported by the backend calendar
+ * @return int Permissions based on \OCP\Constants
+ */
+ public function getPermissions(): int {
+ // Make sure to only promote write support if the backend implement the correct interface
+ if ($this->calendar instanceof ICreateFromString) {
+ return $this->calendar->getPermissions();
+ }
+ return Constants::PERMISSION_READ;
+ }
+
+ public function getOwner(): ?string {
+ return $this->principal;
+ }
+
+ public function getGroup(): ?string {
+ return null;
+ }
+
+ public function getACL(): array {
+ $acl = [
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}write-properties',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ]
+ ];
+ if ($this->getPermissions() & Constants::PERMISSION_CREATE) {
+ $acl[] = [
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ];
+ }
+ return $acl;
+ }
+
+ public function setACL(array $acl): void {
+ throw new Forbidden('Setting ACL is not supported on this node');
+ }
+
+ public function getSupportedPrivilegeSet(): ?array {
+ // Use the default one
+ return null;
+ }
+
+ public function getLastModified(): ?int {
+ // unknown
+ return null;
+ }
+
+ public function delete(): void {
+ // No method for deleting a calendar in OCP\Calendar\ICalendar
+ throw new Forbidden('Deleting an entry is not implemented');
+ }
+
+ public function createFile($name, $data = null) {
+ if ($this->calendar instanceof ICreateFromString) {
+ if (is_resource($data)) {
+ $data = stream_get_contents($data) ?: null;
+ }
+ $this->calendar->createFromString($name, is_null($data) ? '' : $data);
+ return null;
+ } else {
+ throw new Forbidden('Creating a new entry is not allowed');
+ }
+ }
+
+ public function getProperties($properties) {
+ return [
+ '{DAV:}displayname' => $this->calendar->getDisplayName() ?: $this->calendar->getKey(),
+ '{http://apple.com/ns/ical/}calendar-color' => $this->calendar->getDisplayColor() ?: '#0082c9',
+ '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VEVENT', 'VJOURNAL', 'VTODO']),
+ ];
+ }
+
+ public function calendarQuery(array $filters) {
+ $result = [];
+ $objects = $this->getChildren();
+
+ foreach ($objects as $object) {
+ if ($this->validateFilterForObject($object, $filters)) {
+ $result[] = $object->getName();
+ }
+ }
+
+ return $result;
+ }
+
+ protected function validateFilterForObject(ICalendarObject $object, array $filters): bool {
+ /** @var \Sabre\VObject\Component\VCalendar */
+ $vObject = Reader::read($object->get());
+
+ $validator = new CalendarQueryValidator();
+ $result = $validator->validate($vObject, $filters);
+
+ // Destroy circular references so PHP will GC the object.
+ $vObject->destroy();
+
+ return $result;
+ }
+
+ public function childExists($name): bool {
+ try {
+ $this->getChild($name);
+ return true;
+ } catch (NotFound $error) {
+ return false;
+ }
+ }
+
+ public function getChild($name) {
+ // Try to get calendar by filename
+ $children = $this->calendar->search($name, ['X-FILENAME']);
+ if (count($children) === 0) {
+ // If nothing found try to get by UID from filename
+ $pos = strrpos($name, '.ics');
+ $children = $this->calendar->search(substr($name, 0, $pos ?: null), ['UID']);
+ }
+
+ if (count($children) > 0) {
+ return new CalendarObject($this, $this->calendar, new VCalendar($children));
+ }
+
+ throw new NotFound('Node not found');
+ }
+
+ /**
+ * @return ICalendarObject[]
+ */
+ public function getChildren(): array {
+ $objects = $this->calendar->search('');
+ // We need to group by UID (actually by filename but we do not have that information)
+ $result = [];
+ foreach ($objects as $object) {
+ $uid = (string)$object['UID'] ?: uniqid();
+ if (!isset($result[$uid])) {
+ $result[$uid] = [];
+ }
+ $result[$uid][] = $object;
+ }
+
+ return array_map(function (array $children) {
+ return new CalendarObject($this, $this->calendar, new VCalendar($children));
+ }, $result);
+ }
+
+ public function propPatch(PropPatch $propPatch): void {
+ // no setDisplayColor or setDisplayName in \OCP\Calendar\ICalendar
+ }
+}
diff --git a/apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php b/apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php
new file mode 100644
index 00000000000..72f2ed2c163
--- /dev/null
+++ b/apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php
@@ -0,0 +1,58 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * 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;
+use Psr\Log\LoggerInterface;
+
+/* Plugin for wrapping application generated calendars registered in nextcloud core (OCP\Calendar\ICalendarProvider) */
+class AppCalendarPlugin implements ICalendarProvider {
+ public function __construct(
+ protected IManager $manager,
+ protected LoggerInterface $logger,
+ ) {
+ }
+
+ public function getAppID(): string {
+ return 'dav-wrapper';
+ }
+
+ public function fetchAllForCalendarHome(string $principalUri): array {
+ return array_map(function ($calendar) use (&$principalUri) {
+ return new AppCalendar($this->getAppID(), $calendar, $principalUri);
+ }, $this->getWrappedCalendars($principalUri));
+ }
+
+ public function hasCalendarInCalendarHome(string $principalUri, string $calendarUri): bool {
+ return count($this->getWrappedCalendars($principalUri, [ $calendarUri ])) > 0;
+ }
+
+ public function getCalendarInCalendarHome(string $principalUri, string $calendarUri): ?ExternalCalendar {
+ $calendars = $this->getWrappedCalendars($principalUri, [ $calendarUri ]);
+ if (count($calendars) > 0) {
+ return new AppCalendar($this->getAppID(), $calendars[0], $principalUri);
+ }
+
+ return null;
+ }
+
+ protected function getWrappedCalendars(string $principalUri, array $calendarUris = []): array {
+ return array_values(
+ array_filter($this->manager->getCalendarsForPrincipal($principalUri, $calendarUris), function ($c) {
+ // We must not provide a wrapper for DAV calendars
+ 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
new file mode 100644
index 00000000000..3c62a26df54
--- /dev/null
+++ b/apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php
@@ -0,0 +1,134 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV\AppCalendar;
+
+use OCP\Calendar\ICalendar;
+use OCP\Calendar\ICreateFromString;
+use OCP\Constants;
+use Sabre\CalDAV\ICalendarObject;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAVACL\IACL;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Property\ICalendar\DateTime;
+
+class CalendarObject implements ICalendarObject, IACL {
+ public function __construct(
+ private AppCalendar $calendar,
+ private ICalendar|ICreateFromString $backend,
+ private VCalendar $vobject,
+ ) {
+ }
+
+ public function getOwner() {
+ return $this->calendar->getOwner();
+ }
+
+ public function getGroup() {
+ return $this->calendar->getGroup();
+ }
+
+ public function getACL(): array {
+ $acl = [
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ]
+ ];
+ if ($this->calendar->getPermissions() & Constants::PERMISSION_UPDATE) {
+ $acl[] = [
+ 'privilege' => '{DAV:}write-content',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ];
+ }
+ return $acl;
+ }
+
+ public function setACL(array $acl): void {
+ throw new Forbidden('Setting ACL is not supported on this node');
+ }
+
+ public function getSupportedPrivilegeSet(): ?array {
+ return null;
+ }
+
+ public function put($data): void {
+ if ($this->backend instanceof ICreateFromString && $this->calendar->getPermissions() & Constants::PERMISSION_UPDATE) {
+ if (is_resource($data)) {
+ $data = stream_get_contents($data) ?: '';
+ }
+ $this->backend->createFromString($this->getName(), $data);
+ } else {
+ throw new Forbidden('This calendar-object is read-only');
+ }
+ }
+
+ public function get(): string {
+ return $this->vobject->serialize();
+ }
+
+ public function getContentType(): string {
+ return 'text/calendar; charset=utf-8';
+ }
+
+ public function getETag(): ?string {
+ return null;
+ }
+
+ public function getSize() {
+ return mb_strlen($this->vobject->serialize());
+ }
+
+ public function delete(): void {
+ if ($this->backend instanceof ICreateFromString && $this->calendar->getPermissions() & Constants::PERMISSION_DELETE) {
+ /** @var \Sabre\VObject\Component[] */
+ $components = $this->vobject->getBaseComponents();
+ foreach ($components as $key => $component) {
+ $components[$key]->STATUS = 'CANCELLED';
+ $components[$key]->SEQUENCE = isset($component->SEQUENCE) ? ((int)$component->SEQUENCE->getValue()) + 1 : 1;
+ if ($component->name === 'VEVENT') {
+ $components[$key]->METHOD = 'CANCEL';
+ }
+ }
+ $this->backend->createFromString($this->getName(), (new VCalendar($components))->serialize());
+ } else {
+ throw new Forbidden('This calendar-object is read-only');
+ }
+ }
+
+ public function getName(): string {
+ // Every object is required to have an UID
+ $base = $this->vobject->getBaseComponent();
+ // This should never happen except the app provides invalid calendars (VEvent, VTodo... all require UID to be present)
+ if ($base === null) {
+ throw new NotFound('Invalid node');
+ }
+ if (isset($base->{'X-FILENAME'})) {
+ return (string)$base->{'X-FILENAME'};
+ }
+ return (string)$base->UID . '.ics';
+ }
+
+ public function setName($name): void {
+ throw new Forbidden('This calendar-object is read-only');
+ }
+
+ public function getLastModified(): ?int {
+ $base = $this->vobject->getBaseComponent();
+ if ($base !== null && $this->vobject->getBaseComponent()->{'LAST-MODIFIED'}) {
+ /** @var DateTime */
+ $lastModified = $this->vobject->getBaseComponent()->{'LAST-MODIFIED'};
+ return $lastModified->getDateTime()->getTimestamp();
+ }
+ return null;
+ }
+}
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 b736d9432bd..681709cdb6f 100644
--- a/apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php
+++ b/apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php
@@ -1,32 +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>
- * @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;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
@@ -42,16 +26,6 @@ class EnablePlugin extends ServerPlugin {
public const NS_Nextcloud = 'http://nextcloud.com/ns';
/**
- * @var IConfig
- */
- protected $config;
-
- /**
- * @var BirthdayService
- */
- protected $birthdayService;
-
- /**
* @var Server
*/
protected $server;
@@ -61,10 +35,13 @@ class EnablePlugin extends ServerPlugin {
*
* @param IConfig $config
* @param BirthdayService $birthdayService
+ * @param IUser $user
*/
- public function __construct(IConfig $config, BirthdayService $birthdayService) {
- $this->config = $config;
- $this->birthdayService = $birthdayService;
+ public function __construct(
+ protected IConfig $config,
+ protected BirthdayService $birthdayService,
+ private IUser $user,
+ ) {
}
/**
@@ -117,23 +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;
}
- $principalUri = $node->getOwner();
- $userId = substr($principalUri, 17);
+ $owner = substr($node->getOwner(), 17);
+ if ($owner !== $this->user->getUID()) {
+ $this->server->httpResponse->setStatus(Http::STATUS_FORBIDDEN);
+ return false;
+ }
- $this->config->setUserValue($userId, 'dav', 'generateBirthdayCalendar', 'yes');
- $this->birthdayService->syncUser($userId);
+ $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 bdcf0796283..680b228766f 100644
--- a/apps/dav/lib/CalDAV/BirthdayService.php
+++ b/apps/dav/lib/CalDAV/BirthdayService.php
@@ -3,32 +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>
- *
- * @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;
@@ -53,63 +30,33 @@ use Sabre\VObject\Reader;
*/
class BirthdayService {
public const BIRTHDAY_CALENDAR_URI = 'contact_birthdays';
-
- /** @var GroupPrincipalBackend */
- private $principalBackend;
-
- /** @var CalDavBackend */
- private $calDavBackEnd;
-
- /** @var CardDavBackend */
- private $cardDavBackEnd;
-
- /** @var IConfig */
- private $config;
-
- /** @var IDBConnection */
- private $dbConnection;
-
- /** @var IL10N */
- private $l10n;
+ public const EXCLUDE_FROM_BIRTHDAY_CALENDAR_PROPERTY_NAME = 'X-NC-EXCLUDE-FROM-BIRTHDAY-CALENDAR';
/**
* BirthdayService constructor.
- *
- * @param CalDavBackend $calDavBackEnd
- * @param CardDavBackend $cardDavBackEnd
- * @param GroupPrincipalBackend $principalBackend
- * @param IConfig $config
- * @param IDBConnection $dbConnection
- * @param IL10N $l10n
*/
- 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,
+ ) {
}
- /**
- * @param int $addressBookId
- * @param string $cardUri
- * @param string $cardData
- */
public function onCardChanged(int $addressBookId,
- string $cardUri,
- string $cardData) {
+ string $cardUri,
+ string $cardData): void {
if (!$this->isGloballyEnabled()) {
return;
}
$targetPrincipals = $this->getAllAffectedPrincipals($addressBookId);
$book = $this->cardDavBackEnd->getAddressBookById($addressBookId);
+ if ($book === null) {
+ return;
+ }
$targetPrincipals[] = $book['principaluri'];
$datesToSync = [
['postfix' => '', 'field' => 'BDAY'],
@@ -122,19 +69,20 @@ class BirthdayService {
continue;
}
+ $reminderOffset = $this->getReminderOffsetForUser($principalUri);
+
$calendar = $this->ensureCalendarExists($principalUri);
+ if ($calendar === null) {
+ return;
+ }
foreach ($datesToSync as $type) {
- $this->updateCalendar($cardUri, $cardData, $book, (int) $calendar['id'], $type);
+ $this->updateCalendar($cardUri, $cardData, $book, (int)$calendar['id'], $type, $reminderOffset);
}
}
}
- /**
- * @param int $addressBookId
- * @param string $cardUri
- */
public function onCardDeleted(int $addressBookId,
- string $cardUri) {
+ string $cardUri): void {
if (!$this->isGloballyEnabled()) {
return;
}
@@ -149,18 +97,16 @@ 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);
}
}
}
/**
- * @param string $principal
- * @return array|null
* @throws \Sabre\DAV\Exception\BadRequest
*/
- public function ensureCalendarExists(string $principal):?array {
+ public function ensureCalendarExists(string $principal): ?array {
$calendar = $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI);
if (!is_null($calendar)) {
return $calendar;
@@ -178,12 +124,14 @@ class BirthdayService {
* @param $cardData
* @param $dateField
* @param $postfix
+ * @param $reminderOffset
* @return VCalendar|null
* @throws InvalidDataException
*/
public function buildDateFromContact(string $cardData,
- string $dateField,
- string $postfix):?VCalendar {
+ string $dateField,
+ string $postfix,
+ ?string $reminderOffset):?VCalendar {
if (empty($cardData)) {
return null;
}
@@ -199,6 +147,10 @@ class BirthdayService {
return null;
}
+ if (isset($doc->{self::EXCLUDE_FROM_BIRTHDAY_CALENDAR_PROPERTY_NAME})) {
+ return null;
+ }
+
if (!isset($doc->{$dateField})) {
return null;
}
@@ -220,33 +172,26 @@ class BirthdayService {
} catch (InvalidDataException $e) {
return null;
}
+ if ($dateParts['year'] !== null) {
+ $parameters = $birthday->parameters();
+ $omitYear = (isset($parameters['X-APPLE-OMIT-YEAR'])
+ && $parameters['X-APPLE-OMIT-YEAR'] === $dateParts['year']);
+ // 'X-APPLE-OMIT-YEAR' is not always present, at least iOS 12.4 uses the hard coded date of 1604 (the start of the gregorian calendar) when the year is unknown
+ if ($omitYear || (int)$dateParts['year'] === 1604) {
+ $dateParts['year'] = null;
+ }
+ }
- $unknownYear = false;
$originalYear = null;
- if (!$dateParts['year']) {
- $birthday = '1970-' . $dateParts['month'] . '-' . $dateParts['date'];
+ if ($dateParts['year'] !== null) {
+ $originalYear = (int)$dateParts['year'];
+ }
- $unknownYear = true;
- } else {
- $parameters = $birthday->parameters();
- if (isset($parameters['X-APPLE-OMIT-YEAR'])) {
- $omitYear = $parameters['X-APPLE-OMIT-YEAR'];
- if ($dateParts['year'] === $omitYear) {
- $birthday = '1970-' . $dateParts['month'] . '-' . $dateParts['date'];
- $unknownYear = true;
- }
- } else {
- $originalYear = (int)$dateParts['year'];
- // 'X-APPLE-OMIT-YEAR' is not always present, at least iOS 12.4 uses the hard coded date of 1604 (the start of the gregorian calendar) when the year is unknown
- if ($originalYear == 1604) {
- $originalYear = null;
- $unknownYear = true;
- $birthday = '1970-' . $dateParts['month'] . '-' . $dateParts['date'];
- }
- if ($originalYear < 1970) {
- $birthday = '1970-' . $dateParts['month'] . '-' . $dateParts['date'];
- }
- }
+ $leapDay = ((int)$dateParts['month'] === 2
+ && (int)$dateParts['date'] === 29);
+ if ($dateParts['year'] === null || $originalYear < 1970) {
+ $birthday = ($leapDay ? '1972-' : '1970-')
+ . $dateParts['month'] . '-' . $dateParts['date'];
}
try {
@@ -281,18 +226,25 @@ class BirthdayService {
$vEvent->DTEND['VALUE'] = 'DATE';
$vEvent->{'UID'} = $doc->UID . $postfix;
$vEvent->{'RRULE'} = 'FREQ=YEARLY';
+ if ($leapDay) {
+ /* Sabre\VObject supports BYMONTHDAY only if BYMONTH
+ * is also set */
+ $vEvent->{'RRULE'} = 'FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=-1';
+ }
$vEvent->{'SUMMARY'} = $summary;
$vEvent->{'TRANSP'} = 'TRANSPARENT';
$vEvent->{'X-NEXTCLOUD-BC-FIELD-TYPE'} = $dateField;
- $vEvent->{'X-NEXTCLOUD-BC-UNKNOWN-YEAR'} = $unknownYear ? '1' : '0';
+ $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');
+ $alarm->add($vCal->createProperty('TRIGGER', $reminderOffset, ['VALUE' => 'DURATION']));
+ $alarm->add($vCal->createProperty('ACTION', 'DISPLAY'));
+ $alarm->add($vCal->createProperty('DESCRIPTION', $vEvent->{'SUMMARY'}));
+ $vEvent->add($alarm);
}
- $alarm = $vCal->createComponent('VALARM');
- $alarm->add($vCal->createProperty('TRIGGER', '-PT0M', ['VALUE' => 'DURATION']));
- $alarm->add($vCal->createProperty('ACTION', 'DISPLAY'));
- $alarm->add($vCal->createProperty('DESCRIPTION', $vEvent->{'SUMMARY'}));
- $vEvent->add($alarm);
$vCal->add($vEvent);
return $vCal;
}
@@ -301,8 +253,11 @@ 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
+ }
$calendarObjects = $this->calDavBackEnd->getCalendarObjects($calendar['id'], CalDavBackend::CALENDAR_TYPE_CALENDAR);
foreach ($calendarObjects as $calendarObject) {
@@ -315,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']);
}
}
}
@@ -332,7 +287,7 @@ class BirthdayService {
* @return bool
*/
public function birthdayEvenChanged(string $existingCalendarData,
- VCalendar $newCalendarData):bool {
+ VCalendar $newCalendarData):bool {
try {
$existingBirthday = Reader::read($existingCalendarData);
} catch (Exception $ex) {
@@ -340,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()
);
}
@@ -371,16 +326,18 @@ class BirthdayService {
* @param array $book
* @param int $calendarId
* @param array $type
+ * @param string $reminderOffset
* @throws InvalidDataException
* @throws \Sabre\DAV\Exception\BadRequest
*/
private function updateCalendar(string $cardUri,
- string $cardData,
- array $book,
- int $calendarId,
- array $type):void {
+ string $cardData,
+ array $book,
+ int $calendarId,
+ array $type,
+ ?string $reminderOffset):void {
$objectUri = $book['uri'] . '-' . $cardUri . $type['postfix'] . '.ics';
- $calendarData = $this->buildDateFromContact($cardData, $type['field'], $type['postfix']);
+ $calendarData = $this->buildDateFromContact($cardData, $type['field'], $type['postfix'], $reminderOffset);
$existing = $this->calDavBackEnd->getCalendarObject($calendarId, $objectUri);
if ($calendarData === null) {
if ($existing !== null) {
@@ -421,14 +378,27 @@ class BirthdayService {
}
/**
+ * Extracts the userId part of a principal
+ *
+ * @param string $userPrincipal
+ * @return string|null
+ */
+ private function principalToUserId(string $userPrincipal):?string {
+ if (str_starts_with($userPrincipal, 'principals/users/')) {
+ return substr($userPrincipal, 17);
+ }
+ return null;
+ }
+
+ /**
* Checks if the user opted-out of birthday calendars
*
* @param string $userPrincipal The user principal to check for
* @return bool
*/
private function isUserEnabled(string $userPrincipal):bool {
- if (strpos($userPrincipal, 'principals/users/') === 0) {
- $userId = substr($userPrincipal, 17);
+ $userId = $this->principalToUserId($userPrincipal);
+ if ($userId !== null) {
$isEnabled = $this->config->getUserValue($userId, 'dav', 'generateBirthdayCalendar', 'yes');
return $isEnabled === 'yes';
}
@@ -438,6 +408,23 @@ class BirthdayService {
}
/**
+ * Get the reminder offset value for a user. This is a duration string (e.g.
+ * PT9H) or null if no reminder is wanted.
+ *
+ * @param string $userPrincipal
+ * @return string|null
+ */
+ private function getReminderOffsetForUser(string $userPrincipal):?string {
+ $userId = $this->principalToUserId($userPrincipal);
+ if ($userId !== null) {
+ return $this->config->getUserValue($userId, 'dav', 'birthdayCalendarReminderOffset', 'PT9H') ?: null;
+ }
+
+ // not sure how we got here, just be on the safe side and return the default value
+ return 'PT9H';
+ }
+
+ /**
* Formats title of Birthday event
*
* @param string $field Field name like BDAY, ANNIVERSARY, ...
@@ -447,9 +434,9 @@ class BirthdayService {
* @return string The formatted title
*/
private function formatTitle(string $field,
- string $name,
- int $year = null,
- bool $supports4Byte = true):string {
+ string $name,
+ ?int $year = null,
+ bool $supports4Byte = true):string {
if ($supports4Byte) {
switch ($field) {
case 'BDAY':
diff --git a/apps/dav/lib/CalDAV/CachedSubscription.php b/apps/dav/lib/CalDAV/CachedSubscription.php
index 18e61450ee9..75ee5cb440f 100644
--- a/apps/dav/lib/CalDAV/CachedSubscription.php
+++ b/apps/dav/lib/CalDAV/CachedSubscription.php
@@ -3,41 +3,22 @@
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;
use OCA\DAV\Exception\UnsupportedLimitOnInitialSyncException;
-use Sabre\CalDAV\Backend\BackendInterface;
use Sabre\DAV\Exception\MethodNotAllowed;
use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\INode;
use Sabre\DAV\PropPatch;
/**
* Class CachedSubscription
*
* @package OCA\DAV\CalDAV
- * @property BackendInterface|CalDavBackend $caldavBackend
+ * @property CalDavBackend $caldavBackend
*/
class CachedSubscription extends \Sabre\CalDAV\Calendar {
@@ -51,7 +32,7 @@ class CachedSubscription extends \Sabre\CalDAV\Calendar {
/**
* @return array
*/
- public function getACL():array {
+ public function getACL() {
return [
[
'privilege' => '{DAV:}read',
@@ -73,13 +54,18 @@ class CachedSubscription extends \Sabre\CalDAV\Calendar {
'principal' => '{DAV:}authenticated',
'protected' => true,
],
+ [
+ 'privilege' => '{DAV:}write-properties',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ]
];
}
/**
* @return array
*/
- public function getChildACL():array {
+ public function getChildACL() {
return [
[
'privilege' => '{DAV:}read',
@@ -97,7 +83,6 @@ class CachedSubscription extends \Sabre\CalDAV\Calendar {
'principal' => $this->getOwner() . '/calendar-proxy-read',
'protected' => true,
],
-
];
}
@@ -111,7 +96,7 @@ class CachedSubscription extends \Sabre\CalDAV\Calendar {
return parent::getOwner();
}
-
+
public function delete() {
$this->caldavBackend->deleteSubscription($this->calendarInfo['id']);
}
@@ -139,9 +124,9 @@ class CachedSubscription extends \Sabre\CalDAV\Calendar {
}
/**
- * @return array
+ * @return INode[]
*/
- public function getChildren():array {
+ public function getChildren(): array {
$objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id'], CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
$children = [];
@@ -169,11 +154,11 @@ class CachedSubscription extends \Sabre\CalDAV\Calendar {
/**
* @param string $name
- * @param null $calendarData
- * @return null|string|void
+ * @param null|resource|string $data
+ * @return null|string
* @throws MethodNotAllowed
*/
- public function createFile($name, $calendarData = null) {
+ public function createFile($name, $data = null) {
throw new MethodNotAllowed('Creating objects in cached subscription is not allowed');
}
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 db8c9fa8e80..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;
@@ -50,7 +33,7 @@ class CachedSubscriptionObject extends \Sabre\CalDAV\CalendarObject {
/**
* @param resource|string $calendarData
- * @return string|void
+ * @return string
* @throws MethodNotAllowed
*/
public function put($calendarData) {
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 f0d332adab5..d5b0d875ede 100644
--- a/apps/dav/lib/CalDAV/CalDavBackend.php
+++ b/apps/dav/lib/CalDAV/CalDavBackend.php
@@ -1,48 +1,19 @@
<?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>
- *
- * @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;
-use OCA\DAV\DAV\Sharing\Backend;
use OCA\DAV\DAV\Sharing\IShareable;
use OCA\DAV\Events\CachedCalendarObjectCreatedEvent;
use OCA\DAV\Events\CachedCalendarObjectDeletedEvent;
@@ -50,11 +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\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;
@@ -63,16 +29,23 @@ use OCA\DAV\Events\CalendarUpdatedEvent;
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;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use OCP\IDBConnection;
-use OCP\IGroupManager;
-use OCP\ILogger;
-use OCP\IUser;
use OCP\IUserManager;
use OCP\Security\ISecureRandom;
+use Psr\Log\LoggerInterface;
use RuntimeException;
use Sabre\CalDAV\Backend\AbstractBackend;
use Sabre\CalDAV\Backend\SchedulingSupport;
@@ -95,9 +68,10 @@ use Sabre\VObject\ParseException;
use Sabre\VObject\Property;
use Sabre\VObject\Reader;
use Sabre\VObject\Recur\EventIterator;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
-use Symfony\Component\EventDispatcher\GenericEvent;
+use Sabre\VObject\Recur\MaxInstancesExceededException;
+use Sabre\VObject\Recur\NoInstancesException;
use function array_column;
+use function array_map;
use function array_merge;
use function array_values;
use function explode;
@@ -117,8 +91,23 @@ 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;
+
public const CALENDAR_TYPE_CALENDAR = 0;
public const CALENDAR_TYPE_SUBSCRIPTION = 1;
@@ -150,7 +139,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @var array
* @psalm-var array<string, string[]>
*/
- public $propertyMap = [
+ public array $propertyMap = [
'{DAV:}displayname' => ['displayname', 'string'],
'{urn:ietf:params:xml:ns:caldav}calendar-description' => ['description', 'string'],
'{urn:ietf:params:xml:ns:caldav}calendar-timezone' => ['timezone', 'string'],
@@ -164,7 +153,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
*
* @var array
*/
- public $subscriptionPropertyMap = [
+ public array $subscriptionPropertyMap = [
'{DAV:}displayname' => ['displayname', 'string'],
'{http://apple.com/ns/ical/}refreshrate' => ['refreshrate', 'string'],
'{http://apple.com/ns/ical/}calendar-order' => ['calendarorder', 'int'],
@@ -195,7 +184,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
];
/** @var array parameters to index */
- public static $indexParameters = [
+ public static array $indexParameters = [
'ATTENDEE' => ['CN'],
'ORGANIZER' => ['CN'],
];
@@ -203,86 +192,34 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
/**
* @var string[] Map of uid => display name
*/
- protected $userDisplayNames;
-
- /** @var IDBConnection */
- private $db;
-
- /** @var Backend */
- private $calendarSharingBackend;
-
- /** @var Principal */
- private $principalBackend;
-
- /** @var IUserManager */
- private $userManager;
-
- /** @var ISecureRandom */
- private $random;
+ protected array $userDisplayNames;
- /** @var ILogger */
- private $logger;
+ private string $dbObjectsTable = 'calendarobjects';
+ private string $dbObjectPropertiesTable = 'calendarobjects_props';
+ private string $dbObjectInvitationsTable = 'calendar_invitations';
+ private array $cachedObjects = [];
- /** @var IEventDispatcher */
- private $dispatcher;
-
- /** @var EventDispatcherInterface */
- private $legacyDispatcher;
-
- /** @var IConfig */
- private $config;
-
- /** @var bool */
- private $legacyEndpoint;
-
- /** @var string */
- private $dbObjectPropertiesTable = 'calendarobjects_props';
-
- /**
- * CalDavBackend constructor.
- *
- * @param IDBConnection $db
- * @param Principal $principalBackend
- * @param IUserManager $userManager
- * @param IGroupManager $groupManager
- * @param ISecureRandom $random
- * @param ILogger $logger
- * @param IEventDispatcher $dispatcher
- * @param EventDispatcherInterface $legacyDispatcher
- * @param bool $legacyEndpoint
- */
- public function __construct(IDBConnection $db,
- Principal $principalBackend,
- IUserManager $userManager,
- IGroupManager $groupManager,
- ISecureRandom $random,
- ILogger $logger,
- IEventDispatcher $dispatcher,
- EventDispatcherInterface $legacyDispatcher,
- IConfig $config,
- bool $legacyEndpoint = false) {
- $this->db = $db;
- $this->principalBackend = $principalBackend;
- $this->userManager = $userManager;
- $this->calendarSharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'calendar');
- $this->random = $random;
- $this->logger = $logger;
- $this->dispatcher = $dispatcher;
- $this->legacyDispatcher = $legacyDispatcher;
- $this->config = $config;
- $this->legacyEndpoint = $legacyEndpoint;
+ public function __construct(
+ private IDBConnection $db,
+ private Principal $principalBackend,
+ private IUserManager $userManager,
+ private ISecureRandom $random,
+ private LoggerInterface $logger,
+ private IEventDispatcher $dispatcher,
+ private IConfig $config,
+ private Sharing\Backend $calendarSharingBackend,
+ private bool $legacyEndpoint = false,
+ ) {
}
/**
- * 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('*'))
@@ -305,6 +242,27 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
/**
+ * Return the number of subscriptions for a principal
+ */
+ public function getSubscriptionsForUserCount(string $principalUri): int {
+ $principalUri = $this->convertPrincipal($principalUri, true);
+ $query = $this->db->getQueryBuilder();
+ $query->select($query->func()->count('*'))
+ ->from('calendarsubscriptions');
+
+ if ($principalUri === '') {
+ $query->where($query->expr()->emptyString('principaluri'));
+ } else {
+ $query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
+ }
+
+ $result = $query->executeQuery();
+ $column = (int)$result->fetchOne();
+ $result->closeCursor();
+ return $column;
+ }
+
+ /**
* @return array{id: int, deleted_at: int}[]
*/
public function getDeletedCalendars(int $deletedBefore): array {
@@ -314,14 +272,15 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
->where($qb->expr()->isNotNull('deleted_at'))
->andWhere($qb->expr()->lt('deleted_at', $qb->createNamedParameter($deletedBefore)));
$result = $qb->executeQuery();
- $raw = $result->fetchAll();
- $result->closeCursor();
- return array_map(function ($row) {
- return [
- 'id' => (int) $row['id'],
- 'deleted_at' => (int) $row['deleted_at'],
+ $calendars = [];
+ while (($row = $result->fetch()) !== false) {
+ $calendars[] = [
+ 'id' => (int)$row['id'],
+ 'deleted_at' => (int)$row['deleted_at'],
];
- }, $raw);
+ }
+ $result->closeCursor();
+ return $calendars;
}
/**
@@ -350,132 +309,143 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return array
*/
public function getCalendarsForUser($principalUri) {
- $principalUriOriginal = $principalUri;
- $principalUri = $this->convertPrincipal($principalUri, true);
- $fields = array_column($this->propertyMap, 0);
- $fields[] = 'id';
- $fields[] = 'uri';
- $fields[] = 'synctoken';
- $fields[] = 'components';
- $fields[] = 'principaluri';
- $fields[] = 'transparent';
-
- // Making fields a comma-delimited list
- $query = $this->db->getQueryBuilder();
- $query->select($fields)
- ->from('calendars')
- ->orderBy('calendarorder', 'ASC');
-
- if ($principalUri === '') {
- $query->where($query->expr()->emptyString('principaluri'));
- } else {
- $query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
- }
-
- $result = $query->executeQuery();
-
- $calendars = [];
- while ($row = $result->fetch()) {
- $row['principaluri'] = (string) $row['principaluri'];
- $components = [];
- if ($row['components']) {
- $components = explode(',',$row['components']);
- }
-
- $calendar = [
- '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_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),
- ];
-
- $calendar = $this->rowToCalendar($row, $calendar);
- $calendar = $this->addOwnerPrincipalToCalendar($calendar);
- $calendar = $this->addResourceTypeToCalendar($row, $calendar);
+ return $this->atomic(function () use ($principalUri) {
+ $principalUriOriginal = $principalUri;
+ $principalUri = $this->convertPrincipal($principalUri, true);
+ $fields = array_column($this->propertyMap, 0);
+ $fields[] = 'id';
+ $fields[] = 'uri';
+ $fields[] = 'synctoken';
+ $fields[] = 'components';
+ $fields[] = 'principaluri';
+ $fields[] = 'transparent';
+
+ // Making fields a comma-delimited list
+ $query = $this->db->getQueryBuilder();
+ $query->select($fields)
+ ->from('calendars')
+ ->orderBy('calendarorder', 'ASC');
- if (!isset($calendars[$calendar['id']])) {
- $calendars[$calendar['id']] = $calendar;
+ if ($principalUri === '') {
+ $query->where($query->expr()->emptyString('principaluri'));
+ } else {
+ $query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
}
- }
- $result->closeCursor();
- // query for shared calendars
- $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
- $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
+ $result = $query->executeQuery();
- $principals[] = $principalUri;
+ $calendars = [];
+ while ($row = $result->fetch()) {
+ $row['principaluri'] = (string)$row['principaluri'];
+ $components = [];
+ if ($row['components']) {
+ $components = explode(',', $row['components']);
+ }
- $fields = array_column($this->propertyMap, 0);
- $fields[] = 'a.id';
- $fields[] = 'a.uri';
- $fields[] = 'a.synctoken';
- $fields[] = 'a.components';
- $fields[] = 'a.principaluri';
- $fields[] = 'a.transparent';
- $fields[] = 's.access';
- $query = $this->db->getQueryBuilder();
- $query->select($fields)
- ->from('dav_shares', 's')
- ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
- ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
- ->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
- ->setParameter('type', 'calendar')
- ->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY);
+ $calendar = [
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
+ '{' . 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),
+ ];
- $result = $query->executeQuery();
+ $calendar = $this->rowToCalendar($row, $calendar);
+ $calendar = $this->addOwnerPrincipalToCalendar($calendar);
+ $calendar = $this->addResourceTypeToCalendar($row, $calendar);
- $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
- while ($row = $result->fetch()) {
- $row['principaluri'] = (string) $row['principaluri'];
- if ($row['principaluri'] === $principalUri) {
- continue;
+ if (!isset($calendars[$calendar['id']])) {
+ $calendars[$calendar['id']] = $calendar;
+ }
}
+ $result->closeCursor();
- $readOnly = (int) $row['access'] === Backend::ACCESS_READ;
- if (isset($calendars[$row['id']])) {
- if ($readOnly) {
- // New share can not have more permissions then the old one.
+ // query for shared calendars
+ $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
+ $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
+ $principals[] = $principalUri;
+
+ $fields = array_column($this->propertyMap, 0);
+ $fields = array_map(function (string $field) {
+ return 'a.' . $field;
+ }, $fields);
+ $fields[] = 'a.id';
+ $fields[] = 'a.uri';
+ $fields[] = 'a.synctoken';
+ $fields[] = 'a.components';
+ $fields[] = 'a.principaluri';
+ $fields[] = 'a.transparent';
+ $fields[] = 's.access';
+
+ $select = $this->db->getQueryBuilder();
+ $subSelect = $this->db->getQueryBuilder();
+
+ $subSelect->select('resourceid')
+ ->from('dav_shares', 'd')
+ ->where($subSelect->expr()->eq('d.access', $select->createNamedParameter(Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
+ ->andWhere($subSelect->expr()->in('d.principaluri', $select->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY));
+
+ $select->select($fields)
+ ->from('dav_shares', 's')
+ ->join('s', 'calendars', 'a', $select->expr()->eq('s.resourceid', 'a.id', IQueryBuilder::PARAM_INT))
+ ->where($select->expr()->in('s.principaluri', $select->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY))
+ ->andWhere($select->expr()->eq('s.type', $select->createNamedParameter('calendar', IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR))
+ ->andWhere($select->expr()->notIn('a.id', $select->createFunction($subSelect->getSQL()), IQueryBuilder::PARAM_INT_ARRAY));
+
+ $results = $select->executeQuery();
+
+ $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
+ while ($row = $results->fetch()) {
+ $row['principaluri'] = (string)$row['principaluri'];
+ if ($row['principaluri'] === $principalUri) {
continue;
}
- if (isset($calendars[$row['id']][$readOnlyPropertyName]) &&
- $calendars[$row['id']][$readOnlyPropertyName] === 0) {
- // Old share is already read-write, no more permissions can be gained
- continue;
+
+ $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) {
+ // Old share is already read-write, no more permissions can be gained
+ continue;
+ }
}
- }
- [, $name] = Uri\split($row['principaluri']);
- $uri = $row['uri'] . '_shared_by_' . $name;
- $row['displayname'] = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
- $components = [];
- if ($row['components']) {
- $components = explode(',',$row['components']);
- }
- $calendar = [
- '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_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),
- $readOnlyPropertyName => $readOnly,
- ];
+ [, $name] = Uri\split($row['principaluri']);
+ $uri = $row['uri'] . '_shared_by_' . $name;
+ $row['displayname'] = $row['displayname'] . ' (' . ($this->userManager->getDisplayName($name) ?? ($name ?? '')) . ')';
+ $components = [];
+ if ($row['components']) {
+ $components = explode(',', $row['components']);
+ }
+ $calendar = [
+ 'id' => $row['id'],
+ 'uri' => $uri,
+ 'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
+ '{' . 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),
+ $readOnlyPropertyName => $readOnly,
+ ];
- $calendar = $this->rowToCalendar($row, $calendar);
- $calendar = $this->addOwnerPrincipalToCalendar($calendar);
- $calendar = $this->addResourceTypeToCalendar($row, $calendar);
+ $calendar = $this->rowToCalendar($row, $calendar);
+ $calendar = $this->addOwnerPrincipalToCalendar($calendar);
+ $calendar = $this->addResourceTypeToCalendar($row, $calendar);
- $calendars[$calendar['id']] = $calendar;
- }
- $result->closeCursor();
+ $calendars[$calendar['id']] = $calendar;
+ }
+ $result->closeCursor();
- return array_values($calendars);
+ return array_values($calendars);
+ }, $this->db);
}
/**
@@ -499,17 +469,17 @@ 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']);
+ $components = explode(',', $row['components']);
}
$calendar = [
'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'),
];
@@ -526,25 +496,6 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
return array_values($calendars);
}
-
- /**
- * @param $uid
- * @return string
- */
- private function getUserDisplayName($uid) {
- if (!isset($this->userDisplayNames[$uid])) {
- $user = $this->userManager->get($uid);
-
- if ($user instanceof IUser) {
- $this->userDisplayNames[$uid] = $user->getDisplayName();
- } else {
- $this->userDisplayNames[$uid] = $uid;
- }
- }
-
- return $this->userDisplayNames[$uid];
- }
-
/**
* @return array
*/
@@ -568,19 +519,19 @@ 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 = [];
if ($row['components']) {
- $components = explode(',',$row['components']);
+ $components = explode(',', $row['components']);
}
$calendar = [
'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),
@@ -633,19 +584,19 @@ 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 = [];
if ($row['components']) {
- $components = explode(',',$row['components']);
+ $components = explode(',', $row['components']);
}
$calendar = [
'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),
@@ -688,18 +639,18 @@ 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']);
+ $components = explode(',', $row['components']);
}
$calendar = [
'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'),
];
@@ -712,10 +663,10 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
/**
- * @param $calendarId
+ * @psalm-return CalendarInfo|null
* @return array|null
*/
- public function getCalendarById($calendarId) {
+ public function getCalendarById(int $calendarId): ?array {
$fields = array_column($this->propertyMap, 0);
$fields[] = 'id';
$fields[] = 'uri';
@@ -737,18 +688,18 @@ 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']);
+ $components = explode(',', $row['components']);
}
$calendar = [
'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'),
];
@@ -785,7 +736,44 @@ 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'],
+ '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);
+ }
+
+ 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'],
@@ -793,7 +781,7 @@ 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);
@@ -809,14 +797,20 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @param string $calendarUri
* @param array $properties
* @return int
+ *
+ * @throws CalendarException
*/
public function createCalendar($principalUri, $calendarUri, array $properties) {
+ if (strlen($calendarUri) > 255) {
+ throw new CalendarException('URI too long. Calendar not created');
+ }
+
$values = [
'principaluri' => $this->convertPrincipal($principalUri, true),
'uri' => $calendarUri,
'synctoken' => 1,
'transparent' => 0,
- 'components' => 'VEVENT,VTODO',
+ 'components' => 'VEVENT,VTODO,VJOURNAL',
'displayname' => $calendarUri
];
@@ -826,7 +820,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
}
- $values['components'] = implode(',',$properties[$sccs]->getValue());
+ $values['components'] = implode(',', $properties[$sccs]->getValue());
} elseif (isset($properties['components'])) {
// Allow to provide components internally without having
// to create a SupportedCalendarComponentSet object
@@ -835,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]) {
@@ -844,15 +838,19 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
}
- $query = $this->db->getQueryBuilder();
- $query->insert('calendars');
- foreach ($values as $column => $value) {
- $query->setValue($column, $query->createNamedParameter($value));
- }
- $query->executeStatement();
- $calendarId = $query->getLastInsertId();
+ [$calendarId, $calendarData] = $this->atomic(function () use ($values) {
+ $query = $this->db->getQueryBuilder();
+ $query->insert('calendars');
+ foreach ($values as $column => $value) {
+ $query->setValue($column, $query->createNamedParameter($value));
+ }
+ $query->executeStatement();
+ $calendarId = $query->getLastInsertId();
+
+ $calendarData = $this->getCalendarById($calendarId);
+ return [$calendarId, $calendarData];
+ }, $this->db);
- $calendarData = $this->getCalendarById($calendarId);
$this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int)$calendarId, $calendarData));
return $calendarId;
@@ -884,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];
@@ -892,19 +890,23 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
break;
}
}
- $query = $this->db->getQueryBuilder();
- $query->update('calendars');
- foreach ($newValues as $fieldName => $value) {
- $query->set($fieldName, $query->createNamedParameter($value));
- }
- $query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
- $query->executeStatement();
+ [$calendarData, $shares] = $this->atomic(function () use ($calendarId, $newValues) {
+ $query = $this->db->getQueryBuilder();
+ $query->update('calendars');
+ foreach ($newValues as $fieldName => $value) {
+ $query->set($fieldName, $query->createNamedParameter($value));
+ }
+ $query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
+ $query->executeStatement();
- $this->addChange($calendarId, "", 2);
+ $this->addChanges($calendarId, [''], 2);
- $calendarData = $this->getCalendarById($calendarId);
- $shares = $this->getShares($calendarId);
- $this->dispatcher->dispatchTyped(new CalendarUpdatedEvent((int)$calendarId, $calendarData, $shares, $mutations));
+ $calendarData = $this->getCalendarById($calendarId);
+ $shares = $this->getShares($calendarId);
+ return [$calendarData, $shares];
+ }, $this->db);
+
+ $this->dispatcher->dispatchTyped(new CalendarUpdatedEvent($calendarId, $calendarData, $shares, $mutations));
return true;
});
@@ -917,81 +919,162 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return void
*/
public function deleteCalendar($calendarId, bool $forceDeletePermanently = false) {
- // 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.
- $calendarData = $this->getCalendarById($calendarId);
- $isBirthdayCalendar = isset($calendarData['uri']) && $calendarData['uri'] === BirthdayService::BIRTHDAY_CALENDAR_URI;
- $trashbinDisabled = $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0';
- if ($forceDeletePermanently || $isBirthdayCalendar || $trashbinDisabled) {
+ $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.
$calendarData = $this->getCalendarById($calendarId);
- $shares = $this->getShares($calendarId);
+ $isBirthdayCalendar = isset($calendarData['uri']) && $calendarData['uri'] === BirthdayService::BIRTHDAY_CALENDAR_URI;
+ $trashbinDisabled = $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0';
+ if ($forceDeletePermanently || $isBirthdayCalendar || $trashbinDisabled) {
+ $calendarData = $this->getCalendarById($calendarId);
+ $shares = $this->getShares($calendarId);
- $qbDeleteCalendarObjectProps = $this->db->getQueryBuilder();
- $qbDeleteCalendarObjectProps->delete($this->dbObjectPropertiesTable)
- ->where($qbDeleteCalendarObjectProps->expr()->eq('calendarid', $qbDeleteCalendarObjectProps->createNamedParameter($calendarId)))
- ->andWhere($qbDeleteCalendarObjectProps->expr()->eq('calendartype', $qbDeleteCalendarObjectProps->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
- ->executeStatement();
+ $this->purgeCalendarInvitations($calendarId);
- $qbDeleteCalendarObjects = $this->db->getQueryBuilder();
- $qbDeleteCalendarObjects->delete('calendarobjects')
- ->where($qbDeleteCalendarObjects->expr()->eq('calendarid', $qbDeleteCalendarObjects->createNamedParameter($calendarId)))
- ->andWhere($qbDeleteCalendarObjects->expr()->eq('calendartype', $qbDeleteCalendarObjects->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
- ->executeStatement();
+ $qbDeleteCalendarObjectProps = $this->db->getQueryBuilder();
+ $qbDeleteCalendarObjectProps->delete($this->dbObjectPropertiesTable)
+ ->where($qbDeleteCalendarObjectProps->expr()->eq('calendarid', $qbDeleteCalendarObjectProps->createNamedParameter($calendarId)))
+ ->andWhere($qbDeleteCalendarObjectProps->expr()->eq('calendartype', $qbDeleteCalendarObjectProps->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
+ ->executeStatement();
- $qbDeleteCalendarChanges = $this->db->getQueryBuilder();
- $qbDeleteCalendarObjects->delete('calendarchanges')
- ->where($qbDeleteCalendarChanges->expr()->eq('calendarid', $qbDeleteCalendarChanges->createNamedParameter($calendarId)))
- ->andWhere($qbDeleteCalendarChanges->expr()->eq('calendartype', $qbDeleteCalendarChanges->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
- ->executeStatement();
+ $qbDeleteCalendarObjects = $this->db->getQueryBuilder();
+ $qbDeleteCalendarObjects->delete('calendarobjects')
+ ->where($qbDeleteCalendarObjects->expr()->eq('calendarid', $qbDeleteCalendarObjects->createNamedParameter($calendarId)))
+ ->andWhere($qbDeleteCalendarObjects->expr()->eq('calendartype', $qbDeleteCalendarObjects->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
+ ->executeStatement();
- $this->calendarSharingBackend->deleteAllShares($calendarId);
+ $qbDeleteCalendarChanges = $this->db->getQueryBuilder();
+ $qbDeleteCalendarChanges->delete('calendarchanges')
+ ->where($qbDeleteCalendarChanges->expr()->eq('calendarid', $qbDeleteCalendarChanges->createNamedParameter($calendarId)))
+ ->andWhere($qbDeleteCalendarChanges->expr()->eq('calendartype', $qbDeleteCalendarChanges->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
+ ->executeStatement();
- $qbDeleteCalendar = $this->db->getQueryBuilder();
- $qbDeleteCalendarObjects->delete('calendars')
- ->where($qbDeleteCalendar->expr()->eq('id', $qbDeleteCalendar->createNamedParameter($calendarId)))
- ->executeStatement();
+ $this->calendarSharingBackend->deleteAllShares($calendarId);
- // Only dispatch if we actually deleted anything
- if ($calendarData) {
- $this->dispatcher->dispatchTyped(new CalendarDeletedEvent((int)$calendarId, $calendarData, $shares));
- }
- } else {
- $qbMarkCalendarDeleted = $this->db->getQueryBuilder();
- $qbMarkCalendarDeleted->update('calendars')
- ->set('deleted_at', $qbMarkCalendarDeleted->createNamedParameter(time()))
- ->where($qbMarkCalendarDeleted->expr()->eq('id', $qbMarkCalendarDeleted->createNamedParameter($calendarId)))
- ->executeStatement();
+ $qbDeleteCalendar = $this->db->getQueryBuilder();
+ $qbDeleteCalendar->delete('calendars')
+ ->where($qbDeleteCalendar->expr()->eq('id', $qbDeleteCalendar->createNamedParameter($calendarId)))
+ ->executeStatement();
- $calendarData = $this->getCalendarById($calendarId);
- $shares = $this->getShares($calendarId);
- if ($calendarData) {
- $this->dispatcher->dispatchTyped(new CalendarMovedToTrashEvent(
- (int)$calendarId,
- $calendarData,
- $shares
- ));
+ // Only dispatch if we actually deleted anything
+ if ($calendarData) {
+ $this->dispatcher->dispatchTyped(new CalendarDeletedEvent($calendarId, $calendarData, $shares));
+ }
+ } else {
+ $qbMarkCalendarDeleted = $this->db->getQueryBuilder();
+ $qbMarkCalendarDeleted->update('calendars')
+ ->set('deleted_at', $qbMarkCalendarDeleted->createNamedParameter(time()))
+ ->where($qbMarkCalendarDeleted->expr()->eq('id', $qbMarkCalendarDeleted->createNamedParameter($calendarId)))
+ ->executeStatement();
+
+ $calendarData = $this->getCalendarById($calendarId);
+ $shares = $this->getShares($calendarId);
+ if ($calendarData) {
+ $this->dispatcher->dispatchTyped(new CalendarMovedToTrashEvent(
+ $calendarId,
+ $calendarData,
+ $shares
+ ));
+ }
}
- }
+ }, $this->db);
}
public function restoreCalendar(int $id): void {
+ $this->atomic(function () use ($id): void {
+ $qb = $this->db->getQueryBuilder();
+ $update = $qb->update('calendars')
+ ->set('deleted_at', $qb->createNamedParameter(null))
+ ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
+ $update->executeStatement();
+
+ $calendarData = $this->getCalendarById($id);
+ $shares = $this->getShares($id);
+ if ($calendarData === null) {
+ throw new RuntimeException('Calendar data that was just written can\'t be read back. Check your database configuration.');
+ }
+ $this->dispatcher->dispatchTyped(new CalendarRestoredEvent(
+ $id,
+ $calendarData,
+ $shares
+ ));
+ }, $this->db);
+ }
+
+ /**
+ * 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();
- $update = $qb->update('calendars')
- ->set('deleted_at', $qb->createNamedParameter(null))
- ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
- $update->executeStatement();
+ $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();
- $calendarData = $this->getCalendarById($id);
- $shares = $this->getShares($id);
- if ($calendarData === null) {
- throw new RuntimeException('Calendar data that was just written can\'t be read back. Check your database configuration.');
+ $result = [];
+ while (($row = $stmt->fetch()) !== false) {
+ $result[$row['uid']] = [
+ 'id' => $row['id'],
+ 'etag' => $row['etag'],
+ 'uri' => $row['uri'],
+ 'calendardata' => $row['calendardata'],
+ ];
}
- $this->dispatcher->dispatchTyped(new CalendarRestoredEvent(
- $id,
- $calendarData,
- $shares
- ));
+ $stmt->closeCursor();
+
+ return $result;
}
/**
@@ -1046,7 +1129,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$stmt = $query->executeQuery();
$result = [];
- foreach ($stmt->fetchAll() as $row) {
+ while (($row = $stmt->fetch()) !== false) {
$result[] = [
'id' => $row['id'],
'uri' => $row['uri'],
@@ -1073,18 +1156,18 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$stmt = $query->executeQuery();
$result = [];
- foreach ($stmt->fetchAll() as $row) {
+ while (($row = $stmt->fetch()) !== false) {
$result[] = [
'id' => $row['id'],
'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();
@@ -1123,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();
@@ -1149,8 +1232,12 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return array|null
*/
public function getCalendarObject($calendarId, $objectUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR) {
+ $key = $calendarId . '::' . $objectUri . '::' . $calendarType;
+ if (isset($this->cachedObjects[$key])) {
+ return $this->cachedObjects[$key];
+ }
$query = $this->db->getQueryBuilder();
- $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
+ $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)))
@@ -1163,16 +1250,24 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
return null;
}
+ $object = $this->rowToCalendarObject($row);
+ $this->cachedObjects[$key] = $object;
+ return $object;
+ }
+
+ private function rowToCalendarObject(array $row): array {
return [
'id' => $row['id'],
'uri' => $row['uri'],
+ 'uid' => $row['uid'],
'lastmodified' => $row['lastmodified'],
'etag' => '"' . $row['etag'] . '"',
'calendarid' => $row['calendarid'],
'size' => (int)$row['size'],
'calendardata' => $this->readBlob($row['calendardata']),
'component' => strtolower($row['componenttype']),
- 'classification' => (int)$row['classification']
+ 'classification' => (int)$row['classification'],
+ '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'],
];
}
@@ -1248,81 +1343,79 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return string
*/
public function createCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
+ $this->cachedObjects = [];
$extraData = $this->getDenormalizedData($calendarData);
- // Try to detect duplicates
- $qb = $this->db->getQueryBuilder();
- $qb->select($qb->func()->count('*'))
- ->from('calendarobjects')
- ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
- ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($extraData['uid'])))
- ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
- ->andWhere($qb->expr()->isNull('deleted_at'));
- $result = $qb->executeQuery();
- $count = (int) $result->fetchOne();
- $result->closeCursor();
+ return $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $extraData, $calendarType) {
+ // Try to detect duplicates
+ $qb = $this->db->getQueryBuilder();
+ $qb->select($qb->func()->count('*'))
+ ->from('calendarobjects')
+ ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
+ ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($extraData['uid'])))
+ ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
+ ->andWhere($qb->expr()->isNull('deleted_at'));
+ $result = $qb->executeQuery();
+ $count = (int)$result->fetchOne();
+ $result->closeCursor();
- if ($count !== 0) {
- throw new BadRequest('Calendar object with uid already exists in this calendar collection.');
- }
- // For a more specific error message we also try to explicitly look up the UID but as a deleted entry
- $qbDel = $this->db->getQueryBuilder();
- $qbDel->select($qb->func()->count('*'))
- ->from('calendarobjects')
- ->where($qbDel->expr()->eq('calendarid', $qbDel->createNamedParameter($calendarId)))
- ->andWhere($qbDel->expr()->eq('uid', $qbDel->createNamedParameter($extraData['uid'])))
- ->andWhere($qbDel->expr()->eq('calendartype', $qbDel->createNamedParameter($calendarType)))
- ->andWhere($qbDel->expr()->isNotNull('deleted_at'));
- $result = $qbDel->executeQuery();
- $count = (int) $result->fetchOne();
- $result->closeCursor();
- if ($count !== 0) {
- throw new BadRequest('Deleted calendar object with uid already exists in this calendar collection.');
- }
+ if ($count !== 0) {
+ throw new BadRequest('Calendar object with uid already exists in this calendar collection.');
+ }
+ // For a more specific error message we also try to explicitly look up the UID but as a deleted entry
+ $qbDel = $this->db->getQueryBuilder();
+ $qbDel->select('*')
+ ->from('calendarobjects')
+ ->where($qbDel->expr()->eq('calendarid', $qbDel->createNamedParameter($calendarId)))
+ ->andWhere($qbDel->expr()->eq('uid', $qbDel->createNamedParameter($extraData['uid'])))
+ ->andWhere($qbDel->expr()->eq('calendartype', $qbDel->createNamedParameter($calendarType)))
+ ->andWhere($qbDel->expr()->isNotNull('deleted_at'));
+ $result = $qbDel->executeQuery();
+ $found = $result->fetch();
+ $result->closeCursor();
+ if ($found !== false) {
+ // the object existed previously but has been deleted
+ // remove the trashbin entry and continue as if it was a new object
+ $this->deleteCalendarObject($calendarId, $found['uri']);
+ }
- $query = $this->db->getQueryBuilder();
- $query->insert('calendarobjects')
- ->values([
- 'calendarid' => $query->createNamedParameter($calendarId),
- 'uri' => $query->createNamedParameter($objectUri),
- 'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB),
- 'lastmodified' => $query->createNamedParameter(time()),
- 'etag' => $query->createNamedParameter($extraData['etag']),
- 'size' => $query->createNamedParameter($extraData['size']),
- 'componenttype' => $query->createNamedParameter($extraData['componentType']),
- 'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
- 'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
- 'classification' => $query->createNamedParameter($extraData['classification']),
- 'uid' => $query->createNamedParameter($extraData['uid']),
- 'calendartype' => $query->createNamedParameter($calendarType),
- ])
- ->executeStatement();
+ $query = $this->db->getQueryBuilder();
+ $query->insert('calendarobjects')
+ ->values([
+ 'calendarid' => $query->createNamedParameter($calendarId),
+ 'uri' => $query->createNamedParameter($objectUri),
+ 'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB),
+ 'lastmodified' => $query->createNamedParameter(time()),
+ 'etag' => $query->createNamedParameter($extraData['etag']),
+ 'size' => $query->createNamedParameter($extraData['size']),
+ 'componenttype' => $query->createNamedParameter($extraData['componentType']),
+ 'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
+ 'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
+ 'classification' => $query->createNamedParameter($extraData['classification']),
+ 'uid' => $query->createNamedParameter($extraData['uid']),
+ 'calendartype' => $query->createNamedParameter($calendarType),
+ ])
+ ->executeStatement();
- $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
- $this->addChange($calendarId, $objectUri, 1, $calendarType);
+ $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
+ $this->addChanges($calendarId, [$objectUri], 1, $calendarType);
- $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
- if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
- $calendarRow = $this->getCalendarById($calendarId);
- $shares = $this->getShares($calendarId);
+ $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
+ assert($objectRow !== null);
- $this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
- } else {
- $subscriptionRow = $this->getSubscriptionById($calendarId);
-
- $this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
- $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject', new GenericEvent(
- '\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject',
- [
- 'subscriptionId' => $calendarId,
- 'calendarData' => $subscriptionRow,
- 'shares' => [],
- 'objectData' => $objectRow,
- ]
- ));
- }
+ if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
+ $calendarRow = $this->getCalendarById($calendarId);
+ $shares = $this->getShares($calendarId);
+
+ $this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent($calendarId, $calendarRow, $shares, $objectRow));
+ } else {
+ $subscriptionRow = $this->getSubscriptionById($calendarId);
- return '"' . $extraData['etag'] . '"';
+ $this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent($calendarId, $subscriptionRow, [], $objectRow));
+ }
+
+ return '"' . $extraData['etag'] . '"';
+ }, $this->db);
}
/**
@@ -1345,9 +1438,12 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return string
*/
public function updateCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
+ $this->cachedObjects = [];
$extraData = $this->getDenormalizedData($calendarData);
- $query = $this->db->getQueryBuilder();
- $query->update('calendarobjects')
+
+ 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']))
@@ -1357,105 +1453,88 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
->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)))
- ->executeStatement();
+ ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
+ ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
+ ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
+ ->executeStatement();
- $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
- $this->addChange($calendarId, $objectUri, 2, $calendarType);
+ $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
+ $this->addChanges($calendarId, [$objectUri], 2, $calendarType);
- $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
- if (is_array($objectRow)) {
- if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
- $calendarRow = $this->getCalendarById($calendarId);
- $shares = $this->getShares($calendarId);
+ $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
+ if (is_array($objectRow)) {
+ if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
+ $calendarRow = $this->getCalendarById($calendarId);
+ $shares = $this->getShares($calendarId);
- $this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
- } else {
- $subscriptionRow = $this->getSubscriptionById($calendarId);
+ $this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent($calendarId, $calendarRow, $shares, $objectRow));
+ } else {
+ $subscriptionRow = $this->getSubscriptionById($calendarId);
- $this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
- $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject', new GenericEvent(
- '\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject',
- [
- 'subscriptionId' => $calendarId,
- 'calendarData' => $subscriptionRow,
- 'shares' => [],
- 'objectData' => $objectRow,
- ]
- ));
+ $this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent($calendarId, $subscriptionRow, [], $objectRow));
+ }
}
- }
- return '"' . $extraData['etag'] . '"';
+ return '"' . $extraData['etag'] . '"';
+ }, $this->db);
}
/**
* 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 $principalUri
+ * @param string $tragetObjectUri
* @param int $calendarType
* @return bool
* @throws Exception
*/
- public function moveCalendarObject(int $sourceCalendarId, int $targetCalendarId, int $objectId, string $principalUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool {
- $object = $this->getCalendarObjectById($principalUri, $objectId);
- if (empty($object)) {
- return false;
- }
-
- $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))
- ->executeStatement();
+ 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 ($sourcePrincipalUri, $sourceObjectId, $targetPrincipalUri, $targetCalendarId, $tragetObjectUri, $calendarType) {
+ $object = $this->getCalendarObjectById($sourcePrincipalUri, $sourceObjectId);
+ if (empty($object)) {
+ return false;
+ }
- $this->purgeProperties($sourceCalendarId, $objectId);
- $this->updateProperties($targetCalendarId, $object['uri'], $object['calendardata'], $calendarType);
+ $sourceCalendarId = $object['calendarid'];
+ $sourceObjectUri = $object['uri'];
- $this->addChange($sourceCalendarId, $object['uri'], 1, $calendarType);
- $this->addChange($targetCalendarId, $object['uri'], 3, $calendarType);
+ $query = $this->db->getQueryBuilder();
+ $query->update('calendarobjects')
+ ->set('calendarid', $query->createNamedParameter($targetCalendarId, 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();
- $object = $this->getCalendarObjectById($principalUri, $objectId);
- // Calendar Object wasn't found - possibly because it was deleted in the meantime by a different client
- if (empty($object)) {
- return false;
- }
+ $this->purgeProperties($sourceCalendarId, $sourceObjectId);
+ $this->updateProperties($targetCalendarId, $tragetObjectUri, $object['calendardata'], $calendarType);
- $calendarRow = $this->getCalendarById($targetCalendarId);
- // the calendar this event is being moved to does not exist any longer
- if (empty($calendarRow)) {
- return false;
- }
+ $this->addChanges($sourceCalendarId, [$sourceObjectUri], 3, $calendarType);
+ $this->addChanges($targetCalendarId, [$tragetObjectUri], 1, $calendarType);
- if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
- $shares = $this->getShares($targetCalendarId);
- $this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent($targetCalendarId, $calendarRow, $shares, $object));
- }
- return true;
- }
+ $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;
+ }
+ $targetCalendarRow = $this->getCalendarById($targetCalendarId);
+ // the calendar this event is being moved to does not exist any longer
+ if (empty($targetCalendarRow)) {
+ return false;
+ }
- /**
- * @param int $calendarObjectId
- * @param int $classification
- */
- public function setClassification($calendarObjectId, $classification) {
- 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();
+ if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
+ $sourceShares = $this->getShares($sourceCalendarId);
+ $targetShares = $this->getShares($targetCalendarId);
+ $sourceCalendarRow = $this->getCalendarById($sourceCalendarId);
+ $this->dispatcher->dispatchTyped(new CalendarObjectMovedEvent($sourceCalendarId, $sourceCalendarRow, $targetCalendarId, $targetCalendarRow, $sourceShares, $targetShares, $object));
+ }
+ return true;
+ }, $this->db);
}
/**
@@ -1470,86 +1549,82 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return void
*/
public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR, bool $forceDeletePermanently = false) {
- $data = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
+ $this->cachedObjects = [];
+ $this->atomic(function () use ($calendarId, $objectUri, $calendarType, $forceDeletePermanently): void {
+ $data = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
- if ($data === null) {
- // Nothing to delete
- return;
- }
+ if ($data === null) {
+ // Nothing to delete
+ return;
+ }
- if ($forceDeletePermanently || $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0') {
- $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?');
- $stmt->execute([$calendarId, $objectUri, $calendarType]);
+ if ($forceDeletePermanently || $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0') {
+ $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?');
+ $stmt->execute([$calendarId, $objectUri, $calendarType]);
- $this->purgeProperties($calendarId, $data['id']);
+ $this->purgeProperties($calendarId, $data['id']);
- if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
- $calendarRow = $this->getCalendarById($calendarId);
- $shares = $this->getShares($calendarId);
+ $this->purgeObjectInvitations($data['uid']);
- $this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent((int)$calendarId, $calendarRow, $shares, $data));
- } else {
- $subscriptionRow = $this->getSubscriptionById($calendarId);
+ if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
+ $calendarRow = $this->getCalendarById($calendarId);
+ $shares = $this->getShares($calendarId);
- $this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent((int)$calendarId, $subscriptionRow, [], $data));
- $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', new GenericEvent(
- '\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject',
- [
- 'subscriptionId' => $calendarId,
- 'calendarData' => $subscriptionRow,
- 'shares' => [],
- 'objectData' => $data,
- ]
- ));
- }
- } else {
- $pathInfo = pathinfo($data['uri']);
- if (!empty($pathInfo['extension'])) {
- // Append a suffix to "free" the old URI for recreation
- $newUri = sprintf(
- "%s-deleted.%s",
- $pathInfo['filename'],
- $pathInfo['extension']
- );
- } else {
- $newUri = sprintf(
- "%s-deleted",
- $pathInfo['filename']
- );
- }
+ $this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent($calendarId, $calendarRow, $shares, $data));
+ } else {
+ $subscriptionRow = $this->getSubscriptionById($calendarId);
- // Try to detect conflicts before the DB does
- // As unlikely as it seems, this can happen when the user imports, then deletes, imports and deletes again
- $newObject = $this->getCalendarObject($calendarId, $newUri, $calendarType);
- if ($newObject !== null) {
- throw new Forbidden("A calendar object with URI $newUri already exists in calendar $calendarId, therefore this object can't be moved into the trashbin");
- }
+ $this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent($calendarId, $subscriptionRow, [], $data));
+ }
+ } else {
+ $pathInfo = pathinfo($data['uri']);
+ if (!empty($pathInfo['extension'])) {
+ // Append a suffix to "free" the old URI for recreation
+ $newUri = sprintf(
+ '%s-deleted.%s',
+ $pathInfo['filename'],
+ $pathInfo['extension']
+ );
+ } else {
+ $newUri = sprintf(
+ '%s-deleted',
+ $pathInfo['filename']
+ );
+ }
- $qb = $this->db->getQueryBuilder();
- $markObjectDeletedQuery = $qb->update('calendarobjects')
- ->set('deleted_at', $qb->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
- ->set('uri', $qb->createNamedParameter($newUri))
- ->where(
- $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
- $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
- $qb->expr()->eq('uri', $qb->createNamedParameter($objectUri))
- );
- $markObjectDeletedQuery->executeStatement();
+ // Try to detect conflicts before the DB does
+ // As unlikely as it seems, this can happen when the user imports, then deletes, imports and deletes again
+ $newObject = $this->getCalendarObject($calendarId, $newUri, $calendarType);
+ if ($newObject !== null) {
+ throw new Forbidden("A calendar object with URI $newUri already exists in calendar $calendarId, therefore this object can't be moved into the trashbin");
+ }
- $calendarData = $this->getCalendarById($calendarId);
- if ($calendarData !== null) {
- $this->dispatcher->dispatchTyped(
- new CalendarObjectMovedToTrashEvent(
- (int)$calendarId,
- $calendarData,
- $this->getShares($calendarId),
- $data
- )
- );
+ $qb = $this->db->getQueryBuilder();
+ $markObjectDeletedQuery = $qb->update('calendarobjects')
+ ->set('deleted_at', $qb->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
+ ->set('uri', $qb->createNamedParameter($newUri))
+ ->where(
+ $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
+ $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
+ $qb->expr()->eq('uri', $qb->createNamedParameter($objectUri))
+ );
+ $markObjectDeletedQuery->executeStatement();
+
+ $calendarData = $this->getCalendarById($calendarId);
+ if ($calendarData !== null) {
+ $this->dispatcher->dispatchTyped(
+ new CalendarObjectMovedToTrashEvent(
+ $calendarId,
+ $calendarData,
+ $this->getShares($calendarId),
+ $data
+ )
+ );
+ }
}
- }
- $this->addChange($calendarId, $objectUri, 3, $calendarType);
+ $this->addChanges($calendarId, [$objectUri], 3, $calendarType);
+ }, $this->db);
}
/**
@@ -1558,50 +1633,53 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @throws Forbidden
*/
public function restoreCalendarObject(array $objectData): void {
- $id = (int) $objectData['id'];
- $restoreUri = str_replace("-deleted.ics", ".ics", $objectData['uri']);
- $targetObject = $this->getCalendarObject(
- $objectData['calendarid'],
- $restoreUri
- );
- if ($targetObject !== null) {
- throw new Forbidden("Can not restore calendar $id because a calendar object with the URI $restoreUri already exists");
- }
+ $this->cachedObjects = [];
+ $this->atomic(function () use ($objectData): void {
+ $id = (int)$objectData['id'];
+ $restoreUri = str_replace('-deleted.ics', '.ics', $objectData['uri']);
+ $targetObject = $this->getCalendarObject(
+ $objectData['calendarid'],
+ $restoreUri
+ );
+ if ($targetObject !== null) {
+ throw new Forbidden("Can not restore calendar $id because a calendar object with the URI $restoreUri already exists");
+ }
- $qb = $this->db->getQueryBuilder();
- $update = $qb->update('calendarobjects')
- ->set('uri', $qb->createNamedParameter($restoreUri))
- ->set('deleted_at', $qb->createNamedParameter(null))
- ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
- $update->executeStatement();
-
- // Make sure this change is tracked in the changes table
- $qb2 = $this->db->getQueryBuilder();
- $selectObject = $qb2->select('calendardata', 'uri', 'calendarid', 'calendartype')
- ->selectAlias('componenttype', 'component')
- ->from('calendarobjects')
- ->where($qb2->expr()->eq('id', $qb2->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
- $result = $selectObject->executeQuery();
- $row = $result->fetch();
- $result->closeCursor();
- if ($row === false) {
- // Welp, this should possibly not have happened, but let's ignore
- return;
- }
- $this->addChange($row['calendarid'], $row['uri'], 1, (int) $row['calendartype']);
+ $qb = $this->db->getQueryBuilder();
+ $update = $qb->update('calendarobjects')
+ ->set('uri', $qb->createNamedParameter($restoreUri))
+ ->set('deleted_at', $qb->createNamedParameter(null))
+ ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
+ $update->executeStatement();
+
+ // Make sure this change is tracked in the changes table
+ $qb2 = $this->db->getQueryBuilder();
+ $selectObject = $qb2->select('calendardata', 'uri', 'calendarid', 'calendartype')
+ ->selectAlias('componenttype', 'component')
+ ->from('calendarobjects')
+ ->where($qb2->expr()->eq('id', $qb2->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
+ $result = $selectObject->executeQuery();
+ $row = $result->fetch();
+ $result->closeCursor();
+ if ($row === false) {
+ // Welp, this should possibly not have happened, but let's ignore
+ return;
+ }
+ $this->addChanges($row['calendarid'], [$row['uri']], 1, (int)$row['calendartype']);
- $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'],
- $calendarRow,
- $this->getShares((int) $row['calendarid']),
- $row
- )
- );
+ $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'],
+ $calendarRow,
+ $this->getShares((int)$row['calendarid']),
+ $row
+ )
+ );
+ }, $this->db);
}
/**
@@ -1644,7 +1722,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* Note that especially time-range-filters may be difficult to parse. A
* time-range filter specified on a VEVENT must for instance also handle
* recurrence rules correctly.
- * A good example of how to interprete all these filters can also simply
+ * A good example of how to interpret all these filters can also simply
* be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
* as possible, so it gives you a good idea on what type of stuff you need
* to think of.
@@ -1683,12 +1761,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
}
}
- $columns = ['uri'];
- if ($requirePostFilter) {
- $columns = ['uri', 'calendardata'];
- }
$query = $this->db->getQueryBuilder();
- $query->select($columns)
+ $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)))
@@ -1709,21 +1783,32 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$result = [];
while ($row = $stmt->fetch()) {
+ // if we leave it as a blob we can't read it both from the post filter and the rowToCalendarObject
+ if (isset($row['calendardata'])) {
+ $row['calendardata'] = $this->readBlob($row['calendardata']);
+ }
+
if ($requirePostFilter) {
// validateFilterForObject will parse the calendar data
// catch parsing errors
try {
$matches = $this->validateFilterForObject($row, $filters);
} catch (ParseException $ex) {
- $this->logger->logException($ex, [
+ $this->logger->error('Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:' . $calendarId . ' uri:' . $row['uri'], [
'app' => 'dav',
- 'message' => 'Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri']
+ 'exception' => $ex,
]);
continue;
} catch (InvalidDataException $ex) {
- $this->logger->logException($ex, [
+ $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',
- 'message' => 'Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri']
+ 'exception' => $ex,
]);
continue;
}
@@ -1733,6 +1818,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
}
$result[] = $row['uri'];
+ $key = $calendarId . '::' . $row['uri'] . '::' . $calendarType;
+ $this->cachedObjects[$key] = $this->rowToCalendarObject($row);
}
return $result;
@@ -1750,118 +1837,120 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return array
*/
public function calendarSearch($principalUri, array $filters, $limit = null, $offset = null) {
- $calendars = $this->getCalendarsForUser($principalUri);
- $ownCalendars = [];
- $sharedCalendars = [];
+ return $this->atomic(function () use ($principalUri, $filters, $limit, $offset) {
+ $calendars = $this->getCalendarsForUser($principalUri);
+ $ownCalendars = [];
+ $sharedCalendars = [];
- $uriMapper = [];
+ $uriMapper = [];
- foreach ($calendars as $calendar) {
- if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) {
- $ownCalendars[] = $calendar['id'];
- } else {
- $sharedCalendars[] = $calendar['id'];
+ foreach ($calendars as $calendar) {
+ if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) {
+ $ownCalendars[] = $calendar['id'];
+ } else {
+ $sharedCalendars[] = $calendar['id'];
+ }
+ $uriMapper[$calendar['id']] = $calendar['uri'];
+ }
+ if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) {
+ return [];
}
- $uriMapper[$calendar['id']] = $calendar['uri'];
- }
- if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) {
- return [];
- }
- $query = $this->db->getQueryBuilder();
- // Calendar id expressions
- $calendarExpressions = [];
- foreach ($ownCalendars as $id) {
- $calendarExpressions[] = $query->expr()->andX(
- $query->expr()->eq('c.calendarid',
- $query->createNamedParameter($id)),
- $query->expr()->eq('c.calendartype',
+ $query = $this->db->getQueryBuilder();
+ // Calendar id expressions
+ $calendarExpressions = [];
+ foreach ($ownCalendars as $id) {
+ $calendarExpressions[] = $query->expr()->andX(
+ $query->expr()->eq('c.calendarid',
+ $query->createNamedParameter($id)),
+ $query->expr()->eq('c.calendartype',
$query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
- }
- foreach ($sharedCalendars as $id) {
- $calendarExpressions[] = $query->expr()->andX(
- $query->expr()->eq('c.calendarid',
- $query->createNamedParameter($id)),
- $query->expr()->eq('c.classification',
- $query->createNamedParameter(self::CLASSIFICATION_PUBLIC)),
- $query->expr()->eq('c.calendartype',
- $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
- }
-
- if (count($calendarExpressions) === 1) {
- $calExpr = $calendarExpressions[0];
- } else {
- $calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions);
- }
+ }
+ foreach ($sharedCalendars as $id) {
+ $calendarExpressions[] = $query->expr()->andX(
+ $query->expr()->eq('c.calendarid',
+ $query->createNamedParameter($id)),
+ $query->expr()->eq('c.classification',
+ $query->createNamedParameter(self::CLASSIFICATION_PUBLIC)),
+ $query->expr()->eq('c.calendartype',
+ $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
+ }
- // Component expressions
- $compExpressions = [];
- foreach ($filters['comps'] as $comp) {
- $compExpressions[] = $query->expr()
- ->eq('c.componenttype', $query->createNamedParameter($comp));
- }
+ if (count($calendarExpressions) === 1) {
+ $calExpr = $calendarExpressions[0];
+ } else {
+ $calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions);
+ }
- if (count($compExpressions) === 1) {
- $compExpr = $compExpressions[0];
- } else {
- $compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions);
- }
+ // Component expressions
+ $compExpressions = [];
+ foreach ($filters['comps'] as $comp) {
+ $compExpressions[] = $query->expr()
+ ->eq('c.componenttype', $query->createNamedParameter($comp));
+ }
- if (!isset($filters['props'])) {
- $filters['props'] = [];
- }
- if (!isset($filters['params'])) {
- $filters['params'] = [];
- }
+ if (count($compExpressions) === 1) {
+ $compExpr = $compExpressions[0];
+ } else {
+ $compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions);
+ }
- $propParamExpressions = [];
- foreach ($filters['props'] as $prop) {
- $propParamExpressions[] = $query->expr()->andX(
- $query->expr()->eq('i.name', $query->createNamedParameter($prop)),
- $query->expr()->isNull('i.parameter')
- );
- }
- foreach ($filters['params'] as $param) {
- $propParamExpressions[] = $query->expr()->andX(
- $query->expr()->eq('i.name', $query->createNamedParameter($param['property'])),
- $query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter']))
- );
- }
+ if (!isset($filters['props'])) {
+ $filters['props'] = [];
+ }
+ if (!isset($filters['params'])) {
+ $filters['params'] = [];
+ }
- if (count($propParamExpressions) === 1) {
- $propParamExpr = $propParamExpressions[0];
- } else {
- $propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions);
- }
+ $propParamExpressions = [];
+ foreach ($filters['props'] as $prop) {
+ $propParamExpressions[] = $query->expr()->andX(
+ $query->expr()->eq('i.name', $query->createNamedParameter($prop)),
+ $query->expr()->isNull('i.parameter')
+ );
+ }
+ foreach ($filters['params'] as $param) {
+ $propParamExpressions[] = $query->expr()->andX(
+ $query->expr()->eq('i.name', $query->createNamedParameter($param['property'])),
+ $query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter']))
+ );
+ }
- $query->select(['c.calendarid', 'c.uri'])
- ->from($this->dbObjectPropertiesTable, 'i')
- ->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id'))
- ->where($calExpr)
- ->andWhere($compExpr)
- ->andWhere($propParamExpr)
- ->andWhere($query->expr()->iLike('i.value',
- $query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%')))
- ->andWhere($query->expr()->isNull('deleted_at'));
+ if (count($propParamExpressions) === 1) {
+ $propParamExpr = $propParamExpressions[0];
+ } else {
+ $propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions);
+ }
- if ($offset) {
- $query->setFirstResult($offset);
- }
- if ($limit) {
- $query->setMaxResults($limit);
- }
+ $query->select(['c.calendarid', 'c.uri'])
+ ->from($this->dbObjectPropertiesTable, 'i')
+ ->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id'))
+ ->where($calExpr)
+ ->andWhere($compExpr)
+ ->andWhere($propParamExpr)
+ ->andWhere($query->expr()->iLike('i.value',
+ $query->createNamedParameter('%' . $this->db->escapeLikeParameter($filters['search-term']) . '%')))
+ ->andWhere($query->expr()->isNull('deleted_at'));
+
+ if ($offset) {
+ $query->setFirstResult($offset);
+ }
+ if ($limit) {
+ $query->setMaxResults($limit);
+ }
- $stmt = $query->executeQuery();
+ $stmt = $query->executeQuery();
- $result = [];
- while ($row = $stmt->fetch()) {
- $path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
- if (!in_array($path, $result)) {
- $result[] = $path;
+ $result = [];
+ while ($row = $stmt->fetch()) {
+ $path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
+ if (!in_array($path, $result)) {
+ $result[] = $path;
+ }
}
- }
- return $result;
+ return $result;
+ }, $this->db);
}
/**
@@ -1876,110 +1965,151 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
*
* @return array
*/
- public function search(array $calendarInfo, $pattern, array $searchProperties,
- array $options, $limit, $offset) {
+ public function search(
+ array $calendarInfo,
+ $pattern,
+ array $searchProperties,
+ array $options,
+ $limit,
+ $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')
+ ->where($outerQuery->expr()->isNull('deleted_at'));
// only return public items for shared calendars for now
if (isset($calendarInfo['{http://owncloud.org/ns}owner-principal']) === false || $calendarInfo['principaluri'] !== $calendarInfo['{http://owncloud.org/ns}owner-principal']) {
- $innerQuery->andWhere($innerQuery->expr()->eq('c.classification',
+ $outerQuery->andWhere($outerQuery->expr()->eq('c.classification',
$outerQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
}
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) . '%')));
}
- $outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
- ->from('calendarobjects', 'c')
- ->where($outerQuery->expr()->isNull('deleted_at'));
+ $start = null;
+ $end = null;
- 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())));
- }
+ $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'])) {
+ $outerQuery->andWhere($outerQuery->expr()->eq('uid', $outerQuery->createNamedParameter($options['uid'])));
}
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) {
+ // 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');
+
+ $offset = (int)$offset;
+ $outerQuery->setFirstResult($offset);
+
+ $calendarObjects = [];
+
+ 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 = $outerQuery->executeQuery();
- $calendarObjects = array_filter($result->fetchAll(), function (array $row) use ($options) {
- $start = $options['timerange']['start'] ?? null;
- $end = $options['timerange']['end'] ?? null;
+ $calendarObjects = array_map(function ($o) use ($options) {
+ $calendarData = Reader::read($o['calendardata']);
- if ($start === null || !($start instanceof DateTimeInterface) || $end === null || !($end instanceof DateTimeInterface)) {
- // No filter required
- return true;
+ // Expand recurrences if an explicit time range is requested
+ if ($calendarData instanceof VCalendar
+ && isset($options['timerange']['start'], $options['timerange']['end'])) {
+ $calendarData = $calendarData->expand(
+ $options['timerange']['start'],
+ $options['timerange']['end'],
+ );
}
- $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']);
- }
- return $isValid;
- });
- $result->closeCursor();
-
- return array_map(function ($o) {
- $calendarData = Reader::read($o['calendardata']);
$comps = $calendarData->getComponents();
$objects = [];
$timezones = [];
@@ -2004,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;
}
/**
@@ -2077,115 +2273,136 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return array
*/
public function searchPrincipalUri(string $principalUri,
- string $pattern,
- array $componentTypes,
- array $searchProperties,
- array $searchParameters,
- array $options = []): array {
- $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();
+ string $pattern,
+ array $componentTypes,
+ array $searchProperties,
+ array $searchParameters,
+ 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 = [];
+ $searchOr = [];
+
+ // Fetch calendars and subscription
+ $calendars = $this->getCalendarsForUser($principalUri);
+ $subscriptions = $this->getSubscriptionsForUser($principalUri);
+ foreach ($calendars as $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)),
+ );
- // 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)));
+ // If it's shared, limit search to public events
+ if (isset($calendar['{http://owncloud.org/ns}owner-principal'])
+ && $calendar['principaluri'] !== $calendar['{http://owncloud.org/ns}owner-principal']) {
+ $calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
+ }
- // If it's shared, limit search to public events
- if (isset($calendar['{http://owncloud.org/ns}owner-principal'])
- && $calendar['principaluri'] !== $calendar['{http://owncloud.org/ns}owner-principal']) {
- $calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
+ $calendarOr[] = $calendarAnd;
}
+ foreach ($subscriptions as $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)),
+ );
- $calendarOr->add($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)));
+ // If it's shared, limit search to public events
+ if (isset($subscription['{http://owncloud.org/ns}owner-principal'])
+ && $subscription['principaluri'] !== $subscription['{http://owncloud.org/ns}owner-principal']) {
+ $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
+ }
- // If it's shared, limit search to public events
- if (isset($subscription['{http://owncloud.org/ns}owner-principal'])
- && $subscription['principaluri'] !== $subscription['{http://owncloud.org/ns}owner-principal']) {
- $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
+ $calendarOr[] = $subscriptionAnd;
}
- $calendarOr->add($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'));
-
- $searchOr->add($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)));
-
- $searchOr->add($parameterAnd);
- }
+ foreach ($searchProperties as $property) {
+ $propertyAnd = $calendarObjectIdQuery->expr()->andX(
+ $calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)),
+ $calendarObjectIdQuery->expr()->isNull('cob.parameter'),
+ );
- if ($calendarOr->count() === 0) {
- return [];
- }
- if ($searchOr->count() === 0) {
- return [];
- }
+ $searchOr[] = $propertyAnd;
+ }
+ foreach ($searchParameters as $property => $parameter) {
+ $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)),
+ );
- $calendarObjectIdQuery->selectDistinct('cob.objectid')
- ->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()->isNull('deleted_at'));
+ $searchOr[] = $parameterAnd;
+ }
- if ('' !== $pattern) {
- if (!$escapePattern) {
- $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern)));
- } else {
- $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
+ if (empty($calendarOr)) {
+ return [];
+ }
+ if (empty($searchOr)) {
+ return [];
}
- }
- if (isset($options['limit'])) {
- $calendarObjectIdQuery->setMaxResults($options['limit']);
- }
- if (isset($options['offset'])) {
- $calendarObjectIdQuery->setFirstResult($options['offset']);
- }
+ $calendarObjectIdQuery->selectDistinct('cob.objectid')
+ ->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($calendarObjectIdQuery->expr()->orX(...$calendarOr))
+ ->andWhere($calendarObjectIdQuery->expr()->orX(...$searchOr))
+ ->andWhere($calendarObjectIdQuery->expr()->isNull('deleted_at'));
+
+ if ($pattern !== '') {
+ if (!$escapePattern) {
+ $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern)));
+ } else {
+ $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
+ }
+ }
- $result = $calendarObjectIdQuery->executeQuery();
- $matches = $result->fetchAll();
- $result->closeCursor();
- $matches = array_map(static function (array $match):int {
- return (int) $match['objectid'];
- }, $matches);
+ if (isset($options['limit'])) {
+ $calendarObjectIdQuery->setMaxResults($options['limit']);
+ }
+ if (isset($options['offset'])) {
+ $calendarObjectIdQuery->setFirstResult($options['offset']);
+ }
+ if (isset($options['timerange'])) {
+ if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
+ $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->gt(
+ 'lastoccurence',
+ $calendarObjectIdQuery->createNamedParameter($options['timerange']['start']->getTimeStamp()),
+ ));
+ }
+ if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
+ $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->lt(
+ 'firstoccurence',
+ $calendarObjectIdQuery->createNamedParameter($options['timerange']['end']->getTimeStamp()),
+ ));
+ }
+ }
- $query = $this->db->getQueryBuilder();
- $query->select('calendardata', 'uri', 'calendarid', 'calendartype')
- ->from('calendarobjects')
- ->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
+ $result = $calendarObjectIdQuery->executeQuery();
+ $matches = [];
+ while (($row = $result->fetch()) !== false) {
+ $matches[] = (int)$row['objectid'];
+ }
+ $result->closeCursor();
- $result = $query->executeQuery();
- $calendarObjects = $result->fetchAll();
- $result->closeCursor();
+ $query = $this->db->getQueryBuilder();
+ $query->select('calendardata', 'uri', 'calendarid', 'calendartype')
+ ->from('calendarobjects')
+ ->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
- return array_map(function (array $array): array {
- $array['calendarid'] = (int)$array['calendarid'];
- $array['calendartype'] = (int)$array['calendartype'];
- $array['calendardata'] = $this->readBlob($array['calendardata']);
+ $result = $query->executeQuery();
+ $calendarObjects = [];
+ while (($array = $result->fetch()) !== false) {
+ $array['calendarid'] = (int)$array['calendarid'];
+ $array['calendartype'] = (int)$array['calendartype'];
+ $array['calendardata'] = $this->readBlob($array['calendardata']);
- return $array;
- }, $calendarObjects);
+ $calendarObjects[] = $array;
+ }
+ $result->closeCursor();
+ return $calendarObjects;
+ }, $this->db);
}
/**
@@ -2252,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,
];
}
@@ -2311,87 +2528,74 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @param int $syncLevel
* @param int|null $limit
* @param int $calendarType
- * @return array
+ * @return ?array
*/
public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
- // Current synctoken
- $qb = $this->db->getQueryBuilder();
- $qb->select('synctoken')
- ->from('calendars')
- ->where(
- $qb->expr()->eq('id', $qb->createNamedParameter($calendarId))
- );
- $stmt = $qb->executeQuery();
- $currentToken = $stmt->fetchOne();
-
- if ($currentToken === false) {
- return null;
- }
-
- $result = [
- 'syncToken' => $currentToken,
- 'added' => [],
- 'modified' => [],
- 'deleted' => [],
- ];
+ $table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
- if ($syncToken) {
+ return $this->atomic(function () use ($calendarId, $syncToken, $syncLevel, $limit, $calendarType, $table) {
+ // Current synctoken
$qb = $this->db->getQueryBuilder();
-
- $qb->select('uri', 'operation')
- ->from('calendarchanges')
+ $qb->select('synctoken')
+ ->from($table)
->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
+ $qb->expr()->eq('id', $qb->createNamedParameter($calendarId))
+ );
$stmt = $qb->executeQuery();
- $changes = [];
+ $currentToken = $stmt->fetchOne();
+ $initialSync = !is_numeric($syncToken);
- // 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'];
+ if ($currentToken === false) {
+ return null;
}
- $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;
- }
+ // 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()->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');
}
- } else {
- // No synctoken supplied, this is the initial sync.
- $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))
- )
- );
+ // evaluate if limit exists
+ if (is_numeric($limit)) {
+ $qb->setMaxResults($limit);
+ }
+ // execute command
$stmt = $qb->executeQuery();
- $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
+ // build results
+ $result = ['syncToken' => $currentToken, 'added' => [], 'modified' => [], 'deleted' => []];
+ // retrieve results
+ if ($initialSync) {
+ $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
+ } 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;
+
+ return $result;
+ }, $this->db);
}
/**
@@ -2452,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);
@@ -2495,28 +2699,23 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
}
- $valuesToInsert = [];
-
- $query = $this->db->getQueryBuilder();
-
- foreach (array_keys($values) as $name) {
- $valuesToInsert[$name] = $query->createNamedParameter($values[$name]);
- }
+ [$subscriptionId, $subscriptionRow] = $this->atomic(function () use ($values) {
+ $valuesToInsert = [];
+ $query = $this->db->getQueryBuilder();
+ foreach (array_keys($values) as $name) {
+ $valuesToInsert[$name] = $query->createNamedParameter($values[$name]);
+ }
+ $query->insert('calendarsubscriptions')
+ ->values($valuesToInsert)
+ ->executeStatement();
- $query->insert('calendarsubscriptions')
- ->values($valuesToInsert)
- ->executeStatement();
+ $subscriptionId = $query->getLastInsertId();
- $subscriptionId = $query->getLastInsertId();
+ $subscriptionRow = $this->getSubscriptionById($subscriptionId);
+ return [$subscriptionId, $subscriptionRow];
+ }, $this->db);
- $subscriptionRow = $this->getSubscriptionById($subscriptionId);
$this->dispatcher->dispatchTyped(new SubscriptionCreatedEvent($subscriptionId, $subscriptionRow));
- $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createSubscription', new GenericEvent(
- '\OCA\DAV\CalDAV\CalDavBackend::createSubscription',
- [
- 'subscriptionId' => $subscriptionId,
- 'subscriptionData' => $subscriptionRow,
- ]));
return $subscriptionId;
}
@@ -2553,24 +2752,20 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
}
- $query = $this->db->getQueryBuilder();
- $query->update('calendarsubscriptions')
- ->set('lastmodified', $query->createNamedParameter(time()));
- foreach ($newValues as $fieldName => $value) {
- $query->set($fieldName, $query->createNamedParameter($value));
- }
- $query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
- ->executeStatement();
+ $subscriptionRow = $this->atomic(function () use ($subscriptionId, $newValues) {
+ $query = $this->db->getQueryBuilder();
+ $query->update('calendarsubscriptions')
+ ->set('lastmodified', $query->createNamedParameter(time()));
+ foreach ($newValues as $fieldName => $value) {
+ $query->set($fieldName, $query->createNamedParameter($value));
+ }
+ $query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
+ ->executeStatement();
+
+ return $this->getSubscriptionById($subscriptionId);
+ }, $this->db);
- $subscriptionRow = $this->getSubscriptionById($subscriptionId);
$this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations));
- $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', new GenericEvent(
- '\OCA\DAV\CalDAV\CalDavBackend::updateSubscription',
- [
- 'subscriptionId' => $subscriptionId,
- 'subscriptionData' => $subscriptionRow,
- 'propertyMutations' => $mutations,
- ]));
return true;
});
@@ -2583,39 +2778,34 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return void
*/
public function deleteSubscription($subscriptionId) {
- $subscriptionRow = $this->getSubscriptionById($subscriptionId);
-
- $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription', new GenericEvent(
- '\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription',
- [
- 'subscriptionId' => $subscriptionId,
- 'subscriptionData' => $this->getSubscriptionById($subscriptionId),
- ]));
+ $this->atomic(function () use ($subscriptionId): void {
+ $subscriptionRow = $this->getSubscriptionById($subscriptionId);
- $query = $this->db->getQueryBuilder();
- $query->delete('calendarsubscriptions')
- ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
- ->executeStatement();
+ $query = $this->db->getQueryBuilder();
+ $query->delete('calendarsubscriptions')
+ ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
+ ->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)))
- ->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)))
+ ->executeStatement();
- $query->delete('calendarchanges')
- ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
- ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
- ->executeStatement();
+ $query->delete('calendarchanges')
+ ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
+ ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
+ ->executeStatement();
- $query->delete($this->dbObjectPropertiesTable)
- ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
- ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
- ->executeStatement();
+ $query->delete($this->dbObjectPropertiesTable)
+ ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
+ ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
+ ->executeStatement();
- if ($subscriptionRow) {
- $this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, []));
- }
+ if ($subscriptionRow) {
+ $this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, []));
+ }
+ }, $this->db);
}
/**
@@ -2671,13 +2861,13 @@ 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();
- $result = [];
- foreach ($stmt->fetchAll() as $row) {
- $result[] = [
+ $results = [];
+ while (($row = $stmt->fetch()) !== false) {
+ $results[] = [
'calendardata' => $row['calendardata'],
'uri' => $row['uri'],
'lastmodified' => $row['lastmodified'],
@@ -2687,7 +2877,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
$stmt->closeCursor();
- return $result;
+ return $results;
}
/**
@@ -2698,11 +2888,50 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return void
*/
public function deleteSchedulingObject($principalUri, $objectUri) {
+ $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);
+ }
}
/**
@@ -2714,6 +2943,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return void
*/
public function createSchedulingObject($principalUri, $objectUri, $objectData) {
+ $this->cachedObjects = [];
$query = $this->db->getQueryBuilder();
$query->insert('schedulingobjects')
->values([
@@ -2731,37 +2961,86 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* Adds a change record to the calendarchanges table.
*
* @param mixed $calendarId
- * @param string $objectUri
+ * @param string[] $objectUris
* @param int $operation 1 = add, 2 = modify, 3 = delete.
* @param int $calendarType
* @return void
*/
- protected function addChange($calendarId, $objectUri, $operation, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
+ protected function addChanges(int $calendarId, array $objectUris, int $operation, int $calendarType = self::CALENDAR_TYPE_CALENDAR): void {
+ $this->cachedObjects = [];
$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
- $query = $this->db->getQueryBuilder();
- $query->select('synctoken')
- ->from($table)
- ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
- $result = $query->executeQuery();
- $syncToken = (int)$result->fetchOne();
- $result->closeCursor();
+ $this->atomic(function () use ($calendarId, $objectUris, $operation, $calendarType, $table): void {
+ $query = $this->db->getQueryBuilder();
+ $query->select('synctoken')
+ ->from($table)
+ ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
+ $result = $query->executeQuery();
+ $syncToken = (int)$result->fetchOne();
+ $result->closeCursor();
- $query = $this->db->getQueryBuilder();
- $query->insert('calendarchanges')
- ->values([
- 'uri' => $query->createNamedParameter($objectUri),
- 'synctoken' => $query->createNamedParameter($syncToken),
- 'calendarid' => $query->createNamedParameter($calendarId),
- 'operation' => $query->createNamedParameter($operation),
- 'calendartype' => $query->createNamedParameter($calendarType),
- ])
- ->executeStatement();
+ $query = $this->db->getQueryBuilder();
+ $query->insert('calendarchanges')
+ ->values([
+ 'uri' => $query->createParameter('uri'),
+ 'synctoken' => $query->createNamedParameter($syncToken),
+ 'calendarid' => $query->createNamedParameter($calendarId),
+ 'operation' => $query->createNamedParameter($operation),
+ 'calendartype' => $query->createNamedParameter($calendarType),
+ 'created_at' => $query->createNamedParameter(time()),
+ ]);
+ foreach ($objectUris as $uri) {
+ $query->setParameter('uri', $uri);
+ $query->executeStatement();
+ }
- $stmt = $this->db->prepare("UPDATE `*PREFIX*$table` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?");
- $stmt->execute([
- $calendarId
- ]);
+ $query = $this->db->getQueryBuilder();
+ $query->update($table)
+ ->set('synctoken', $query->createNamedParameter($syncToken + 1, IQueryBuilder::PARAM_INT))
+ ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
+ ->executeStatement();
+ }, $this->db);
+ }
+
+ public function restoreChanges(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR): void {
+ $this->cachedObjects = [];
+
+ $this->atomic(function () use ($calendarId, $calendarType): void {
+ $qbAdded = $this->db->getQueryBuilder();
+ $qbAdded->select('uri')
+ ->from('calendarobjects')
+ ->where(
+ $qbAdded->expr()->andX(
+ $qbAdded->expr()->eq('calendarid', $qbAdded->createNamedParameter($calendarId)),
+ $qbAdded->expr()->eq('calendartype', $qbAdded->createNamedParameter($calendarType)),
+ $qbAdded->expr()->isNull('deleted_at'),
+ )
+ );
+ $resultAdded = $qbAdded->executeQuery();
+ $addedUris = $resultAdded->fetchAll(\PDO::FETCH_COLUMN);
+ $resultAdded->closeCursor();
+ // Track everything as changed
+ // Tracking the creation is not necessary because \OCA\DAV\CalDAV\CalDavBackend::getChangesForCalendar
+ // only returns the last change per object.
+ $this->addChanges($calendarId, $addedUris, 2, $calendarType);
+
+ $qbDeleted = $this->db->getQueryBuilder();
+ $qbDeleted->select('uri')
+ ->from('calendarobjects')
+ ->where(
+ $qbDeleted->expr()->andX(
+ $qbDeleted->expr()->eq('calendarid', $qbDeleted->createNamedParameter($calendarId)),
+ $qbDeleted->expr()->eq('calendartype', $qbDeleted->createNamedParameter($calendarType)),
+ $qbDeleted->expr()->isNotNull('deleted_at'),
+ )
+ );
+ $resultDeleted = $qbDeleted->executeQuery();
+ $deletedUris = array_map(function (string $uri) {
+ return str_replace('-deleted.ics', '.ics', $uri);
+ }, $resultDeleted->fetchAll(\PDO::FETCH_COLUMN));
+ $resultDeleted->closeCursor();
+ $this->addChanges($calendarId, $deletedUris, 3, $calendarType);
+ }, $this->db);
}
/**
@@ -2779,7 +3058,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @param string $calendarData
* @return array
*/
- public function getDenormalizedData($calendarData) {
+ public function getDenormalizedData(string $calendarData): array {
$vObject = Reader::read($calendarData);
$vEvents = [];
$componentType = null;
@@ -2793,7 +3072,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
if ($component->name !== 'VTIMEZONE') {
// Finding all VEVENTs, and track them
if ($component->name === 'VEVENT') {
- array_push($vEvents, $component);
+ $vEvents[] = $component;
if ($component->DTSTART) {
$hasDTSTART = true;
}
@@ -2829,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()) {
@@ -2861,7 +3148,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'size' => strlen($calendarData),
'componentType' => $componentType,
'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
- 'lastOccurence' => $lastOccurrence,
+ 'lastOccurence' => is_null($lastOccurrence) ? null : max(0, $lastOccurrence),
'uid' => $uid,
'classification' => $classification
];
@@ -2880,80 +3167,73 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
/**
- * @param IShareable $shareable
- * @param array $add
- * @param array $remove
+ * @param list<array{href: string, commonName: string, readOnly: bool}> $add
+ * @param list<string> $remove
*/
- public function updateShares($shareable, $add, $remove) {
- $calendarId = $shareable->getResourceId();
- $calendarRow = $this->getCalendarById($calendarId);
- $oldShares = $this->getShares($calendarId);
+ public function updateShares(IShareable $shareable, array $add, array $remove): void {
+ $this->atomic(function () use ($shareable, $add, $remove): void {
+ $calendarId = $shareable->getResourceId();
+ $calendarRow = $this->getCalendarById($calendarId);
+ if ($calendarRow === null) {
+ throw new \RuntimeException('Trying to update shares for non-existing calendar: ' . $calendarId);
+ }
+ $oldShares = $this->getShares($calendarId);
- $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateShares', new GenericEvent(
- '\OCA\DAV\CalDAV\CalDavBackend::updateShares',
- [
- 'calendarId' => $calendarId,
- 'calendarData' => $calendarRow,
- 'shares' => $oldShares,
- 'add' => $add,
- 'remove' => $remove,
- ]));
- $this->calendarSharingBackend->updateShares($shareable, $add, $remove);
+ $this->calendarSharingBackend->updateShares($shareable, $add, $remove, $oldShares);
- $this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent((int)$calendarId, $calendarRow, $oldShares, $add, $remove));
+ $this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent($calendarId, $calendarRow, $oldShares, $add, $remove));
+ }, $this->db);
}
/**
- * @param int $resourceId
- * @return array
+ * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}>
*/
- public function getShares($resourceId) {
+ public function getShares(int $resourceId): array {
return $this->calendarSharingBackend->getShares($resourceId);
}
+ public function preloadShares(array $resourceIds): void {
+ $this->calendarSharingBackend->preloadShares($resourceIds);
+ }
+
/**
* @param boolean $value
- * @param \OCA\DAV\CalDAV\Calendar $calendar
+ * @param Calendar $calendar
* @return string|null
*/
public function setPublishStatus($value, $calendar) {
- $calendarId = $calendar->getResourceId();
- $calendarData = $this->getCalendarById($calendarId);
- $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', new GenericEvent(
- '\OCA\DAV\CalDAV\CalDavBackend::updateShares',
- [
- 'calendarId' => $calendarId,
- 'calendarData' => $calendarData,
- 'public' => $value,
- ]));
+ return $this->atomic(function () use ($value, $calendar) {
+ $calendarId = $calendar->getResourceId();
+ $calendarData = $this->getCalendarById($calendarId);
- $query = $this->db->getQueryBuilder();
- if ($value) {
- $publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE);
- $query->insert('dav_shares')
- ->values([
- 'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()),
- 'type' => $query->createNamedParameter('calendar'),
- 'access' => $query->createNamedParameter(self::ACCESS_PUBLIC),
- 'resourceid' => $query->createNamedParameter($calendar->getResourceId()),
- 'publicuri' => $query->createNamedParameter($publicUri)
- ]);
- $query->executeStatement();
+ $query = $this->db->getQueryBuilder();
+ if ($value) {
+ $publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE);
+ $query->insert('dav_shares')
+ ->values([
+ 'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()),
+ 'type' => $query->createNamedParameter('calendar'),
+ 'access' => $query->createNamedParameter(self::ACCESS_PUBLIC),
+ 'resourceid' => $query->createNamedParameter($calendar->getResourceId()),
+ 'publicuri' => $query->createNamedParameter($publicUri)
+ ]);
+ $query->executeStatement();
- $this->dispatcher->dispatchTyped(new CalendarPublishedEvent((int)$calendarId, $calendarData, $publicUri));
- return $publicUri;
- }
- $query->delete('dav_shares')
- ->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
- ->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
- $query->executeStatement();
+ $this->dispatcher->dispatchTyped(new CalendarPublishedEvent($calendarId, $calendarData, $publicUri));
+ return $publicUri;
+ }
+ $query->delete('dav_shares')
+ ->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
+ ->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
+ $query->executeStatement();
- $this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent((int)$calendarId, $calendarData));
- return null;
+ $this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent($calendarId, $calendarData));
+ return null;
+ }, $this->db);
}
/**
- * @param \OCA\DAV\CalDAV\Calendar $calendar
+ * @param Calendar $calendar
* @return mixed
*/
public function getPublishStatus($calendar) {
@@ -2971,15 +3251,14 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
/**
* @param int $resourceId
- * @param array $acl
- * @return array
+ * @param list<array{privilege: string, principal: string, protected: bool}> $acl
+ * @return list<array{privilege: string, principal: string, protected: bool}>
*/
- public function applyShareAcl($resourceId, $acl) {
- return $this->calendarSharingBackend->applyShareAcl($resourceId, $acl);
+ public function applyShareAcl(int $resourceId, array $acl): array {
+ $shares = $this->calendarSharingBackend->getShares($resourceId);
+ return $this->calendarSharingBackend->applyShareAcl($shares, $acl);
}
-
-
/**
* update properties table
*
@@ -2989,127 +3268,172 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @param int $calendarType
*/
public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
- $objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType);
+ $this->cachedObjects = [];
+ $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $calendarType): void {
+ $objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType);
+
+ try {
+ $vCalendar = $this->readCalendarData($calendarData);
+ } catch (\Exception $ex) {
+ return;
+ }
- try {
- $vCalendar = $this->readCalendarData($calendarData);
- } catch (\Exception $ex) {
- return;
- }
+ $this->purgeProperties($calendarId, $objectId);
- $this->purgeProperties($calendarId, $objectId);
+ $query = $this->db->getQueryBuilder();
+ $query->insert($this->dbObjectPropertiesTable)
+ ->values(
+ [
+ 'calendarid' => $query->createNamedParameter($calendarId),
+ 'calendartype' => $query->createNamedParameter($calendarType),
+ 'objectid' => $query->createNamedParameter($objectId),
+ 'name' => $query->createParameter('name'),
+ 'parameter' => $query->createParameter('parameter'),
+ 'value' => $query->createParameter('value'),
+ ]
+ );
- $query = $this->db->getQueryBuilder();
- $query->insert($this->dbObjectPropertiesTable)
- ->values(
- [
- 'calendarid' => $query->createNamedParameter($calendarId),
- 'calendartype' => $query->createNamedParameter($calendarType),
- 'objectid' => $query->createNamedParameter($objectId),
- 'name' => $query->createParameter('name'),
- 'parameter' => $query->createParameter('parameter'),
- 'value' => $query->createParameter('value'),
- ]
- );
+ $indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO'];
+ foreach ($vCalendar->getComponents() as $component) {
+ if (!in_array($component->name, $indexComponents)) {
+ continue;
+ }
- $indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO'];
- foreach ($vCalendar->getComponents() as $component) {
- if (!in_array($component->name, $indexComponents)) {
- continue;
- }
+ foreach ($component->children() as $property) {
+ if (in_array($property->name, self::INDEXED_PROPERTIES, true)) {
+ $value = $property->getValue();
+ // is this a shitty db?
+ if (!$this->db->supports4ByteText()) {
+ $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
+ }
+ $value = mb_strcut($value, 0, 254);
- foreach ($component->children() as $property) {
- if (in_array($property->name, self::INDEXED_PROPERTIES, true)) {
- $value = $property->getValue();
- // is this a shitty db?
- if (!$this->db->supports4ByteText()) {
- $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
+ $query->setParameter('name', $property->name);
+ $query->setParameter('parameter', null);
+ $query->setParameter('value', mb_strcut($value, 0, 254));
+ $query->executeStatement();
}
- $value = mb_strcut($value, 0, 254);
-
- $query->setParameter('name', $property->name);
- $query->setParameter('parameter', null);
- $query->setParameter('value', $value);
- $query->executeStatement();
- }
-
- if (array_key_exists($property->name, self::$indexParameters)) {
- $parameters = $property->parameters();
- $indexedParametersForProperty = self::$indexParameters[$property->name];
- foreach ($parameters as $key => $value) {
- if (in_array($key, $indexedParametersForProperty)) {
- // is this a shitty db?
- if ($this->db->supports4ByteText()) {
- $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
+ if (array_key_exists($property->name, self::$indexParameters)) {
+ $parameters = $property->parameters();
+ $indexedParametersForProperty = self::$indexParameters[$property->name];
+
+ foreach ($parameters as $key => $value) {
+ if (in_array($key, $indexedParametersForProperty)) {
+ // is this a shitty db?
+ if ($this->db->supports4ByteText()) {
+ $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
+ }
+
+ $query->setParameter('name', $property->name);
+ $query->setParameter('parameter', mb_strcut($key, 0, 254));
+ $query->setParameter('value', mb_strcut($value, 0, 254));
+ $query->executeStatement();
}
-
- $query->setParameter('name', $property->name);
- $query->setParameter('parameter', mb_strcut($key, 0, 254));
- $query->setParameter('value', mb_strcut($value, 0, 254));
- $query->executeStatement();
}
}
}
}
- }
+ }, $this->db);
}
/**
* deletes all birthday calendars
*/
public function deleteAllBirthdayCalendars() {
- $query = $this->db->getQueryBuilder();
- $result = $query->select(['id'])->from('calendars')
- ->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)))
- ->executeQuery();
+ $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)))
+ ->executeQuery();
- $ids = $result->fetchAll();
- $result->closeCursor();
- foreach ($ids as $id) {
- $this->deleteCalendar(
- $id['id'],
- true // No data to keep in the trashbin, if the user re-enables then we regenerate
- );
- }
+ while (($row = $result->fetch()) !== false) {
+ $this->deleteCalendar(
+ $row['id'],
+ true // No data to keep in the trashbin, if the user re-enables then we regenerate
+ );
+ }
+ $result->closeCursor();
+ }, $this->db);
}
/**
* @param $subscriptionId
*/
public function purgeAllCachedEventsForSubscription($subscriptionId) {
- $query = $this->db->getQueryBuilder();
- $query->select('uri')
- ->from('calendarobjects')
- ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
- ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
- $stmt = $query->executeQuery();
+ $this->atomic(function () use ($subscriptionId): void {
+ $query = $this->db->getQueryBuilder();
+ $query->select('uri')
+ ->from('calendarobjects')
+ ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
+ ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
+ $stmt = $query->executeQuery();
- $uris = [];
- foreach ($stmt->fetchAll() as $row) {
- $uris[] = $row['uri'];
- }
- $stmt->closeCursor();
+ $uris = [];
+ while (($row = $stmt->fetch()) !== false) {
+ $uris[] = $row['uri'];
+ }
+ $stmt->closeCursor();
- $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)))
- ->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)))
+ ->executeStatement();
- $query->delete('calendarchanges')
- ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
- ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
- ->executeStatement();
+ $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)))
+ ->executeStatement();
- $query->delete($this->dbObjectPropertiesTable)
- ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
- ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
- ->executeStatement();
+ $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)))
+ ->executeStatement();
+
+ $this->addChanges($subscriptionId, $uris, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
+ }, $this->db);
+ }
- foreach ($uris as $uri) {
- $this->addChange($subscriptionId, $uri, 3, self::CALENDAR_TYPE_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);
}
/**
@@ -3147,6 +3471,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @param int $objectId
*/
protected function purgeProperties($calendarId, $objectId) {
+ $this->cachedObjects = [];
$query = $this->db->getQueryBuilder();
$query->delete($this->dbObjectPropertiesTable)
->where($query->expr()->eq('objectid', $query->createNamedParameter($objectId)))
@@ -3182,6 +3507,34 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
/**
+ * @throws \InvalidArgumentException
+ */
+ public function pruneOutdatedSyncTokens(int $keep, int $retention): int {
+ if ($keep < 0) {
+ throw new \InvalidArgumentException();
+ }
+
+ $query = $this->db->getQueryBuilder();
+ $query->select($query->func()->max('id'))
+ ->from('calendarchanges');
+
+ $result = $query->executeQuery();
+ $maxId = (int)$result->fetchOne();
+ $result->closeCursor();
+ if (!$maxId || $maxId < $keep) {
+ return 0;
+ }
+
+ $query = $this->db->getQueryBuilder();
+ $query->delete('calendarchanges')
+ ->where(
+ $query->expr()->lte('id', $query->createNamedParameter($maxId - $keep, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
+ $query->expr()->lte('created_at', $query->createNamedParameter($retention)),
+ );
+ return $query->executeStatement();
+ }
+
+ /**
* return legacy endpoint principal name to new principal name
*
* @param $principalUri
@@ -3270,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 75c815c3b0a..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,28 +31,16 @@ use Sabre\DAV\PropPatch;
* @property CalDavBackend $caldavBackend
*/
class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable, IMoveTarget {
-
- /** @var IConfig */
- private $config;
-
- /** @var IL10N */
- protected $l10n;
-
- /** @var bool */
- private $useTrashbin = true;
-
- /** @var LoggerInterface */
- private $logger;
-
- /**
- * Calendar constructor.
- *
- * @param BackendInterface $caldavBackend
- * @param $calendarInfo
- * @param IL10N $l10n
- * @param IConfig $config
- */
- public function __construct(BackendInterface $caldavBackend, $calendarInfo, IL10N $l10n, IConfig $config, LoggerInterface $logger) {
+ protected IL10N $l10n;
+ private bool $useTrashbin = true;
+
+ 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())
@@ -82,39 +50,21 @@ 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;
}
/**
- * Updates the list of shares.
- *
- * The first array is a list of people that are to be added to the
- * resource.
- *
- * Every element in the add array has the following properties:
- * * href - A url. Usually a mailto: address
- * * commonName - Usually a first and last name, or false
- * * summary - A description of the share, can also be false
- * * readOnly - A boolean value
- *
- * Every element in the remove array is just the address string.
- *
- * @param array $add
- * @param array $remove
- * @return void
+ * {@inheritdoc}
* @throws Forbidden
*/
- public function updateShares(array $add, array $remove) {
+ public function updateShares(array $add, array $remove): void {
if ($this->isShared()) {
throw new Forbidden();
}
@@ -131,19 +81,16 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
* * readOnly - boolean
* * summary - Optional, a description for the share
*
- * @return array
+ * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}>
*/
- public function getShares() {
+ public function getShares(): array {
if ($this->isShared()) {
return [];
}
return $this->caldavBackend->getShares($this->getResourceId());
}
- /**
- * @return int
- */
- public function getResourceId() {
+ public function getResourceId(): int {
return $this->calendarInfo['id'];
}
@@ -155,7 +102,9 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
}
/**
- * @return array
+ * @param int $resourceId
+ * @param list<array{privilege: string, principal: string, protected: bool}> $acl
+ * @return list<array{privilege: string, principal: ?string, protected: bool}>
*/
public function getACL() {
$acl = [
@@ -241,21 +190,23 @@ 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'
];
- return array_filter($acl, function ($rule) use ($allowedPrincipals) {
+ /** @var list<array{privilege: string, principal: string, protected: bool}> $acl */
+ $acl = array_filter($acl, function (array $rule) use ($allowedPrincipals): bool {
return \in_array($rule['principal'], $allowedPrincipals, true);
});
+ return $acl;
}
public function getChildACL() {
return $this->getACL();
}
- public function getOwner() {
+ public function getOwner(): ?string {
if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) {
return $this->calendarInfo['{http://owncloud.org/ns}owner-principal'];
}
@@ -263,20 +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();
- $shares = $this->caldavBackend->getShares($this->getResourceId());
- $shares = array_filter($shares, function ($share) use ($principal) {
- return $share['href'] === $principal;
- });
- if (empty($shares)) {
- throw new Forbidden();
- }
-
- $this->caldavBackend->updateShares($this, [], [
- $principal
- ]);
+ if ($this->isShared()) {
+ $this->caldavBackend->unshare($this, 'principal:' . $this->getPrincipalURI());
return;
}
@@ -400,7 +339,7 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
return isset($this->calendarInfo['{http://owncloud.org/ns}public']);
}
- protected function isShared() {
+ public function isShared() {
if (!isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) {
return false;
}
@@ -412,6 +351,13 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
return isset($this->calendarInfo['{http://calendarserver.org/ns/}source']);
}
+ public function isDeleted(): bool {
+ if (!isset($this->calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT])) {
+ return false;
+ }
+ return $this->calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT] !== null;
+ }
+
/**
* @inheritDoc
*/
@@ -427,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 {
@@ -441,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->getPrincipalUri());
+ 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 ceeba31800e..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,30 +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;
-
- public function __construct(BackendInterface $caldavBackend, $principalInfo, LoggerInterface $logger) {
+ private ?array $cachedChildren = null;
+
+ 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;
}
/**
@@ -97,6 +81,9 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome {
* @inheritdoc
*/
public function getChildren() {
+ if ($this->cachedChildren) {
+ return $this->cachedChildren;
+ }
$calendars = $this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']);
$objects = [];
foreach ($calendars as $calendar) {
@@ -136,6 +123,7 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome {
}
}
+ $this->cachedChildren = $objects;
return $objects;
}
@@ -159,7 +147,16 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome {
return new TrashbinHome($this->caldavBackend, $this->principalInfo);
}
- // Calendars
+ // Calendar - this covers all "regular" calendars, but not shared
+ // only check if the method is available
+ if ($this->caldavBackend instanceof CalDavBackend) {
+ $calendar = $this->caldavBackend->getCalendarByUri($this->principalInfo['uri'], $name);
+ if (!empty($calendar)) {
+ return new Calendar($this->caldavBackend, $calendar, $this->l10n, $this->config, $this->logger);
+ }
+ }
+
+ // Fallback to cover shared calendars
foreach ($this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']) as $calendar) {
if ($calendar['uri'] === $name) {
return new Calendar($this->caldavBackend, $calendar, $this->l10n, $this->config, $this->logger);
@@ -205,9 +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 406389e3a3d..5f912da732e 100644
--- a/apps/dav/lib/CalDAV/CalendarImpl.php
+++ b/apps/dav/lib/CalDAV/CalendarImpl.php
@@ -3,70 +3,48 @@
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>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have 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;
+use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
use Sabre\DAV\Exception\Conflict;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VEvent;
+use Sabre\VObject\Component\VTimeZone;
+use Sabre\VObject\ITip\Message;
+use Sabre\VObject\Property;
+use Sabre\VObject\Reader;
use function Sabre\Uri\split as uriSplit;
-class CalendarImpl implements ICreateFromString {
-
- /** @var CalDavBackend */
- private $backend;
-
- /** @var Calendar */
- private $calendar;
-
- /** @var array */
- private $calendarInfo;
-
- /**
- * CalendarImpl constructor.
- *
- * @param Calendar $calendar
- * @param array $calendarInfo
- * @param CalDavBackend $backend
- */
- 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,
+ ) {
}
/**
* @return string defining the technical unique key
* @since 13.0.0
*/
- public function getKey() {
- return $this->calendarInfo['id'];
+ public function getKey(): string {
+ return (string)$this->calendarInfo['id'];
}
/**
@@ -78,45 +56,60 @@ class CalendarImpl implements ICreateFromString {
/**
* In comparison to getKey() this function returns a human readable (maybe translated) name
- * @return null|string
* @since 13.0.0
*/
- public function getDisplayName() {
+ public function getDisplayName(): ?string {
return $this->calendarInfo['{DAV:}displayname'];
}
/**
* Calendar color
- * @return null|string
* @since 13.0.0
*/
- public function getDisplayColor() {
+ public function getDisplayColor(): ?string {
return $this->calendarInfo['{http://apple.com/ns/ical/}calendar-color'];
}
- /**
- * @param string $pattern which should match within the $searchProperties
- * @param array $searchProperties defines the properties within the query pattern should match
- * @param array $options - optional parameters:
- * ['timerange' => ['start' => new DateTime(...), 'end' => new DateTime(...)]]
- * @param integer|null $limit - limit number of search results
- * @param integer|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($pattern, array $searchProperties = [], array $options = [], $limit = null, $offset = null) {
+ public function getSchedulingTransparency(): ?ScheduleCalendarTransp {
+ return $this->calendarInfo['{' . \OCA\DAV\CalDAV\Schedule\Plugin::NS_CALDAV . '}schedule-calendar-transp'];
+ }
+
+ public function getSchedulingTimezone(): ?VTimeZone {
+ $tzProp = '{' . \OCA\DAV\CalDAV\Schedule\Plugin::NS_CALDAV . '}calendar-timezone';
+ if (!isset($this->calendarInfo[$tzProp])) {
+ return null;
+ }
+ // This property contains a VCALENDAR with a single VTIMEZONE
+ /** @var string $timezoneProp */
+ $timezoneProp = $this->calendarInfo[$tzProp];
+ /** @var VCalendar $vobj */
+ $vobj = Reader::read($timezoneProp);
+ $components = $vobj->getComponents();
+ if (empty($components)) {
+ return null;
+ }
+ /** @var VTimeZone $vtimezone */
+ $vtimezone = $components[0];
+ return $vtimezone;
+ }
+
+ 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 integer build up using \OCP\Constants
+ * @return int build up using \OCP\Constants
* @since 13.0.0
*/
- public function getPermissions() {
+ public function getPermissions(): int {
$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;
@@ -135,19 +128,43 @@ class CalendarImpl implements ICreateFromString {
}
/**
- * Create a new calendar event for this calendar
- * by way of an ICS string
- *
- * @param string $name the file name - needs to contan the .ics ending
- * @param string $calendarData a string containing a valid VEVENT ics
- *
- * @throws CalendarException
+ * @since 32.0.0
*/
- public function createFromString(string $name, string $calendarData): void {
- $server = new InvitationResponseServer(false);
+ 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 {
+ return $this->calendar->isDeleted();
+ }
+
+ /**
+ * @since 31.0.0
+ */
+ 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->server->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
@@ -163,18 +180,113 @@ class CalendarImpl implements ICreateFromString {
// Force calendar change URI
/** @var Schedule\Plugin $schedulingPlugin */
- $schedulingPlugin = $server->server->getPlugin('caldav-schedule');
+ $schedulingPlugin = $server->getPlugin('caldav-schedule');
$schedulingPlugin->setPathOfCalendarObjectChange($fullCalendarFilename);
$stream = fopen('php://memory', 'rb+');
fwrite($stream, $calendarData);
rewind($stream);
try {
- $server->server->createFile($fullCalendarFilename, $stream);
+ $server->createFile($fullCalendarFilename, $stream);
} catch (Conflict $e) {
throw new CalendarException('Could not create new calendar event: ' . $e->getMessage(), 0, $e);
} finally {
fclose($stream);
}
}
+
+ 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
+ */
+ public function handleIMipMessage(string $name, string $calendarData): void {
+ $server = $this->getInvitationResponseServer();
+
+ /** @var CustomPrincipalPlugin $plugin */
+ $plugin = $server->getServer()->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
+ $plugin->setCurrentPrincipal($this->calendar->getPrincipalURI());
+
+ if (empty($this->calendarInfo['uri'])) {
+ throw new CalendarException('Could not write to calendar as URI parameter is missing');
+ }
+ // Force calendar change URI
+ /** @var Schedule\Plugin $schedulingPlugin */
+ $schedulingPlugin = $server->getServer()->getPlugin('caldav-schedule');
+ // Let sabre handle the rest
+ $iTipMessage = new Message();
+ /** @var VCalendar $vObject */
+ $vObject = Reader::read($calendarData);
+ /** @var VEvent $vEvent */
+ $vEvent = $vObject->{'VEVENT'};
+
+ if ($vObject->{'METHOD'} === null) {
+ throw new CalendarException('No Method provided for scheduling data. Could not process message');
+ }
+
+ if (!isset($vEvent->{'ORGANIZER'}) || !isset($vEvent->{'ATTENDEE'})) {
+ throw new CalendarException('Could not process scheduling data, neccessary data missing from ICAL');
+ }
+ $organizer = $vEvent->{'ORGANIZER'}->getValue();
+ $attendee = $vEvent->{'ATTENDEE'}->getValue();
+
+ $iTipMessage->method = $vObject->{'METHOD'}->getValue();
+ if ($iTipMessage->method === 'REQUEST') {
+ $iTipMessage->sender = $organizer;
+ $iTipMessage->recipient = $attendee;
+ } elseif ($iTipMessage->method === 'REPLY') {
+ if ($server->isExternalAttendee($vEvent->{'ATTENDEE'}->getValue())) {
+ $iTipMessage->recipient = $organizer;
+ } else {
+ $iTipMessage->recipient = $attendee;
+ }
+ $iTipMessage->sender = $attendee;
+ } elseif ($iTipMessage->method === 'CANCEL') {
+ $iTipMessage->recipient = $attendee;
+ $iTipMessage->sender = $organizer;
+ }
+ $iTipMessage->uid = isset($vEvent->{'UID'}) ? $vEvent->{'UID'}->getValue() : '';
+ $iTipMessage->component = 'VEVENT';
+ $iTipMessage->sequence = isset($vEvent->{'SEQUENCE'}) ? (int)$vEvent->{'SEQUENCE'}->getValue() : 0;
+ $iTipMessage->message = $vObject;
+ $server->server->emit('schedule', [$iTipMessage]);
+ }
+
+ 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 c927254fba3..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,
- array $calendarInfo,
- array $objectData) {
+ public function __construct(
+ CalDavBackend $caldavBackend,
+ protected IL10N $l10n,
+ array $calendarInfo,
+ 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,28 +76,29 @@ 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) {
case 'CREATED':
case 'DTSTART':
case 'RRULE':
+ case 'RECURRENCE-ID':
+ case 'RDATE':
case 'DURATION':
case 'DTEND':
case 'CLASS':
+ case 'EXRULE':
+ case 'EXDATE':
case 'UID':
break;
case 'SUMMARY':
@@ -162,4 +142,11 @@ class CalendarObject extends \Sabre\CalDAV\CalendarObject {
public function getPrincipalUri(): string {
return $this->calendarInfo['principaluri'];
}
+
+ public function getOwner(): ?string {
+ if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) {
+ return $this->calendarInfo['{http://owncloud.org/ns}owner-principal'];
+ }
+ return parent::getOwner();
+ }
}
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
new file mode 100644
index 00000000000..63395e7ce1c
--- /dev/null
+++ b/apps/dav/lib/CalDAV/EventComparisonService.php
@@ -0,0 +1,100 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\CalDAV;
+
+use OCA\DAV\CalDAV\Schedule\IMipService;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VEvent;
+
+class EventComparisonService {
+
+ /** @var string[] */
+ private const EVENT_DIFF = [
+ 'RECURRENCE-ID',
+ 'RRULE',
+ 'SEQUENCE',
+ 'LAST-MODIFIED'
+ ];
+
+
+ /**
+ * If found, remove the event from $eventsToFilter that
+ * is identical to the passed $filterEvent
+ * and return whether an identical event was found
+ *
+ * This function takes into account the SEQUENCE,
+ * RRULE, RECURRENCE-ID and LAST-MODIFIED parameters
+ *
+ * @param VEvent $filterEvent
+ * @param array $eventsToFilter
+ * @return bool true if there was an identical event found and removed, false if there wasn't
+ */
+ private function removeIfUnchanged(VEvent $filterEvent, array &$eventsToFilter): bool {
+ $filterEventData = [];
+ 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) {
+ $eventToFilterData[] = IMipService::readPropertyWithDefault($eventToFilter, $eventDiff, '');
+ }
+ // events are identical and can be removed
+ if ($filterEventData === $eventToFilterData) {
+ unset($eventsToFilter[$k]);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Compare two VCalendars with each other and find all changed elements
+ *
+ * Returns an array of old and new events
+ *
+ * Old events are only detected if they are also changed
+ * If there is no corresponding old event for a VEvent, it
+ * has been newly created
+ *
+ * @param VCalendar $new
+ * @param VCalendar|null $old
+ * @return array<string, VEvent[]|null>
+ */
+ public function findModified(VCalendar $new, ?VCalendar $old): array {
+ $newEventComponents = $new->getComponents();
+
+ foreach ($newEventComponents as $k => $event) {
+ if (!$event instanceof VEvent) {
+ unset($newEventComponents[$k]);
+ }
+ }
+
+ if (empty($old)) {
+ return ['old' => null, 'new' => $newEventComponents];
+ }
+
+ $oldEventComponents = $old->getComponents();
+ if (is_array($oldEventComponents) && !empty($oldEventComponents)) {
+ foreach ($oldEventComponents as $k => $event) {
+ if (!$event instanceof VEvent) {
+ unset($oldEventComponents[$k]);
+ continue;
+ }
+ if ($this->removeIfUnchanged($event, $newEventComponents)) {
+ unset($oldEventComponents[$k]);
+ }
+ }
+ }
+
+ return ['old' => array_values($oldEventComponents), 'new' => array_values($newEventComponents)];
+ }
+}
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
new file mode 100644
index 00000000000..c2c474a90fe
--- /dev/null
+++ b/apps/dav/lib/CalDAV/FreeBusy/FreeBusyGenerator.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV\FreeBusy;
+
+use Sabre\VObject\Component\VCalendar;
+
+/**
+ * @psalm-suppress PropertyNotSetInConstructor
+ */
+class FreeBusyGenerator extends \Sabre\VObject\FreeBusyGenerator {
+
+ public function __construct() {
+ parent::__construct();
+ }
+
+ public function getVCalendar(): VCalendar {
+ return new VCalendar();
+ }
+}
diff --git a/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php b/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php
index ae568720c55..08dc10f7bf4 100644
--- a/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php
+++ b/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php
@@ -1,29 +1,13 @@
<?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;
use OCP\IConfig;
-use OCP\ILogger;
+use Psr\Log\LoggerInterface;
use Sabre\HTTP\ResponseInterface;
use Sabre\VObject\DateTimeParser;
use Sabre\VObject\InvalidDataException;
@@ -35,24 +19,16 @@ use Sabre\VObject\Property\ICalendar\Duration;
* @package OCA\DAV\CalDAV\ICSExportPlugin
*/
class ICSExportPlugin extends \Sabre\CalDAV\ICSExportPlugin {
-
- /** @var IConfig */
- private $config;
-
- /** @var ILogger */
- private $logger;
-
/** @var string */
private const DEFAULT_REFRESH_INTERVAL = 'PT4H';
/**
* ICSExportPlugin constructor.
- *
- * @param IConfig $config
*/
- public function __construct(IConfig $config, ILogger $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 9c801a08a26..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,
+ ) {
}
/**
@@ -98,7 +77,7 @@ abstract class ExternalCalendar implements CalDAV\ICalendar, DAV\IProperties {
* @return bool
*/
public static function isAppGeneratedCalendar(string $calendarUri):bool {
- return strpos($calendarUri, self::PREFIX) === 0 && substr_count($calendarUri, self::DELIMITER) >= 2;
+ return str_starts_with($calendarUri, self::PREFIX) && substr_count($calendarUri, self::DELIMITER) >= 2;
}
/**
@@ -126,6 +105,6 @@ abstract class ExternalCalendar implements CalDAV\ICalendar, DAV\IProperties {
* @return bool
*/
public static function doesViolateReservedName(string $calendarUri):bool {
- return strpos($calendarUri, self::PREFIX) === 0;
+ return str_starts_with($calendarUri, self::PREFIX);
}
}
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 5317dc1b169..c8a7109abde 100644
--- a/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php
+++ b/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php
@@ -1,44 +1,35 @@
<?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;
class InvitationResponseServer {
-
/** @var \OCA\DAV\Connector\Sabre\Server */
public $server;
@@ -47,21 +38,23 @@ class InvitationResponseServer {
*/
public function __construct(bool $public = true) {
$baseUri = \OC::$WEBROOT . '/remote.php/dav/';
- $logger = \OC::$server->getLogger();
- /** @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,21 +82,21 @@ 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()));
+ $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
- $this->server->on('beforeMethod:*', function () use ($root) {
+ $this->server->on('beforeMethod:*', function () use ($root): void {
// 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);
@@ -126,7 +119,11 @@ class InvitationResponseServer {
public function isExternalAttendee(string $principalUri): bool {
/** @var \Sabre\DAVACL\Plugin $aclPlugin */
- $aclPlugin = $this->server->getPlugin('acl');
+ $aclPlugin = $this->getServer()->getPlugin('acl');
return $aclPlugin->getPrincipalByUri($principalUri) === null;
}
+
+ public function getServer(): \OCA\DAV\Connector\Sabre\Server {
+ return $this->server;
+ }
}
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 19c72ffa0e9..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;
@@ -34,6 +15,8 @@ use OCP\IDBConnection;
* Class ProxyMapper
*
* @package OCA\DAV\CalDAV\Proxy
+ *
+ * @template-extends QBMapper<Proxy>
*/
class ProxyMapper extends QBMapper {
public const PERMISSION_READ = 1;
diff --git a/apps/dav/lib/CalDAV/PublicCalendar.php b/apps/dav/lib/CalDAV/PublicCalendar.php
index 4a29c8d237a..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;
@@ -84,7 +66,7 @@ class PublicCalendar extends Calendar {
* public calendars are always shared
* @return bool
*/
- protected function isShared() {
+ public function isShared() {
return true;
}
}
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 4f7dfea2682..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 97e942f9da2..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,
+ ) {
}
/**
@@ -114,28 +87,28 @@ class PublishPlugin extends ServerPlugin {
$this->server = $server;
$this->server->on('method:POST', [$this, 'httpPost']);
- $this->server->on('propFind', [$this, 'propFind']);
+ $this->server->on('propFind', [$this, 'propFind']);
}
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());
if ($this->config->getAppValue('dav', 'limitAddressBookAndCalendarSharingToOwner', 'no') === 'yes') {
- $canShare &= ($node->getOwner() === $node->getPrincipalURI());
- $canPublish &= ($node->getOwner() === $node->getPrincipalURI());
+ $canShare = $canShare && ($node->getOwner() === $node->getPrincipalURI());
+ $canPublish = $canPublish && ($node->getOwner() === $node->getPrincipalURI());
}
return new AllowedSharingModes($canShare, $canPublish);
@@ -155,8 +128,8 @@ class PublishPlugin extends ServerPlugin {
$path = $request->getPath();
// Only handling xml
- $contentType = $request->getHeader('Content-Type');
- if (strpos($contentType, 'application/xml') === false && strpos($contentType, 'text/xml') === false) {
+ $contentType = (string)$request->getHeader('Content-Type');
+ if (!str_contains($contentType, 'application/xml') && !str_contains($contentType, 'text/xml')) {
return;
}
@@ -182,74 +155,74 @@ 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) {
- return;
- }
- $this->server->transactionType = 'post-publish-calendar';
+ // We can only deal with IShareableCalendar objects
+ if (!$node instanceof Calendar) {
+ return;
+ }
+ $this->server->transactionType = 'post-publish-calendar';
- // Getting ACL info
- $acl = $this->server->getPlugin('acl');
+ // Getting ACL info
+ $acl = $this->server->getPlugin('acl');
- // If there's no ACL support, we allow everything
- if ($acl) {
- /** @var \Sabre\DAVACL\Plugin $acl */
- $acl->checkPrivileges($path, '{DAV:}write');
+ // If there's no ACL support, we allow everything
+ if ($acl) {
+ /** @var \Sabre\DAVACL\Plugin $acl */
+ $acl->checkPrivileges($path, '{DAV:}write');
- $limitSharingToOwner = $this->config->getAppValue('dav', 'limitAddressBookAndCalendarSharingToOwner', 'no') === 'yes';
- $isOwner = $acl->getCurrentUserPrincipal() === $node->getOwner();
- if ($limitSharingToOwner && !$isOwner) {
- return;
+ $limitSharingToOwner = $this->config->getAppValue('dav', 'limitAddressBookAndCalendarSharingToOwner', 'no') === 'yes';
+ $isOwner = $acl->getCurrentUserPrincipal() === $node->getOwner();
+ if ($limitSharingToOwner && !$isOwner) {
+ return;
+ }
}
- }
- $node->setPublishStatus(true);
+ $node->setPublishStatus(true);
- // iCloud sends back the 202, so we will too.
- $response->setStatus(202);
+ // iCloud sends back the 202, so we will too.
+ $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.
- $response->setHeader('X-Sabre-Status', 'everything-went-well');
+ // 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');
- // Breaking the event chain
- return false;
+ // 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) {
- return;
- }
- $this->server->transactionType = 'post-unpublish-calendar';
+ // We can only deal with IShareableCalendar objects
+ if (!$node instanceof Calendar) {
+ return;
+ }
+ $this->server->transactionType = 'post-unpublish-calendar';
- // Getting ACL info
- $acl = $this->server->getPlugin('acl');
+ // Getting ACL info
+ $acl = $this->server->getPlugin('acl');
- // If there's no ACL support, we allow everything
- if ($acl) {
- /** @var \Sabre\DAVACL\Plugin $acl */
- $acl->checkPrivileges($path, '{DAV:}write');
+ // If there's no ACL support, we allow everything
+ if ($acl) {
+ /** @var \Sabre\DAVACL\Plugin $acl */
+ $acl->checkPrivileges($path, '{DAV:}write');
- $limitSharingToOwner = $this->config->getAppValue('dav', 'limitAddressBookAndCalendarSharingToOwner', 'no') === 'yes';
- $isOwner = $acl->getCurrentUserPrincipal() === $node->getOwner();
- if ($limitSharingToOwner && !$isOwner) {
- return;
+ $limitSharingToOwner = $this->config->getAppValue('dav', 'limitAddressBookAndCalendarSharingToOwner', 'no') === 'yes';
+ $isOwner = $acl->getCurrentUserPrincipal() === $node->getOwner();
+ if ($limitSharingToOwner && !$isOwner) {
+ return;
+ }
}
- }
- $node->setPublishStatus(false);
+ $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.
- $response->setHeader('X-Sabre-Status', 'everything-went-well');
+ // 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');
- // Breaking the event chain
- return false;
+ // Breaking the event chain
+ return false;
}
}
diff --git a/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php b/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php
index 35bce872bf8..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,
+ ) {
}
/**
@@ -55,7 +29,7 @@ class Publisher implements XmlSerializable {
}
/**
- * The xmlSerialize metod is called during xml writing.
+ * The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
diff --git a/apps/dav/lib/CalDAV/Reminder/Backend.php b/apps/dav/lib/CalDAV/Reminder/Backend.php
index b0476e9594c..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'],
@@ -114,17 +89,17 @@ class Backend {
* @return int The insert id
*/
public function insertReminder(int $calendarId,
- int $objectId,
- string $uid,
- bool $isRecurring,
- int $recurrenceId,
- bool $isRecurrenceException,
- string $eventHash,
- string $alarmHash,
- string $type,
- bool $isRelative,
- int $notificationDate,
- bool $isRepeatBased):int {
+ int $objectId,
+ string $uid,
+ bool $isRecurring,
+ int $recurrenceId,
+ bool $isRecurrenceException,
+ string $eventHash,
+ string $alarmHash,
+ string $type,
+ bool $isRelative,
+ int $notificationDate,
+ bool $isRepeatBased):int {
$query = $this->db->getQueryBuilder();
$query->insert('calendar_reminders')
->values([
@@ -141,7 +116,7 @@ class Backend {
'notification_date' => $query->createNamedParameter($notificationDate),
'is_repeat_based' => $query->createNamedParameter($isRepeatBased ? 1 : 0),
])
- ->execute();
+ ->executeStatement();
return $query->getLastInsertId();
}
@@ -153,12 +128,12 @@ class Backend {
* @param int $newNotificationDate
*/
public function updateReminder(int $reminderId,
- int $newNotificationDate):void {
+ int $newNotificationDate):void {
$query = $this->db->getQueryBuilder();
$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 a6b439c0b4f..31d60f1531d 100644
--- a/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php
+++ b/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php
@@ -3,27 +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>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have 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;
@@ -41,11 +22,13 @@ interface INotificationProvider {
* Send notification
*
* @param VEvent $vevent
- * @param string $calendarDisplayName
+ * @param string|null $calendarDisplayName
+ * @param string[] $principalEmailAddresses All email addresses associated to the principal owning the calendar object
* @param IUser[] $users
* @return void
*/
public function send(VEvent $vevent,
- string $calendarDisplayName,
- array $users = []): void;
+ ?string $calendarDisplayName,
+ 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 044e5fac4e2..94edff98e52 100644
--- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php
+++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php
@@ -3,39 +3,18 @@
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>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have 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;
use OCA\DAV\CalDAV\Reminder\INotificationProvider;
use OCP\IConfig;
use OCP\IL10N;
-use OCP\ILogger;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\L10N\IFactory as L10NFactory;
+use Psr\Log\LoggerInterface;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\DateTimeParser;
use Sabre\VObject\Property;
@@ -50,51 +29,33 @@ abstract class AbstractProvider implements INotificationProvider {
/** @var string */
public const NOTIFICATION_TYPE = '';
- /** @var ILogger */
- protected $logger;
-
- /** @var L10NFactory */
- protected $l10nFactory;
-
/** @var IL10N[] */
private $l10ns;
/** @var string */
private $fallbackLanguage;
- /** @var IURLGenerator */
- protected $urlGenerator;
-
- /** @var IConfig */
- protected $config;
-
- /**
- * @param ILogger $logger
- * @param L10NFactory $l10nFactory
- * @param IConfig $config
- * @param IUrlGenerator $urlGenerator
- */
- public function __construct(ILogger $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,
+ ) {
}
/**
* Send notification
*
* @param VEvent $vevent
- * @param string $calendarDisplayName
+ * @param string|null $calendarDisplayName
+ * @param string[] $principalEmailAddresses
* @param IUser[] $users
* @return void
*/
abstract public function send(VEvent $vevent,
- string $calendarDisplayName,
- array $users = []): void;
+ ?string $calendarDisplayName,
+ array $principalEmailAddresses,
+ array $users = []): void;
/**
* @return string
@@ -139,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,
@@ -189,4 +150,8 @@ abstract class AbstractProvider implements INotificationProvider {
return clone $vevent->DTSTART;
}
+
+ protected function getCalendarDisplayNameFallback(string $lang): string {
+ return $this->getL10NForLang($lang)->t('Untitled calendar');
+ }
}
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 456b9f8b42d..0fd39a9e459 100644
--- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php
+++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php
@@ -3,42 +3,22 @@
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;
use DateTime;
use OCP\IConfig;
use OCP\IL10N;
-use OCP\ILogger;
use OCP\IURLGenerator;
+use OCP\IUser;
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;
use Sabre\VObject\Parameter;
@@ -50,44 +30,46 @@ use Sabre\VObject\Property;
* @package OCA\DAV\CalDAV\Reminder\NotificationProvider
*/
class EmailProvider extends AbstractProvider {
-
/** @var string */
public const NOTIFICATION_TYPE = 'EMAIL';
- /** @var IMailer */
- private $mailer;
-
- /**
- * @param IConfig $config
- * @param IMailer $mailer
- * @param ILogger $logger
- * @param L10NFactory $l10nFactory
- * @param IUrlGenerator $urlGenerator
- */
- public function __construct(IConfig $config,
- IMailer $mailer,
- ILogger $logger,
- L10NFactory $l10nFactory,
- IURLGenerator $urlGenerator) {
+ public function __construct(
+ IConfig $config,
+ private IMailer $mailer,
+ LoggerInterface $logger,
+ L10NFactory $l10nFactory,
+ IURLGenerator $urlGenerator,
+ ) {
parent::__construct($logger, $l10nFactory, $urlGenerator, $config);
- $this->mailer = $mailer;
}
/**
* Send out notification via email
*
* @param VEvent $vevent
- * @param string $calendarDisplayName
+ * @param string|null $calendarDisplayName
+ * @param string[] $principalEmailAddresses
* @param array $users
* @throws \Exception
*/
public function send(VEvent $vevent,
- string $calendarDisplayName,
- array $users = []):void {
+ ?string $calendarDisplayName,
+ array $principalEmailAddresses,
+ array $users = []):void {
$fallbackLanguage = $this->getFallbackLanguage();
+ $organizerEmailAddress = null;
+ if (isset($vevent->ORGANIZER)) {
+ $organizerEmailAddress = $this->getEMailAddressOfAttendee($vevent->ORGANIZER);
+ }
+
$emailAddressesOfSharees = $this->getEMailAddressesOfAllUsersWithWriteAccessToCalendar($users);
- $emailAddressesOfAttendees = $this->getAllEMailAddressesFromEvent($vevent);
+ $emailAddressesOfAttendees = [];
+ if (count($principalEmailAddresses) === 0
+ || ($organizerEmailAddress && in_array($organizerEmailAddress, $principalEmailAddresses, true))
+ ) {
+ $emailAddressesOfAttendees = $this->getAllEMailAddressesFromEvent($vevent);
+ }
// Quote from php.net:
// If the input arrays have the same string keys, then the later value for that key will overwrite the previous one.
@@ -105,12 +87,12 @@ 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();
$this->addSubjectAndHeading($template, $l10n, $vevent);
- $this->addBulletList($template, $l10n, $calendarDisplayName, $vevent);
+ $this->addBulletList($template, $l10n, $calendarDisplayName ?? $this->getCalendarDisplayNameFallback($lang), $vevent);
$template->addFooter();
foreach ($emailAddresses as $emailAddress) {
@@ -126,6 +108,7 @@ class EmailProvider extends AbstractProvider {
}
$message->setTo([$emailAddress]);
$message->useTemplate($template);
+ $message->setAutoSubmitted(AutoSubmitted::VALUE_AUTO_GENERATED);
try {
$failed = $this->mailer->send($message);
@@ -133,7 +116,7 @@ class EmailProvider extends AbstractProvider {
$this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]);
}
} catch (\Exception $ex) {
- $this->logger->logException($ex, ['app' => 'dav']);
+ $this->logger->error($ex->getMessage(), ['app' => 'dav', 'exception' => $ex]);
}
}
}
@@ -156,9 +139,9 @@ class EmailProvider extends AbstractProvider {
* @param array $eventData
*/
private function addBulletList(IEMailTemplate $template,
- IL10N $l10n,
- string $calendarDisplayName,
- VEvent $vevent):void {
+ IL10N $l10n,
+ string $calendarDisplayName,
+ VEvent $vevent):void {
$template->addBodyListItem($calendarDisplayName, $l10n->t('Calendar:'),
$this->getAbsoluteImagePath('actions/info.png'));
@@ -166,19 +149,15 @@ 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'));
}
}
- /**
- * @param string $path
- * @return string
- */
private function getAbsoluteImagePath(string $path):string {
return $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->imagePath('core', $path)
@@ -201,7 +180,7 @@ class EmailProvider extends AbstractProvider {
$organizerEMail = substr($organizer->getValue(), 7);
- if ($organizerEMail === false || !$this->mailer->validateMailAddress($organizerEMail)) {
+ if (!$this->mailer->validateMailAddress($organizerEMail)) {
return null;
}
@@ -214,12 +193,11 @@ class EmailProvider extends AbstractProvider {
}
/**
- * @param array $emails
- * @param string $defaultLanguage
- * @return array
+ * @param array<string, array{LANG?: string}> $emails
+ * @return array<string, string[]>
*/
private function sortEMailAddressesByLanguage(array $emails,
- string $defaultLanguage):array {
+ string $defaultLanguage):array {
$sortedByLanguage = [];
foreach ($emails as $emailAddress => $parameters) {
@@ -241,7 +219,7 @@ class EmailProvider extends AbstractProvider {
/**
* @param VEvent $vevent
- * @return array
+ * @return array<string, array{LANG?: string}>
*/
private function getAllEMailAddressesFromEvent(VEvent $vevent):array {
$emailAddresses = [];
@@ -272,7 +250,10 @@ class EmailProvider extends AbstractProvider {
$emailAddressesOfDelegates = $delegates->getParts();
foreach ($emailAddressesOfDelegates as $addressesOfDelegate) {
if (strcasecmp($addressesOfDelegate, 'mailto:') === 0) {
- $emailAddresses[substr($addressesOfDelegate, 7)] = [];
+ $delegateEmail = substr($addressesOfDelegate, 7);
+ if ($this->mailer->validateMailAddress($delegateEmail)) {
+ $emailAddresses[$delegateEmail] = [];
+ }
}
}
@@ -284,7 +265,7 @@ class EmailProvider extends AbstractProvider {
$properties = [];
$langProp = $attendee->offsetGet('LANG');
- if ($langProp instanceof VObject\Parameter) {
+ if ($langProp instanceof VObject\Parameter && $langProp->getValue() !== null) {
$properties['LANG'] = $langProp->getValue();
}
@@ -294,18 +275,15 @@ class EmailProvider extends AbstractProvider {
}
if (isset($vevent->ORGANIZER) && $this->hasAttendeeMailURI($vevent->ORGANIZER)) {
- $emailAddresses[$this->getEMailAddressOfAttendee($vevent->ORGANIZER)] = [];
+ $organizerEmailAddress = $this->getEMailAddressOfAttendee($vevent->ORGANIZER);
+ if ($organizerEmailAddress !== null) {
+ $emailAddresses[$organizerEmailAddress] = [];
+ }
}
return $emailAddresses;
}
-
-
- /**
- * @param VObject\Property $attendee
- * @return string
- */
private function getCUTypeOfAttendee(VObject\Property $attendee):string {
$cuType = $attendee->offsetGet('CUTYPE');
if ($cuType instanceof VObject\Parameter) {
@@ -315,10 +293,6 @@ class EmailProvider extends AbstractProvider {
return 'INDIVIDUAL';
}
- /**
- * @param VObject\Property $attendee
- * @return string
- */
private function getPartstatOfAttendee(VObject\Property $attendee):string {
$partstat = $attendee->offsetGet('PARTSTAT');
if ($partstat instanceof VObject\Parameter) {
@@ -328,29 +302,25 @@ class EmailProvider extends AbstractProvider {
return 'NEEDS-ACTION';
}
- /**
- * @param VObject\Property $attendee
- * @return bool
- */
- private function hasAttendeeMailURI(VObject\Property $attendee):bool {
+ private function hasAttendeeMailURI(VObject\Property $attendee): bool {
return stripos($attendee->getValue(), 'mailto:') === 0;
}
- /**
- * @param VObject\Property $attendee
- * @return string|null
- */
- private function getEMailAddressOfAttendee(VObject\Property $attendee):?string {
+ private function getEMailAddressOfAttendee(VObject\Property $attendee): ?string {
if (!$this->hasAttendeeMailURI($attendee)) {
return null;
}
+ $attendeeEMail = substr($attendee->getValue(), 7);
+ if (!$this->mailer->validateMailAddress($attendeeEMail)) {
+ return null;
+ }
- return substr($attendee->getValue(), 7);
+ return $attendeeEMail;
}
/**
- * @param array $users
- * @return array
+ * @param IUser[] $users
+ * @return array<string, array{LANG?: string}>
*/
private function getEMailAddressesOfAllUsersWithWriteAccessToCalendar(array $users):array {
$emailAddresses = [];
@@ -373,12 +343,9 @@ class EmailProvider extends AbstractProvider {
}
/**
- * @param IL10N $l10n
- * @param VEvent $vevent
- * @return string
* @throws \Exception
*/
- private function generateDateString(IL10N $l10n, VEvent $vevent):string {
+ private function generateDateString(IL10N $l10n, VEvent $vevent): string {
$isAllDay = $vevent->DTSTART instanceof Property\ICalendar\Date;
/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
@@ -444,57 +411,27 @@ class EmailProvider extends AbstractProvider {
. ' (' . $startTimezone . ')';
}
- /**
- * @param DateTime $dtStart
- * @param DateTime $dtEnd
- * @return bool
- */
private function isDayEqual(DateTime $dtStart,
- DateTime $dtEnd):bool {
+ DateTime $dtEnd):bool {
return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
}
- /**
- * @param IL10N $l10n
- * @param DateTime $dt
- * @return string
- */
private function getWeekDayName(IL10N $l10n, DateTime $dt):string {
- return $l10n->l('weekdayName', $dt, ['width' => 'abbreviated']);
+ return (string)$l10n->l('weekdayName', $dt, ['width' => 'abbreviated']);
}
- /**
- * @param IL10N $l10n
- * @param DateTime $dt
- * @return string
- */
private function getDateString(IL10N $l10n, DateTime $dt):string {
- return $l10n->l('date', $dt, ['width' => 'medium']);
+ return (string)$l10n->l('date', $dt, ['width' => 'medium']);
}
- /**
- * @param IL10N $l10n
- * @param DateTime $dt
- * @return string
- */
private function getDateTimeString(IL10N $l10n, DateTime $dt):string {
- return $l10n->l('datetime', $dt, ['width' => 'medium|short']);
+ return (string)$l10n->l('datetime', $dt, ['width' => 'medium|short']);
}
- /**
- * @param IL10N $l10n
- * @param DateTime $dt
- * @return string
- */
private function getTimeString(IL10N $l10n, DateTime $dt):string {
- return $l10n->l('time', $dt, ['width' => 'short']);
+ return (string)$l10n->l('time', $dt, ['width' => 'short']);
}
- /**
- * @param VEvent $vevent
- * @param IL10N $l10n
- * @return string
- */
private function getTitleFromVEvent(VEvent $vevent, IL10N $l10n):string {
if (isset($vevent->SUMMARY)) {
return (string)$vevent->SUMMARY;
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 fb123960df8..a3f0cce547a 100644
--- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php
+++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php
@@ -3,41 +3,20 @@
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\NotificationProvider;
use OCA\DAV\AppInfo\Application;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
-use OCP\ILogger;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\L10N\IFactory as L10NFactory;
use OCP\Notification\IManager;
use OCP\Notification\INotification;
+use Psr\Log\LoggerInterface;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\Property;
@@ -51,55 +30,44 @@ class PushProvider extends AbstractProvider {
/** @var string */
public const NOTIFICATION_TYPE = 'DISPLAY';
- /** @var IManager */
- private $manager;
-
- /** @var ITimeFactory */
- private $timeFactory;
-
- /**
- * @param IConfig $config
- * @param IManager $manager
- * @param ILogger $logger
- * @param L10NFactory $l10nFactory
- * @param IUrlGenerator $urlGenerator
- * @param ITimeFactory $timeFactory
- */
- public function __construct(IConfig $config,
- IManager $manager,
- ILogger $logger,
- L10NFactory $l10nFactory,
- IURLGenerator $urlGenerator,
- ITimeFactory $timeFactory) {
+ public function __construct(
+ IConfig $config,
+ private IManager $manager,
+ LoggerInterface $logger,
+ L10NFactory $l10nFactory,
+ IURLGenerator $urlGenerator,
+ private ITimeFactory $timeFactory,
+ ) {
parent::__construct($logger, $l10nFactory, $urlGenerator, $config);
- $this->manager = $manager;
- $this->timeFactory = $timeFactory;
}
/**
* Send push notification to all users.
*
* @param VEvent $vevent
- * @param string $calendarDisplayName
+ * @param string|null $calendarDisplayName
+ * @param string[] $principalEmailAddresses
* @param IUser[] $users
* @throws \Exception
*/
public function send(VEvent $vevent,
- string $calendarDisplayName = null,
- array $users = []):void {
- if ($this->config->getAppValue('dav', 'sendEventRemindersPush', 'no') !== 'yes') {
+ ?string $calendarDisplayName,
+ array $principalEmailAddresses,
+ array $users = []):void {
+ if ($this->config->getAppValue('dav', 'sendEventRemindersPush', 'yes') !== 'yes') {
return;
}
$eventDetails = $this->extractEventDetails($vevent);
- $eventDetails['calendar_displayname'] = $calendarDisplayName;
- $eventUUID = (string) $vevent->UID;
+ $eventUUID = (string)$vevent->UID;
if (!$eventUUID) {
return;
};
$eventUUIDHash = hash('sha256', $eventUUID, false);
foreach ($users as $user) {
+ $eventDetails['calendar_displayname'] = $calendarDisplayName ?? $this->getCalendarDisplayNameFallback($this->l10nFactory->getUserLanguage($user));
+
/** @var INotification $notification */
$notification = $this->manager->createNotification();
$notification->setApp(Application::APP_ID)
@@ -117,8 +85,6 @@ class PushProvider extends AbstractProvider {
}
/**
- * @var VEvent $vevent
- * @return array
* @throws \Exception
*/
protected function extractEventDetails(VEvent $vevent):array {
@@ -128,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 8535c55054a..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 {
+ 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');
}
}
@@ -170,21 +140,35 @@ class Notifier implements INotifier {
$components[] = $this->l10n->n('%n minute', '%n minutes', $diff->i);
}
- // Limiting to the first three components to prevent
- // the string from getting too long
- $firstThreeComponents = array_slice($components, 0, 2);
- $diffLabel = implode(', ', $firstThreeComponents);
+ if (count($components) > 0 && !$this->hasPhpDatetimeDiffBug()) {
+ // Limiting to the first three components to prevent
+ // the string from getting too long
+ $firstThreeComponents = array_slice($components, 0, 2);
+ $diffLabel = implode(', ', $firstThreeComponents);
- if ($diff->invert) {
- $title = $this->l10n->t('%s (in %s)', [$title, $diffLabel]);
- } else {
- $title = $this->l10n->t('%s (%s ago)', [$title, $diffLabel]);
+ if ($diff->invert) {
+ $title = $this->l10n->t('%s (in %s)', [$title, $diffLabel]);
+ } else {
+ $title = $this->l10n->t('%s (%s ago)', [$title, $diffLabel]);
+ }
}
$notification->setParsedSubject($title);
}
/**
+ * @see https://github.com/nextcloud/server/issues/41615
+ * @see https://github.com/php/php-src/issues/9699
+ */
+ private function hasPhpDatetimeDiffBug(): bool {
+ $d1 = DateTime::createFromFormat(\DateTimeInterface::ATOM, '2023-11-22T11:52:00+01:00');
+ $d2 = new DateTime('2023-11-22T10:52:03', new \DateTimeZone('UTC'));
+
+ // The difference is 3 seconds, not -1year+11months+…
+ return $d1->diff($d2)->y < 0;
+ }
+
+ /**
* Sets the notification message based on the parameters set in PushProvider
*
* @param INotification $notification
@@ -289,7 +273,7 @@ class Notifier implements INotifier {
* @return bool
*/
private function isDayEqual(DateTime $dtStart,
- DateTime $dtEnd):bool {
+ DateTime $dtEnd):bool {
return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
}
@@ -298,7 +282,7 @@ class Notifier implements INotifier {
* @return string
*/
private function getWeekDayName(DateTime $dt):string {
- return $this->l10n->l('weekdayName', $dt, ['width' => 'abbreviated']);
+ return (string)$this->l10n->l('weekdayName', $dt, ['width' => 'abbreviated']);
}
/**
@@ -306,7 +290,7 @@ class Notifier implements INotifier {
* @return string
*/
private function getDateString(DateTime $dt):string {
- return $this->l10n->l('date', $dt, ['width' => 'medium']);
+ return (string)$this->l10n->l('date', $dt, ['width' => 'medium']);
}
/**
@@ -314,7 +298,7 @@ class Notifier implements INotifier {
* @return string
*/
private function getDateTimeString(DateTime $dt):string {
- return $this->l10n->l('datetime', $dt, ['width' => 'medium|short']);
+ return (string)$this->l10n->l('datetime', $dt, ['width' => 'medium|short']);
}
/**
@@ -322,6 +306,6 @@ class Notifier implements INotifier {
* @return string
*/
private function getTimeString(DateTime $dt):string {
- return $this->l10n->l('time', $dt, ['width' => 'short']);
+ return (string)$this->l10n->l('time', $dt, ['width' => 'short']);
}
}
diff --git a/apps/dav/lib/CalDAV/Reminder/ReminderService.php b/apps/dav/lib/CalDAV/Reminder/ReminderService.php
index d6901cc4fb0..c75090e1560 100644
--- a/apps/dav/lib/CalDAV/Reminder/ReminderService.php
+++ b/apps/dav/lib/CalDAV/Reminder/ReminderService.php
@@ -3,73 +3,35 @@
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>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have 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 DateTimeImmutable;
+use DateTimeZone;
use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\Connector\Sabre\Principal;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserManager;
+use Psr\Log\LoggerInterface;
use Sabre\VObject;
use Sabre\VObject\Component\VAlarm;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\InvalidDataException;
use Sabre\VObject\ParseException;
use Sabre\VObject\Recur\EventIterator;
+use Sabre\VObject\Recur\MaxInstancesExceededException;
use Sabre\VObject\Recur\NoInstancesException;
+use function count;
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;
-
public const REMINDER_TYPE_EMAIL = 'EMAIL';
public const REMINDER_TYPE_DISPLAY = 'DISPLAY';
public const REMINDER_TYPE_AUDIO = 'AUDIO';
@@ -85,31 +47,17 @@ class ReminderService {
self::REMINDER_TYPE_AUDIO
];
- /**
- * ReminderService constructor.
- *
- * @param Backend $backend
- * @param NotificationProviderManager $notificationProviderManager
- * @param IUserManager $userManager
- * @param IGroupManager $groupManager
- * @param CalDavBackend $caldavBackend
- * @param ITimeFactory $timeFactory
- * @param IConfig $config
- */
- public function __construct(Backend $backend,
- NotificationProviderManager $notificationProviderManager,
- IUserManager $userManager,
- IGroupManager $groupManager,
- CalDavBackend $caldavBackend,
- ITimeFactory $timeFactory,
- IConfig $config) {
- $this->backend = $backend;
- $this->notificationProviderManager = $notificationProviderManager;
- $this->userManager = $userManager;
- $this->groupManager = $groupManager;
- $this->caldavBackend = $caldavBackend;
- $this->timeFactory = $timeFactory;
- $this->config = $config;
+ 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,
+ ) {
}
/**
@@ -118,8 +66,11 @@ class ReminderService {
* @throws NotificationProvider\ProviderNotAvailableException
* @throws NotificationTypeDoesNotExistException
*/
- public function processReminders():void {
+ public function processReminders() :void {
$reminders = $this->backend->getRemindersToProcess();
+ $this->logger->debug('{numReminders} reminders to process', [
+ 'numReminders' => count($reminders),
+ ]);
foreach ($reminders as $reminder) {
$calendarData = is_resource($reminder['calendardata'])
@@ -132,27 +83,46 @@ class ReminderService {
$vcalendar = $this->parseCalendarData($calendarData);
if (!$vcalendar) {
+ $this->logger->debug('Reminder {id} does not belong to a valid calendar', [
+ 'id' => $reminder['id'],
+ ]);
+ $this->backend->removeReminder($reminder['id']);
+ continue;
+ }
+
+ try {
+ $vevent = $this->getVEventByRecurrenceId($vcalendar, $reminder['recurrence_id'], $reminder['is_recurrence_exception']);
+ } catch (MaxInstancesExceededException $e) {
+ $this->logger->debug('Recurrence with too many instances detected, skipping VEVENT', ['exception' => $e]);
$this->backend->removeReminder($reminder['id']);
continue;
}
- $vevent = $this->getVEventByRecurrenceId($vcalendar, $reminder['recurrence_id'], $reminder['is_recurrence_exception']);
if (!$vevent) {
+ $this->logger->debug('Reminder {id} does not belong to a valid event', [
+ 'id' => $reminder['id'],
+ ]);
$this->backend->removeReminder($reminder['id']);
continue;
}
if ($this->wasEventCancelled($vevent)) {
+ $this->logger->debug('Reminder {id} belongs to a cancelled event', [
+ 'id' => $reminder['id'],
+ ]);
$this->deleteOrProcessNext($reminder, $vevent);
continue;
}
if (!$this->notificationProviderManager->hasProvider($reminder['type'])) {
+ $this->logger->debug('Reminder {id} does not belong to a valid notification provider', [
+ 'id' => $reminder['id'],
+ ]);
$this->deleteOrProcessNext($reminder, $vevent);
continue;
}
- if ($this->config->getAppValue('dav', 'sendEventRemindersToSharedGroupMembers', 'yes') === 'no') {
+ if ($this->config->getAppValue('dav', 'sendEventRemindersToSharedUsers', 'yes') === 'no') {
$users = $this->getAllUsersWithWriteAccessToCalendar($reminder['calendar_id']);
} else {
$users = [];
@@ -163,8 +133,18 @@ class ReminderService {
$users[] = $user;
}
+ $userPrincipalEmailAddresses = [];
+ $userPrincipal = $this->principalConnector->getPrincipalByPath($reminder['principaluri']);
+ if ($userPrincipal) {
+ $userPrincipalEmailAddresses = $this->principalConnector->getEmailAddressesOfPrincipal($userPrincipal);
+ }
+
+ $this->logger->debug('Reminder {id} will be sent to {numUsers} users', [
+ 'id' => $reminder['id'],
+ 'numUsers' => count($users),
+ ]);
$notificationProvider = $this->notificationProviderManager->getProvider($reminder['type']);
- $notificationProvider->send($vevent, $reminder['displayname'], $users);
+ $notificationProvider->send($vevent, $reminder['displayname'], $userPrincipalEmailAddresses, $users);
$this->deleteOrProcessNext($reminder, $vevent);
}
@@ -188,18 +168,18 @@ class ReminderService {
return;
}
- /** @var VObject\Component\VCalendar $vcalendar */
$vcalendar = $this->parseCalendarData($calendarData);
if (!$vcalendar) {
return;
}
+ $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();
@@ -221,7 +201,7 @@ class ReminderService {
continue;
}
- $alarms = $this->getRemindersForVAlarm($valarm, $objectData,
+ $alarms = $this->getRemindersForVAlarm($valarm, $objectData, $calendarTimeZone,
$eventHash, $alarmHash, true, true);
$this->writeRemindersToDatabase($alarms);
}
@@ -247,6 +227,10 @@ class ReminderService {
// instance. We are skipping this event from the output
// entirely.
return;
+ } catch (MaxInstancesExceededException $e) {
+ // The event has more than 3500 recurring-instances
+ // so we can ignore it
+ return;
}
while ($iterator->valid() && count($processedAlarms) < count($masterAlarms)) {
@@ -265,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;
@@ -274,6 +258,16 @@ class ReminderService {
try {
$triggerTime = $valarm->getEffectiveTriggerTime();
+ /**
+ * @psalm-suppress DocblockTypeContradiction
+ * https://github.com/vimeo/psalm/issues/9244
+ */
+ if ($triggerTime->getTimezone() === false || $triggerTime->getTimezone()->getName() === 'UTC') {
+ $triggerTime = new DateTimeImmutable(
+ $triggerTime->format('Y-m-d H:i:s'),
+ $calendarTimeZone
+ );
+ }
} catch (InvalidDataException $e) {
continue;
}
@@ -292,7 +286,7 @@ class ReminderService {
continue;
}
- $alarms = $this->getRemindersForVAlarm($valarm, $objectData, $masterHash, $alarmHash, $isRecurring, false);
+ $alarms = $this->getRemindersForVAlarm($valarm, $objectData, $calendarTimeZone, $masterHash, $alarmHash, $isRecurring, false);
$this->writeRemindersToDatabase($alarms);
$processedAlarms[] = $alarmHash;
}
@@ -325,12 +319,13 @@ class ReminderService {
return;
}
- $this->backend->cleanRemindersForEvent((int) $objectData['id']);
+ $this->backend->cleanRemindersForEvent((int)$objectData['id']);
}
/**
* @param VAlarm $valarm
* @param array $objectData
+ * @param DateTimeZone $calendarTimeZone
* @param string|null $eventHash
* @param string|null $alarmHash
* @param bool $isRecurring
@@ -338,11 +333,12 @@ class ReminderService {
* @return array
*/
private function getRemindersForVAlarm(VAlarm $valarm,
- array $objectData,
- string $eventHash = null,
- string $alarmHash = null,
- bool $isRecurring = false,
- bool $isRecurrenceException = false):array {
+ array $objectData,
+ DateTimeZone $calendarTimeZone,
+ ?string $eventHash = null,
+ ?string $alarmHash = null,
+ bool $isRecurring = false,
+ bool $isRecurrenceException = false):array {
if ($eventHash === null) {
$eventHash = $this->getEventHash($valarm->parent);
}
@@ -354,6 +350,16 @@ class ReminderService {
$isRelative = $this->isAlarmRelative($valarm);
/** @var DateTimeImmutable $notificationDate */
$notificationDate = $valarm->getEffectiveTriggerTime();
+ /**
+ * @psalm-suppress DocblockTypeContradiction
+ * https://github.com/vimeo/psalm/issues/9244
+ */
+ if ($notificationDate->getTimezone() === false || $notificationDate->getTimezone()->getName() === 'UTC') {
+ $notificationDate = new DateTimeImmutable(
+ $notificationDate->format('Y-m-d H:i:s'),
+ $calendarTimeZone
+ );
+ }
$clonedNotificationDate = new \DateTime('now', $notificationDate->getTimezone());
$clonedNotificationDate->setTimestamp($notificationDate->getTimestamp());
@@ -362,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;
@@ -384,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,
@@ -404,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']
);
}
@@ -427,11 +440,11 @@ class ReminderService {
* @param VEvent $vevent
*/
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']) {
+ VObject\Component\VEvent $vevent):void {
+ if ($reminder['is_repeat_based']
+ || !$reminder['is_recurring']
+ || !$reminder['is_relative']
+ || $reminder['is_recurrence_exception']) {
$this->backend->removeReminder($reminder['id']);
return;
}
@@ -439,6 +452,7 @@ class ReminderService {
$vevents = $this->getAllVEventsFromVCalendar($vevent->parent);
$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
$now = $this->timeFactory->getDateTime();
+ $calendarTimeZone = $this->getCalendarTimeZone((int)$reminder['calendar_id']);
try {
$iterator = new EventIterator($vevents, $reminder['uid']);
@@ -449,49 +463,54 @@ class ReminderService {
return;
}
- while ($iterator->valid()) {
- $event = $iterator->getEventObject();
-
- // Recurrence-exceptions are handled separately, so just ignore them here
- if (\in_array($event, $recurrenceExceptions, true)) {
- $iterator->next();
- continue;
- }
-
- $recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($event);
- if ($reminder['recurrence_id'] >= $recurrenceId) {
- $iterator->next();
- continue;
- }
+ try {
+ while ($iterator->valid()) {
+ $event = $iterator->getEventObject();
- foreach ($event->VALARM as $valarm) {
- /** @var VAlarm $valarm */
- $alarmHash = $this->getAlarmHash($valarm);
- if ($alarmHash !== $reminder['alarm_hash']) {
+ // Recurrence-exceptions are handled separately, so just ignore them here
+ if (\in_array($event, $recurrenceExceptions, true)) {
+ $iterator->next();
continue;
}
- $triggerTime = $valarm->getEffectiveTriggerTime();
-
- // If effective trigger time is in the past
- // just skip and generate for next event
- $diff = $now->diff($triggerTime);
- if ($diff->invert === 1) {
+ $recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($event);
+ if ($reminder['recurrence_id'] >= $recurrenceId) {
+ $iterator->next();
continue;
}
- $this->backend->removeReminder($reminder['id']);
- $alarms = $this->getRemindersForVAlarm($valarm, [
- 'calendarid' => $reminder['calendar_id'],
- 'id' => $reminder['object_id'],
- ], $reminder['event_hash'], $alarmHash, true, false);
- $this->writeRemindersToDatabase($alarms);
+ foreach ($event->VALARM as $valarm) {
+ /** @var VAlarm $valarm */
+ $alarmHash = $this->getAlarmHash($valarm);
+ if ($alarmHash !== $reminder['alarm_hash']) {
+ continue;
+ }
- // Abort generating reminders after creating one successfully
- return;
- }
+ $triggerTime = $valarm->getEffectiveTriggerTime();
+
+ // If effective trigger time is in the past
+ // just skip and generate for next event
+ $diff = $now->diff($triggerTime);
+ if ($diff->invert === 1) {
+ continue;
+ }
+
+ $this->backend->removeReminder($reminder['id']);
+ $alarms = $this->getRemindersForVAlarm($valarm, [
+ 'calendarid' => $reminder['calendar_id'],
+ 'id' => $reminder['object_id'],
+ ], $calendarTimeZone, $reminder['event_hash'], $alarmHash, true, false);
+ $this->writeRemindersToDatabase($alarms);
+
+ // Abort generating reminders after creating one successfully
+ return;
+ }
- $iterator->next();
+ $iterator->next();
+ }
+ } catch (MaxInstancesExceededException $e) {
+ // Using debug logger as this isn't really an error
+ $this->logger->debug('Recurrence with too many instances detected, skipping VEVENT', ['exception' => $e]);
}
$this->backend->removeReminder($reminder['id']);
@@ -549,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));
@@ -583,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));
@@ -604,14 +623,14 @@ class ReminderService {
* @return VEvent|null
*/
private function getVEventByRecurrenceId(VObject\Component\VCalendar $vcalendar,
- int $recurrenceId,
- bool $isRecurrenceException):?VEvent {
+ int $recurrenceId,
+ bool $isRecurrenceException):?VEvent {
$vevents = $this->getAllVEventsFromVCalendar($vcalendar);
if (count($vevents) === 0) {
return null;
}
- $uid = (string) $vevents[0]->UID;
+ $uid = (string)$vevents[0]->UID;
$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
$masterItem = $this->getMasterItemFromListOfVEvents($vevents);
@@ -662,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,
@@ -724,6 +743,10 @@ class ReminderService {
if ($child->name !== 'VEVENT') {
continue;
}
+ // Ignore invalid events with no DTSTART
+ if ($child->DTSTART === null) {
+ continue;
+ }
$vevents[] = $child;
}
@@ -788,4 +811,26 @@ class ReminderService {
private function isRecurring(VEvent $vevent):bool {
return isset($vevent->RRULE) || isset($vevent->RDATE);
}
+
+ /**
+ * @param int $calendarid
+ *
+ * @return DateTimeZone
+ */
+ private function getCalendarTimeZone(int $calendarid): DateTimeZone {
+ $calendarInfo = $this->caldavBackend->getCalendarById($calendarid);
+ $tzProp = '{urn:ietf:params:xml:ns:caldav}calendar-timezone';
+ if (empty($calendarInfo[$tzProp])) {
+ // Defaulting to UTC
+ return new DateTimeZone('UTC');
+ }
+ // This property contains a VCALENDAR with a single VTIMEZONE
+ /** @var string $timezoneProp */
+ $timezoneProp = $calendarInfo[$tzProp];
+ /** @var VObject\Component\VCalendar $vtimezoneObj */
+ $vtimezoneObj = VObject\Reader::read($timezoneProp);
+ /** @var VObject\Component\VTimeZone $vtimezone */
+ $vtimezone = $vtimezoneObj->VTIMEZONE;
+ return $vtimezone->getTimeZone();
+ }
}
diff --git a/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php b/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php
index aebb5a24f0e..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;
@@ -33,8 +14,8 @@ use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IGroupManager;
-use OCP\ILogger;
use OCP\IUserSession;
+use Psr\Log\LoggerInterface;
use Sabre\DAV\PropPatch;
use Sabre\DAVACL\PrincipalBackend\BackendInterface;
use function array_intersect;
@@ -45,24 +26,6 @@ use function array_values;
abstract class AbstractPrincipalBackend implements BackendInterface {
- /** @var IDBConnection */
- private $db;
-
- /** @var IUserSession */
- private $userSession;
-
- /** @var IGroupManager */
- private $groupManager;
-
- /** @var ILogger */
- private $logger;
-
- /** @var ProxyMapper */
- private $proxyMapper;
-
- /** @var string */
- private $principalPrefix;
-
/** @var string */
private $dbTableName;
@@ -72,36 +35,19 @@ abstract class AbstractPrincipalBackend implements BackendInterface {
/** @var string */
private $dbForeignKeyName;
- /** @var string */
- private $cuType;
-
- /**
- * @param IDBConnection $dbConnection
- * @param IUserSession $userSession
- * @param IGroupManager $groupManager
- * @param ILogger $logger
- * @param string $principalPrefix
- * @param string $dbPrefix
- * @param string $cuType
- */
- public function __construct(IDBConnection $dbConnection,
- IUserSession $userSession,
- IGroupManager $groupManager,
- ILogger $logger,
- ProxyMapper $proxyMapper,
- string $principalPrefix,
- string $dbPrefix,
- string $cuType) {
- $this->db = $dbConnection;
- $this->userSession = $userSession;
- $this->groupManager = $groupManager;
- $this->logger = $logger;
- $this->proxyMapper = $proxyMapper;
- $this->principalPrefix = $principalPrefix;
+ public function __construct(
+ private IDBConnection $db,
+ private IUserSession $userSession,
+ private IGroupManager $groupManager,
+ private LoggerInterface $logger,
+ private ProxyMapper $proxyMapper,
+ private string $principalPrefix,
+ string $dbPrefix,
+ private string $cuType,
+ ) {
$this->dbTableName = 'calendar_' . $dbPrefix . 's';
$this->dbMetaDataTableName = $this->dbTableName . '_md';
$this->dbForeignKeyName = $dbPrefix . '_id';
- $this->cuType = $cuType;
}
use PrincipalProxyTrait;
@@ -140,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)) {
@@ -170,12 +116,12 @@ abstract class AbstractPrincipalBackend implements BackendInterface {
* @return array
*/
public function getPrincipalByPath($path) {
- if (strpos($path, $this->principalPrefix) !== 0) {
+ if (!str_starts_with($path, $this->principalPrefix)) {
return null;
}
[, $name] = \Sabre\Uri\split($path);
- [$backendId, $resourceId] = explode('-', $name, 2);
+ [$backendId, $resourceId] = explode('-', $name, 2);
$query = $this->db->getQueryBuilder();
$query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname'])
@@ -319,7 +265,7 @@ abstract class AbstractPrincipalBackend implements BackendInterface {
case IRoomMetadata::CAPACITY:
case IResourceMetadata::VEHICLE_SEATING_CAPACITY:
- $results[] = $this->searchPrincipalsByCapacity($prop,$value);
+ $results[] = $this->searchPrincipalsByCapacity($prop, $value);
break;
default:
@@ -416,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 = [];
@@ -453,7 +399,7 @@ abstract class AbstractPrincipalBackend implements BackendInterface {
}
$usersGroups = $this->groupManager->getUserGroupIds($user);
- if (strpos($uri, 'mailto:') === 0) {
+ if (str_starts_with($uri, 'mailto:')) {
$email = substr($uri, 7);
$query = $this->db->getQueryBuilder();
$query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions'])
@@ -473,14 +419,14 @@ abstract class AbstractPrincipalBackend implements BackendInterface {
return $this->rowToPrincipal($row)['uri'];
}
- if (strpos($uri, 'principal:') === 0) {
+ if (str_starts_with($uri, 'principal:')) {
$path = substr($uri, 10);
- if (strpos($path, $this->principalPrefix) !== 0) {
+ if (!str_starts_with($path, $this->principalPrefix)) {
return null;
}
[, $name] = \Sabre\Uri\split($path);
- [$backendId, $resourceId] = explode('-', $name, 2);
+ [$backendId, $resourceId] = explode('-', $name, 2);
$query = $this->db->getQueryBuilder();
$query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions'])
@@ -525,14 +471,14 @@ 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;
}
// group restrictions contains something, but not parsable, deny access and log warning
- $json = json_decode($row['group_restrictions']);
+ $json = json_decode($row['group_restrictions'], null, 512, JSON_THROW_ON_ERROR);
if (!\is_array($json)) {
$this->logger->info('group_restrictions field could not be parsed for ' . $this->dbTableName . '::' . $row['id'] . ', denying access to resource');
return false;
diff --git a/apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php b/apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php
index 65203e24da5..c70d93daf52 100644
--- a/apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php
+++ b/apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php
@@ -1,32 +1,16 @@
<?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;
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
use OCP\IDBConnection;
use OCP\IGroupManager;
-use OCP\ILogger;
use OCP\IUserSession;
+use Psr\Log\LoggerInterface;
/**
* Class ResourcePrincipalBackend
@@ -37,18 +21,12 @@ class ResourcePrincipalBackend extends AbstractPrincipalBackend {
/**
* ResourcePrincipalBackend constructor.
- *
- * @param IDBConnection $dbConnection
- * @param IUserSession $userSession
- * @param IGroupManager $groupManager
- * @param ILogger $logger
- * @param ProxyMapper $proxyMapper
*/
public function __construct(IDBConnection $dbConnection,
- IUserSession $userSession,
- IGroupManager $groupManager,
- ILogger $logger,
- ProxyMapper $proxyMapper) {
+ IUserSession $userSession,
+ IGroupManager $groupManager,
+ LoggerInterface $logger,
+ ProxyMapper $proxyMapper) {
parent::__construct($dbConnection, $userSession, $groupManager, $logger,
$proxyMapper, 'principals/calendar-resources', 'resource', 'RESOURCE');
}
diff --git a/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php b/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php
index ca78ebd4bc4..5704b23ae14 100644
--- a/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php
+++ b/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php
@@ -1,32 +1,16 @@
<?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;
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
use OCP\IDBConnection;
use OCP\IGroupManager;
-use OCP\ILogger;
use OCP\IUserSession;
+use Psr\Log\LoggerInterface;
/**
* Class RoomPrincipalBackend
@@ -37,18 +21,12 @@ class RoomPrincipalBackend extends AbstractPrincipalBackend {
/**
* RoomPrincipalBackend constructor.
- *
- * @param IDBConnection $dbConnection
- * @param IUserSession $userSession
- * @param IGroupManager $groupManager
- * @param ILogger $logger
- * @param ProxyMapper $proxyMapper
*/
public function __construct(IDBConnection $dbConnection,
- IUserSession $userSession,
- IGroupManager $groupManager,
- ILogger $logger,
- ProxyMapper $proxyMapper) {
+ IUserSession $userSession,
+ IGroupManager $groupManager,
+ LoggerInterface $logger,
+ ProxyMapper $proxyMapper) {
parent::__construct($dbConnection, $userSession, $groupManager, $logger,
$proxyMapper, 'principals/calendar-rooms', 'room', 'ROOM');
}
diff --git a/apps/dav/lib/CalDAV/RetentionService.php b/apps/dav/lib/CalDAV/RetentionService.php
index 1d92d847641..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 8aacc33bb46..2af6b162d8d 100644
--- a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
+++ b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
@@ -1,60 +1,34 @@
<?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/).
- *
- * @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>
- *
- * @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;
+use OCA\DAV\CalDAV\CalendarObject;
+use OCA\DAV\CalDAV\EventComparisonService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Defaults;
-use OCP\IConfig;
-use OCP\IDBConnection;
-use OCP\IL10N;
-use OCP\ILogger;
-use OCP\IURLGenerator;
-use OCP\IUserManager;
-use OCP\L10N\IFactory as L10NFactory;
-use OCP\Mail\IEMailTemplate;
+use OCP\IAppConfig;
+use OCP\IUserSession;
use OCP\Mail\IMailer;
-use OCP\Security\ISecureRandom;
+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;
+use Sabre\DAV;
+use Sabre\DAV\INode;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;
-use Sabre\VObject\DateTimeParser;
use Sabre\VObject\ITip\Message;
use Sabre\VObject\Parameter;
-use Sabre\VObject\Property;
-use Sabre\VObject\Recur\EventIterator;
+use Sabre\VObject\Reader;
/**
* iMIP handler.
@@ -72,75 +46,47 @@ use Sabre\VObject\Recur\EventIterator;
*/
class IMipPlugin extends SabreIMipPlugin {
- /** @var string */
- private $userId;
-
- /** @var IConfig */
- private $config;
-
- /** @var IMailer */
- private $mailer;
-
- /** @var ILogger */
- private $logger;
-
- /** @var ITimeFactory */
- private $timeFactory;
-
- /** @var L10NFactory */
- private $l10nFactory;
-
- /** @var IURLGenerator */
- private $urlGenerator;
-
- /** @var ISecureRandom */
- private $random;
-
- /** @var IDBConnection */
- private $db;
-
- /** @var Defaults */
- private $defaults;
-
- /** @var IUserManager */
- private $userManager;
-
+ private ?VCalendar $vCalendar = null;
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
+ 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('');
+ }
+
+ public function initialize(DAV\Server $server): void {
+ parent::initialize($server);
+ $server->on('beforeWriteContent', [$this, 'beforeWriteContent'], 10);
+ }
/**
- * @param IConfig $config
- * @param IMailer $mailer
- * @param ILogger $logger
- * @param ITimeFactory $timeFactory
- * @param L10NFactory $l10nFactory
- * @param IUrlGenerator $urlGenerator
- * @param Defaults $defaults
- * @param ISecureRandom $random
- * @param IDBConnection $db
- * @param string $userId
+ * Check quota before writing content
+ *
+ * @param string $uri target file URI
+ * @param INode $node Sabre Node
+ * @param resource $data data
+ * @param bool $modified modified
*/
- public function __construct(IConfig $config, IMailer $mailer, ILogger $logger,
- ITimeFactory $timeFactory, L10NFactory $l10nFactory,
- IURLGenerator $urlGenerator, Defaults $defaults,
- ISecureRandom $random, IDBConnection $db, IUserManager $userManager,
- $userId) {
- parent::__construct('');
- $this->userId = $userId;
- $this->config = $config;
- $this->mailer = $mailer;
- $this->logger = $logger;
- $this->timeFactory = $timeFactory;
- $this->l10nFactory = $l10nFactory;
- $this->urlGenerator = $urlGenerator;
- $this->random = $random;
- $this->db = $db;
- $this->defaults = $defaults;
- $this->userManager = $userManager;
+ public function beforeWriteContent($uri, INode $node, $data, $modified): void {
+ if (!$node instanceof CalendarObject) {
+ return;
+ }
+ /** @var VCalendar $vCalendar */
+ $vCalendar = Reader::read($node->get());
+ $this->setVCalendar($vCalendar);
}
/**
@@ -151,8 +97,7 @@ class IMipPlugin extends SabreIMipPlugin {
*/
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';
@@ -160,103 +105,114 @@ class IMipPlugin extends SabreIMipPlugin {
return;
}
- $summary = $iTipMessage->message->VEVENT->SUMMARY;
-
- if (parse_url($iTipMessage->sender, PHP_URL_SCHEME) !== 'mailto') {
- return;
- }
-
- if (parse_url($iTipMessage->recipient, PHP_URL_SCHEME) !== 'mailto') {
+ if (parse_url($iTipMessage->sender, PHP_URL_SCHEME) !== 'mailto'
+ || parse_url($iTipMessage->recipient, PHP_URL_SCHEME) !== 'mailto') {
return;
}
// don't send out mails for events that already took place
- $lastOccurrence = $this->getLastOccurrence($iTipMessage->message);
+ $lastOccurrence = $this->imipService->getLastOccurrence($iTipMessage->message);
$currentTime = $this->timeFactory->getTime();
if ($lastOccurrence < $currentTime) {
return;
}
// Strip off mailto:
- $sender = substr($iTipMessage->sender, 7);
$recipient = substr($iTipMessage->recipient, 7);
- if ($recipient === false || !$this->mailer->validateMailAddress($recipient)) {
+ if (!$this->mailer->validateMailAddress($recipient)) {
// Nothing to send if the recipient doesn't have a valid email address
$iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
return;
}
-
- $senderName = $iTipMessage->senderName ?: null;
- $recipientName = $iTipMessage->recipientName ?: null;
-
- if ($senderName === null || empty(trim($senderName))) {
- $user = $this->userManager->get($this->userId);
- if ($user) {
- // getDisplayName automatically uses the uid
- // if no display-name is set
- $senderName = $user->getDisplayName();
- }
+ $recipientName = $iTipMessage->recipientName ? (string)$iTipMessage->recipientName : null;
+
+ $newEvents = $iTipMessage->message;
+ $oldEvents = $this->getVCalendar();
+
+ $modified = $this->eventComparisonService->findModified($newEvents, $oldEvents);
+ /** @var VEvent $vEvent */
+ $vEvent = array_pop($modified['new']);
+ /** @var VEvent $oldVevent */
+ $oldVevent = !empty($modified['old']) && is_array($modified['old']) ? array_pop($modified['old']) : null;
+ $isModified = isset($oldVevent);
+
+ // 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)) {
+ $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;
}
- /** @var VEvent $vevent */
- $vevent = $iTipMessage->message->VEVENT;
-
- $attendee = $this->getCurrentAttendee($iTipMessage);
- $defaultLang = $this->l10nFactory->findGenericLanguage();
- $lang = $this->getAttendeeLangOrDefault($defaultLang, $attendee);
- $l10n = $this->l10nFactory->get('dav', $lang);
-
- $meetingAttendeeName = $recipientName ?: $recipient;
- $meetingInviteeName = $senderName ?: $sender;
-
- $meetingTitle = $vevent->SUMMARY;
- $meetingDescription = $vevent->DESCRIPTION;
-
-
- $meetingUrl = $vevent->URL;
- $meetingLocation = $vevent->LOCATION;
+ // we (should) have one event component left
+ // as the ITip\Broker creates one iTip message per change
+ // and triggers the "schedule" event once per message
+ // 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) {
+ $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 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';
+ return;
+ }
+ $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
+ // 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 = '';
+ }
- $defaultVal = '--';
+ $sender = substr($iTipMessage->sender, 7);
- $method = self::METHOD_REQUEST;
+ $replyingAttendee = null;
switch (strtolower($iTipMessage->method)) {
case self::METHOD_REPLY:
$method = self::METHOD_REPLY;
+ $data = $this->imipService->buildReplyBodyData($vEvent);
+ $replyingAttendee = $this->imipService->getReplyingAttendee($iTipMessage);
break;
case self::METHOD_CANCEL:
$method = self::METHOD_CANCEL;
+ $data = $this->imipService->buildCancelledBodyData($vEvent);
+ break;
+ default:
+ $method = self::METHOD_REQUEST;
+ $data = $this->imipService->buildBodyData($vEvent, $oldVevent);
break;
}
- $data = [
- 'attendee_name' => (string)$meetingAttendeeName ?: $defaultVal,
- 'invitee_name' => (string)$meetingInviteeName ?: $defaultVal,
- 'meeting_title' => (string)$meetingTitle ?: $defaultVal,
- 'meeting_description' => (string)$meetingDescription ?: $defaultVal,
- 'meeting_url' => (string)$meetingUrl ?: $defaultVal,
- ];
+ $data['attendee_name'] = ($recipientName ?: $recipient);
+ $data['invitee_name'] = ($senderName ?: $sender);
$fromEMail = Util::getDefaultEmailAddress('invitations-noreply');
- $fromName = $l10n->t('%1$s via %2$s', [$senderName, $this->defaults->getName()]);
-
- $message = $this->mailer->createMessage()
- ->setFrom([$fromEMail => $fromName])
- ->setTo([$recipient => $recipientName]);
-
- if ($sender !== false) {
- $message->setReplyTo([$sender => $senderName]);
- }
+ $fromName = $this->imipService->getFrom($senderName, $this->defaults->getName());
$template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data);
$template->addHeader();
- $summary = ((string) $summary !== '') ? (string) $summary : $l10n->t('Untitled event');
-
- $this->addSubjectAndHeading($template, $l10n, $method, $summary);
- $this->addBulletList($template, $l10n, $vevent);
+ $this->imipService->addSubjectAndHeading($template, $method, $data['invitee_name'], $data['meeting_title'], $isModified, $replyingAttendee);
+ $this->imipService->addBulletList($template, $vEvent, $data);
// Only add response buttons to invitation requests: Fix Issue #11230
- if (($method == self::METHOD_REQUEST) && $this->getAttendeeRsvpOrReqForParticipant($attendee)) {
+ if (strcasecmp($method, self::METHOD_REQUEST) === 0 && $this->imipService->getAttendeeRsvpOrReqForParticipant($attendee)) {
/*
** Only offer invitation accept/reject buttons, which link back to the
@@ -277,453 +233,106 @@ class IMipPlugin extends SabreIMipPlugin {
** To suppress URLs entirely, set invitation_link_recipients to boolean "no".
*/
- $recipientDomain = substr(strrchr($recipient, "@"), 1);
- $invitationLinkRecipients = explode(',', preg_replace('/\s+/', '', strtolower($this->config->getAppValue('dav', 'invitation_link_recipients', 'yes'))));
+ $recipientDomain = substr(strrchr($recipient, '@'), 1);
+ $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)
- || in_array(strtolower($recipientDomain), $invitationLinkRecipients)) {
- $this->addResponseButtons($template, $l10n, $iTipMessage, $lastOccurrence);
+ || in_array(strtolower($recipient), $invitationLinkRecipients)
+ || in_array(strtolower($recipientDomain), $invitationLinkRecipients)) {
+ $token = $this->imipService->createInvitationToken($iTipMessage, $vEvent, $lastOccurrence);
+ $this->imipService->addResponseButtons($template, $token);
+ $this->imipService->addMoreOptionsButton($template, $token);
}
}
$template->addFooter();
+ // convert iTip Message to string
+ $itip_msg = $iTipMessage->message->serialize();
- $message->useTemplate($template);
-
- $attachment = $this->mailer->createAttachment(
- $iTipMessage->message->serialize(),
- 'event.ics',// TODO(leon): Make file name unique, e.g. add event id
- 'text/calendar; method=' . $iTipMessage->method
- );
- $message->attach($attachment);
+ $mailService = null;
try {
- $failed = $this->mailer->send($message);
- $iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip';
- if ($failed) {
- $this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]);
- $iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
- }
- } catch (\Exception $ex) {
- $this->logger->logException($ex, ['app' => 'dav']);
- $iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
- }
- }
-
- /**
- * check if event took place in the past already
- * @param VCalendar $vObject
- * @return int
- */
- private function getLastOccurrence(VCalendar $vObject) {
- /** @var VEvent $component */
- $component = $vObject->VEVENT;
-
- $firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
- // Finding the last occurrence is a bit harder
- if (!isset($component->RRULE)) {
- if (isset($component->DTEND)) {
- $lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
- } elseif (isset($component->DURATION)) {
- /** @var \DateTime $endDate */
- $endDate = clone $component->DTSTART->getDateTime();
- // $component->DTEND->getDateTime() returns DateTimeImmutable
- $endDate = $endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
- $lastOccurrence = $endDate->getTimestamp();
- } elseif (!$component->DTSTART->hasTime()) {
- /** @var \DateTime $endDate */
- $endDate = clone $component->DTSTART->getDateTime();
- // $component->DTSTART->getDateTime() returns DateTimeImmutable
- $endDate = $endDate->modify('+1 day');
- $lastOccurrence = $endDate->getTimestamp();
- } else {
- $lastOccurrence = $firstOccurrence;
- }
- } else {
- $it = new EventIterator($vObject, (string)$component->UID);
- $maxDate = new \DateTime(self::MAX_DATE);
- if ($it->isInfinite()) {
- $lastOccurrence = $maxDate->getTimestamp();
- } else {
- $end = $it->getDtEnd();
- while ($it->valid() && $end < $maxDate) {
- $end = $it->getDtEnd();
- $it->next();
+ 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);
}
- $lastOccurrence = $end->getTimestamp();
}
- }
-
- return $lastOccurrence;
- }
- /**
- * @param Message $iTipMessage
- * @return null|Property
- */
- private function getCurrentAttendee(Message $iTipMessage) {
- /** @var VEvent $vevent */
- $vevent = $iTipMessage->message->VEVENT;
- $attendees = $vevent->select('ATTENDEE');
- foreach ($attendees as $attendee) {
- /** @var Property $attendee */
- if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) {
- return $attendee;
- }
- }
- return null;
- }
-
- /**
- * @param string $default
- * @param Property|null $attendee
- * @return string
- */
- private function getAttendeeLangOrDefault($default, Property $attendee = null) {
- if ($attendee !== null) {
- $lang = $attendee->offsetGet('LANGUAGE');
- if ($lang instanceof Parameter) {
- return $lang->getValue();
- }
- }
- return $default;
- }
-
- /**
- * @param Property|null $attendee
- * @return bool
- */
- private function getAttendeeRsvpOrReqForParticipant(Property $attendee = null) {
- if ($attendee !== null) {
- $rsvp = $attendee->offsetGet('RSVP');
- if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) {
- return true;
- }
- $role = $attendee->offsetGet('ROLE');
- // @see https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.16
- // Attendees without a role are assumed required and should receive an invitation link even if they have no RSVP set
- if ($role === null
- || (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'REQ-PARTICIPANT') === 0))
- || (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'OPT-PARTICIPANT') === 0))
- ) {
- return true;
- }
- }
- // RFC 5545 3.2.17: default RSVP is false
- return false;
- }
-
- /**
- * @param IL10N $l10n
- * @param VEvent $vevent
- */
- private function generateWhenString(IL10N $l10n, VEvent $vevent) {
- $dtstart = $vevent->DTSTART;
- if (isset($vevent->DTEND)) {
- $dtend = $vevent->DTEND;
- } elseif (isset($vevent->DURATION)) {
- $isFloating = $vevent->DTSTART->isFloating();
- $dtend = clone $vevent->DTSTART;
- $endDateTime = $dtend->getDateTime();
- $endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
- $dtend->setDateTime($endDateTime, $isFloating);
- } elseif (!$vevent->DTSTART->hasTime()) {
- $isFloating = $vevent->DTSTART->isFloating();
- $dtend = clone $vevent->DTSTART;
- $endDateTime = $dtend->getDateTime();
- $endDateTime = $endDateTime->modify('+1 day');
- $dtend->setDateTime($endDateTime, $isFloating);
- } else {
- $dtend = clone $vevent->DTSTART;
- }
-
- $isAllDay = $dtstart instanceof Property\ICalendar\Date;
-
- /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
- /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
- /** @var \DateTimeImmutable $dtstartDt */
- $dtstartDt = $dtstart->getDateTime();
- /** @var \DateTimeImmutable $dtendDt */
- $dtendDt = $dtend->getDateTime();
-
- $diff = $dtstartDt->diff($dtendDt);
-
- $dtstartDt = new \DateTime($dtstartDt->format(\DateTimeInterface::ATOM));
- $dtendDt = new \DateTime($dtendDt->format(\DateTimeInterface::ATOM));
-
- if ($isAllDay) {
- // One day event
- if ($diff->days === 1) {
- return $l10n->l('date', $dtstartDt, ['width' => 'medium']);
- }
-
- // 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');
-
- //event that spans over multiple days
- $localeStart = $l10n->l('date', $dtstartDt, ['width' => 'medium']);
- $localeEnd = $l10n->l('date', $dtendDt, ['width' => 'medium']);
-
- return $localeStart . ' - ' . $localeEnd;
- }
-
- /** @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();
- }
- }
-
- $localeStart = $l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' .
- $l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']);
-
- // always show full date with timezone if timezones are different
- if ($startTimezone !== $endTimezone) {
- $localeEnd = $l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
-
- return $localeStart . ' (' . $startTimezone . ') - ' .
- $localeEnd . ' (' . $endTimezone . ')';
- }
-
- // show only end time if date is the same
- if ($this->isDayEqual($dtstartDt, $dtendDt)) {
- $localeEnd = $l10n->l('time', $dtendDt, ['width' => 'short']);
- } else {
- $localeEnd = $l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' .
- $l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
- }
-
- return $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')';
- }
-
- /**
- * @param \DateTime $dtStart
- * @param \DateTime $dtEnd
- * @return bool
- */
- private function isDayEqual(\DateTime $dtStart, \DateTime $dtEnd) {
- return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
- }
-
- /**
- * @param IEMailTemplate $template
- * @param IL10N $l10n
- * @param string $method
- * @param string $summary
- */
- private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n,
- $method, $summary) {
- if ($method === self::METHOD_CANCEL) {
- // TRANSLATORS Subject for email, when an invitation is cancelled. Ex: "Cancelled: {{Event Name}}"
- $template->setSubject($l10n->t('Cancelled: %1$s', [$summary]));
- $template->addHeading($l10n->t('Invitation canceled'));
- } elseif ($method === self::METHOD_REPLY) {
- // TRANSLATORS Subject for email, when an invitation is updated. Ex: "Re: {{Event Name}}"
- $template->setSubject($l10n->t('Re: %1$s', [$summary]));
- $template->addHeading($l10n->t('Invitation updated'));
- } else {
- // TRANSLATORS Subject for email, when an invitation is sent. Ex: "Invitation: {{Event Name}}"
- $template->setSubject($l10n->t('Invitation: %1$s', [$summary]));
- $template->addHeading($l10n->t('Invitation'));
- }
- }
-
- /**
- * @param IEMailTemplate $template
- * @param IL10N $l10n
- * @param VEVENT $vevent
- */
- private function addBulletList(IEMailTemplate $template, IL10N $l10n, $vevent) {
- if ($vevent->SUMMARY) {
- $template->addBodyListItem($vevent->SUMMARY, $l10n->t('Title:'),
- $this->getAbsoluteImagePath('caldav/title.png'),'','',self::IMIP_INDENT);
- }
- $meetingWhen = $this->generateWhenString($l10n, $vevent);
- if ($meetingWhen) {
- $template->addBodyListItem($meetingWhen, $l10n->t('Time:'),
- $this->getAbsoluteImagePath('caldav/time.png'),'','',self::IMIP_INDENT);
- }
- if ($vevent->LOCATION) {
- $template->addBodyListItem($vevent->LOCATION, $l10n->t('Location:'),
- $this->getAbsoluteImagePath('caldav/location.png'),'','',self::IMIP_INDENT);
- }
- if ($vevent->URL) {
- $url = $vevent->URL->getValue();
- $template->addBodyListItem(sprintf('<a href="%s">%s</a>',
- htmlspecialchars($url),
- htmlspecialchars($url)),
- $l10n->t('Link:'),
- $this->getAbsoluteImagePath('caldav/link.png'),
- $url,'',self::IMIP_INDENT);
- }
-
- $this->addAttendees($template, $l10n, $vevent);
-
- /* Put description last, like an email body, since it can be arbitrarily long */
- if ($vevent->DESCRIPTION) {
- $template->addBodyListItem($vevent->DESCRIPTION->getValue(), $l10n->t('Description:'),
- $this->getAbsoluteImagePath('caldav/description.png'),'','',self::IMIP_INDENT);
- }
- }
-
- /**
- * addAttendees: add organizer and attendee names/emails to iMip mail.
- *
- * Enable with DAV setting: invitation_list_attendees (default: no)
- *
- * The default is 'no', which matches old behavior, and is privacy preserving.
- *
- * To enable including attendees in invitation emails:
- * % php occ config:app:set dav invitation_list_attendees --value yes
- *
- * @param IEMailTemplate $template
- * @param IL10N $l10n
- * @param Message $iTipMessage
- * @param int $lastOccurrence
- * @author brad2014 on github.com
- */
-
- private function addAttendees(IEMailTemplate $template, IL10N $l10n, VEvent $vevent) {
- if ($this->config->getAppValue('dav', 'invitation_list_attendees', 'no') === 'no') {
- return;
- }
-
- if (isset($vevent->ORGANIZER)) {
- /** @var Property\ICalendar\CalAddress $organizer */
- $organizer = $vevent->ORGANIZER;
- $organizerURI = $organizer->getNormalizedValue();
- [$scheme,$organizerEmail] = explode(':',$organizerURI,2); # strip off scheme mailto:
- /** @var string|null $organizerName */
- $organizerName = isset($organizer['CN']) ? $organizer['CN'] : null;
- $organizerHTML = sprintf('<a href="%s">%s</a>',
- htmlspecialchars($organizerURI),
- htmlspecialchars($organizerName ?: $organizerEmail));
- $organizerText = sprintf('%s <%s>', $organizerName, $organizerEmail);
- if (isset($organizer['PARTSTAT'])) {
- /** @var Parameter $partstat */
- $partstat = $organizer['PARTSTAT'];
- if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
- $organizerHTML .= ' ✔︎';
- $organizerText .= ' ✔︎';
- }
+ // 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);
}
- $template->addBodyListItem($organizerHTML, $l10n->t('Organizer:'),
- $this->getAbsoluteImagePath('caldav/organizer.png'),
- $organizerText,'',self::IMIP_INDENT);
- }
- $attendees = $vevent->select('ATTENDEE');
- if (count($attendees) === 0) {
- return;
- }
-
- $attendeesHTML = [];
- $attendeesText = [];
- foreach ($attendees as $attendee) {
- $attendeeURI = $attendee->getNormalizedValue();
- [$scheme,$attendeeEmail] = explode(':',$attendeeURI,2); # strip off scheme mailto:
- $attendeeName = isset($attendee['CN']) ? $attendee['CN'] : null;
- $attendeeHTML = sprintf('<a href="%s">%s</a>',
- htmlspecialchars($attendeeURI),
- htmlspecialchars($attendeeName ?: $attendeeEmail));
- $attendeeText = sprintf('%s <%s>', $attendeeName, $attendeeEmail);
- if (isset($attendee['PARTSTAT'])
- && strcasecmp($attendee['PARTSTAT'], 'ACCEPTED') === 0) {
- $attendeeHTML .= ' ✔︎';
- $attendeeText .= ' ✔︎';
+ $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)]);
+ $iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
}
- array_push($attendeesHTML, $attendeeHTML);
- array_push($attendeesText, $attendeeText);
+ } catch (\Exception $ex) {
+ $this->logger->error($ex->getMessage(), ['app' => 'dav', 'exception' => $ex]);
+ $iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
}
-
- $template->addBodyListItem(implode('<br/>',$attendeesHTML), $l10n->t('Attendees:'),
- $this->getAbsoluteImagePath('caldav/attendees.png'),
- implode("\n",$attendeesText),'',self::IMIP_INDENT);
}
/**
- * @param IEMailTemplate $template
- * @param IL10N $l10n
- * @param Message $iTipMessage
- * @param int $lastOccurrence
+ * @return ?VCalendar
*/
- private function addResponseButtons(IEMailTemplate $template, IL10N $l10n,
- Message $iTipMessage, $lastOccurrence) {
- $token = $this->createInvitationToken($iTipMessage, $lastOccurrence);
-
- $template->addBodyButtonGroup(
- $l10n->t('Accept'),
- $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.accept', [
- 'token' => $token,
- ]),
- $l10n->t('Decline'),
- $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.decline', [
- 'token' => $token,
- ])
- );
-
- $moreOptionsURL = $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.options', [
- 'token' => $token,
- ]);
- $html = vsprintf('<small><a href="%s">%s</a></small>', [
- $moreOptionsURL, $l10n->t('More options …')
- ]);
- $text = $l10n->t('More options at %s', [$moreOptionsURL]);
-
- $template->addBodyText($html, $text);
+ public function getVCalendar(): ?VCalendar {
+ return $this->vCalendar;
}
/**
- * @param string $path
- * @return string
+ * @param ?VCalendar $vCalendar
*/
- private function getAbsoluteImagePath($path) {
- return $this->urlGenerator->getAbsoluteURL(
- $this->urlGenerator->imagePath('core', $path)
- );
+ public function setVCalendar(?VCalendar $vCalendar): void {
+ $this->vCalendar = $vCalendar;
}
- /**
- * @param Message $iTipMessage
- * @param int $lastOccurrence
- * @return string
- */
- private function createInvitationToken(Message $iTipMessage, $lastOccurrence):string {
- $token = $this->random->generate(60, ISecureRandom::CHAR_ALPHANUMERIC);
-
- /** @var VEvent $vevent */
- $vevent = $iTipMessage->message->VEVENT;
- $attendee = $iTipMessage->recipient;
- $organizer = $iTipMessage->sender;
- $sequence = $iTipMessage->sequence;
- $recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ?
- $vevent->{'RECURRENCE-ID'}->serialize() : null;
- $uid = $vevent->{'UID'};
-
- $query = $this->db->getQueryBuilder();
- $query->insert('calendar_invitations')
- ->values([
- 'token' => $query->createNamedParameter($token),
- 'attendee' => $query->createNamedParameter($attendee),
- 'organizer' => $query->createNamedParameter($organizer),
- 'sequence' => $query->createNamedParameter($sequence),
- 'recurrenceid' => $query->createNamedParameter($recurrenceId),
- 'expiration' => $query->createNamedParameter($lastOccurrence),
- 'uid' => $query->createNamedParameter($uid)
- ])
- ->execute();
-
- return $token;
- }
}
diff --git a/apps/dav/lib/CalDAV/Schedule/IMipService.php b/apps/dav/lib/CalDAV/Schedule/IMipService.php
new file mode 100644
index 00000000000..54c0bc31849
--- /dev/null
+++ b/apps/dav/lib/CalDAV/Schedule/IMipService.php
@@ -0,0 +1,1294 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * 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;
+use OCP\L10N\IFactory as L10NFactory;
+use OCP\Mail\IEMailTemplate;
+use OCP\Security\ISecureRandom;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VEvent;
+use Sabre\VObject\DateTimeParser;
+use Sabre\VObject\ITip\Message;
+use Sabre\VObject\Parameter;
+use Sabre\VObject\Property;
+use Sabre\VObject\Recur\EventIterator;
+
+class IMipService {
+
+ private IL10N $l10n;
+
+ /** @var string[] */
+ private const STRING_DIFF = [
+ 'meeting_title' => 'SUMMARY',
+ 'meeting_description' => 'DESCRIPTION',
+ 'meeting_url' => 'URL',
+ 'meeting_location' => 'LOCATION'
+ ];
+
+ 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);
+ }
+
+ /**
+ * @param string|null $senderName
+ * @param string $default
+ * @return string
+ */
+ public function getFrom(?string $senderName, string $default): string {
+ if ($senderName === null) {
+ return $default;
+ }
+
+ return $this->l10n->t('%1$s via %2$s', [$senderName, $default]);
+ }
+
+ public static function readPropertyWithDefault(VEvent $vevent, string $property, string $default) {
+ if (isset($vevent->$property)) {
+ $value = $vevent->$property->getValue();
+ if (!empty($value)) {
+ return $value;
+ }
+ }
+ return $default;
+ }
+
+ private function generateDiffString(VEvent $vevent, VEvent $oldVEvent, string $property, string $default): ?string {
+ $strikethrough = "<span style='text-decoration: line-through'>%s</span><br />%s";
+ if (!isset($vevent->$property)) {
+ return $default;
+ }
+ $newstring = $vevent->$property->getValue();
+ if (isset($oldVEvent->$property) && $oldVEvent->$property->getValue() !== $newstring) {
+ $oldstring = $oldVEvent->$property->getValue();
+ return sprintf($strikethrough, $oldstring, $newstring);
+ }
+ return $newstring;
+ }
+
+ /**
+ * Like generateDiffString() but linkifies the property values if they are urls.
+ */
+ private function generateLinkifiedDiffString(VEvent $vevent, VEvent $oldVEvent, string $property, string $default): ?string {
+ if (!isset($vevent->$property)) {
+ return $default;
+ }
+ /** @var string|null $newString */
+ $newString = $vevent->$property->getValue();
+ $oldString = isset($oldVEvent->$property) ? $oldVEvent->$property->getValue() : null;
+ if ($oldString !== $newString) {
+ return sprintf(
+ "<span style='text-decoration: line-through'>%s</span><br />%s",
+ $this->linkify($oldString) ?? $oldString ?? '',
+ $this->linkify($newString) ?? $newString ?? ''
+ );
+ }
+ return $this->linkify($newString) ?? $newString;
+ }
+
+ /**
+ * Convert a given url to a html link element or return null otherwise.
+ */
+ private function linkify(?string $url): ?string {
+ if ($url === null) {
+ return null;
+ }
+ if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) {
+ return null;
+ }
+
+ return sprintf('<a href="%1$s">%1$s</a>', htmlspecialchars($url));
+ }
+
+ /**
+ * @param VEvent $vEvent
+ * @param VEvent|null $oldVEvent
+ * @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($eventReaderCurrent);
+
+ foreach (self::STRING_DIFF as $key => $property) {
+ $data[$key] = self::readPropertyWithDefault($vEvent, $property, $defaultVal);
+ }
+
+ $data['meeting_url_html'] = self::readPropertyWithDefault($vEvent, 'URL', $defaultVal);
+
+ if (($locationHtml = $this->linkify($data['meeting_location'])) !== null) {
+ $data['meeting_location_html'] = $locationHtml;
+ }
+
+ 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']);
+
+ $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'] ? 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 VEvent $vEvent
+ * @return array
+ */
+ public function buildReplyBodyData(VEvent $vEvent): array {
+ // construct event reader
+ $eventReader = new EventReader($vEvent);
+ $defaultVal = '';
+ $data = [];
+ $data['meeting_when'] = $this->generateWhenString($eventReader);
+
+ foreach (self::STRING_DIFF as $key => $property) {
+ $data[$key] = self::readPropertyWithDefault($vEvent, $property, $defaultVal);
+ }
+
+ if (($locationHtml = $this->linkify($data['meeting_location'])) !== null) {
+ $data['meeting_location_html'] = $locationHtml;
+ }
+
+ $data['meeting_url_html'] = $data['meeting_url'] ? sprintf('<a href="%1$s">%1$s</a>', $data['meeting_url']) : '';
+
+ // generate occurring next string
+ if ($eventReader->recurs()) {
+ $data['meeting_occurring'] = $this->generateOccurringString($eventReader);
+ }
+
+ return $data;
+ }
+
+ /**
+ * 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)
+ };
+ }
+
+ /**
+ * 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')
+ };
+ }
+
+ /**
+ * 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),
+ };
+ }
+
+ /**
+ * 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')
+ };
+
+ }
+
+ /**
+ * 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')
+ };
+
+ }
+
+ /**
+ * 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')
+ };
+ }
+
+ /**
+ * 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 {
+ $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')
+ };
+
+ }
+
+ /**
+ * @param VEvent $vEvent
+ * @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($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;
+ $newLocationHtml = $this->linkify($newLocation) ?? $newLocation;
+
+ $data = [];
+ $data['meeting_when_html'] = $newMeetingWhen === '' ?: sprintf($strikethrough, $newMeetingWhen);
+ $data['meeting_when'] = $newMeetingWhen;
+ $data['meeting_title_html'] = sprintf($strikethrough, $newSummary);
+ $data['meeting_title'] = $newSummary !== '' ? $newSummary: $this->l10n->t('Untitled event');
+ $data['meeting_description_html'] = $newDescription !== '' ? sprintf($strikethrough, $newDescription) : '';
+ $data['meeting_description'] = $newDescription;
+ $data['meeting_url_html'] = $newUrl !== '' ? sprintf($strikethrough, $newUrl) : '';
+ $data['meeting_url'] = isset($vEvent->URL) ? (string)$vEvent->URL : '';
+ $data['meeting_location_html'] = $newLocationHtml !== '' ? sprintf($strikethrough, $newLocationHtml) : '';
+ $data['meeting_location'] = $newLocation;
+ return $data;
+ }
+
+ /**
+ * Check if event took place in the past
+ *
+ * @param VCalendar $vObject
+ * @return int
+ */
+ public function getLastOccurrence(VCalendar $vObject) {
+ /** @var VEvent $component */
+ $component = $vObject->VEVENT;
+
+ if (isset($component->RRULE)) {
+ $it = new EventIterator($vObject, (string)$component->UID);
+ $maxDate = new \DateTime(IMipPlugin::MAX_DATE);
+ if ($it->isInfinite()) {
+ return $maxDate->getTimestamp();
+ }
+
+ $end = $it->getDtEnd();
+ while ($it->valid() && $end < $maxDate) {
+ $end = $it->getDtEnd();
+ $it->next();
+ }
+ return $end->getTimestamp();
+ }
+
+ /** @var Property\ICalendar\DateTime $dtStart */
+ $dtStart = $component->DTSTART;
+
+ if (isset($component->DTEND)) {
+ /** @var Property\ICalendar\DateTime $dtEnd */
+ $dtEnd = $component->DTEND;
+ return $dtEnd->getDateTime()->getTimeStamp();
+ }
+
+ if (isset($component->DURATION)) {
+ /** @var \DateTime $endDate */
+ $endDate = clone $dtStart->getDateTime();
+ // $component->DTEND->getDateTime() returns DateTimeImmutable
+ $endDate = $endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
+ return $endDate->getTimestamp();
+ }
+
+ if (!$dtStart->hasTime()) {
+ /** @var \DateTime $endDate */
+ // $component->DTSTART->getDateTime() returns DateTimeImmutable
+ $endDate = clone $dtStart->getDateTime();
+ $endDate = $endDate->modify('+1 day');
+ return $endDate->getTimestamp();
+ }
+
+ // No computation of end time possible - return start date
+ return $dtStart->getDateTime()->getTimeStamp();
+ }
+
+ /**
+ * @param Property|null $attendee
+ */
+ public function setL10n(?Property $attendee = null) {
+ if ($attendee === null) {
+ return;
+ }
+
+ $lang = $attendee->offsetGet('LANGUAGE');
+ if ($lang instanceof Parameter) {
+ $lang = $lang->getValue();
+ $this->l10n = $this->l10nFactory->get('dav', $lang);
+ }
+ }
+
+ /**
+ * @param Property|null $attendee
+ * @return bool
+ */
+ public function getAttendeeRsvpOrReqForParticipant(?Property $attendee = null) {
+ if ($attendee === null) {
+ return false;
+ }
+
+ $rsvp = $attendee->offsetGet('RSVP');
+ if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) {
+ return true;
+ }
+ $role = $attendee->offsetGet('ROLE');
+ // @see https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.16
+ // Attendees without a role are assumed required and should receive an invitation link even if they have no RSVP set
+ if ($role === null
+ || (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'REQ-PARTICIPANT') === 0))
+ || (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'OPT-PARTICIPANT') === 0))
+ ) {
+ return true;
+ }
+
+ // RFC 5545 3.2.17: default RSVP is false
+ return false;
+ }
+
+ /**
+ * @param IEMailTemplate $template
+ * @param string $method
+ * @param string $sender
+ * @param string $summary
+ * @param string|null $partstat
+ * @param bool $isModified
+ */
+ public function addSubjectAndHeading(IEMailTemplate $template,
+ string $method, string $sender, string $summary, bool $isModified, ?Property $replyingAttendee = null): void {
+ if ($method === IMipPlugin::METHOD_CANCEL) {
+ // TRANSLATORS Subject for email, when an invitation is cancelled. Ex: "Cancelled: {{Event Name}}"
+ $template->setSubject($this->l10n->t('Cancelled: %1$s', [$summary]));
+ $template->addHeading($this->l10n->t('"%1$s" has been canceled', [$summary]));
+ } elseif ($method === IMipPlugin::METHOD_REPLY) {
+ // TRANSLATORS Subject for email, when an invitation is replied to. Ex: "Re: {{Event Name}}"
+ $template->setSubject($this->l10n->t('Re: %1$s', [$summary]));
+ // Build the strings
+ $partstat = (isset($replyingAttendee)) ? $replyingAttendee->offsetGet('PARTSTAT') : null;
+ $partstat = ($partstat instanceof Parameter) ? $partstat->getValue() : null;
+ switch ($partstat) {
+ case 'ACCEPTED':
+ $template->addHeading($this->l10n->t('%1$s has accepted your invitation', [$sender]));
+ break;
+ case 'TENTATIVE':
+ $template->addHeading($this->l10n->t('%1$s has tentatively accepted your invitation', [$sender]));
+ break;
+ case 'DECLINED':
+ $template->addHeading($this->l10n->t('%1$s has declined your invitation', [$sender]));
+ break;
+ case null:
+ default:
+ $template->addHeading($this->l10n->t('%1$s has responded to your invitation', [$sender]));
+ break;
+ }
+ } elseif ($method === IMipPlugin::METHOD_REQUEST && $isModified) {
+ // TRANSLATORS Subject for email, when an invitation is updated. Ex: "Invitation updated: {{Event Name}}"
+ $template->setSubject($this->l10n->t('Invitation updated: %1$s', [$summary]));
+ $template->addHeading($this->l10n->t('%1$s updated the event "%2$s"', [$sender, $summary]));
+ } else {
+ // TRANSLATORS Subject for email, when an invitation is sent. Ex: "Invitation: {{Event Name}}"
+ $template->setSubject($this->l10n->t('Invitation: %1$s', [$summary]));
+ $template->addHeading($this->l10n->t('%1$s would like to invite you to "%2$s"', [$sender, $summary]));
+ }
+ }
+
+ /**
+ * @param string $path
+ * @return string
+ */
+ public function getAbsoluteImagePath($path): string {
+ return $this->urlGenerator->getAbsoluteURL(
+ $this->urlGenerator->imagePath('core', $path)
+ );
+ }
+
+ /**
+ * addAttendees: add organizer and attendee names/emails to iMip mail.
+ *
+ * Enable with DAV setting: invitation_list_attendees (default: no)
+ *
+ * The default is 'no', which matches old behavior, and is privacy preserving.
+ *
+ * To enable including attendees in invitation emails:
+ * % php occ config:app:set dav invitation_list_attendees --value yes
+ *
+ * @param IEMailTemplate $template
+ * @param IL10N $this->l10n
+ * @param VEvent $vevent
+ * @author brad2014 on github.com
+ */
+ public function addAttendees(IEMailTemplate $template, VEvent $vevent) {
+ if ($this->config->getAppValue('dav', 'invitation_list_attendees', 'no') === 'no') {
+ return;
+ }
+
+ if (isset($vevent->ORGANIZER)) {
+ /** @var Property | Property\ICalendar\CalAddress $organizer */
+ $organizer = $vevent->ORGANIZER;
+ $organizerEmail = substr($organizer->getNormalizedValue(), 7);
+ /** @var string|null $organizerName */
+ $organizerName = isset($organizer->CN) ? $organizer->CN->getValue() : null;
+ $organizerHTML = sprintf('<a href="%s">%s</a>',
+ htmlspecialchars($organizer->getNormalizedValue()),
+ htmlspecialchars($organizerName ?: $organizerEmail));
+ $organizerText = sprintf('%s <%s>', $organizerName, $organizerEmail);
+ if (isset($organizer['PARTSTAT'])) {
+ /** @var Parameter $partstat */
+ $partstat = $organizer['PARTSTAT'];
+ if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
+ $organizerHTML .= ' ✔︎';
+ $organizerText .= ' ✔︎';
+ }
+ }
+ $template->addBodyListItem($organizerHTML, $this->l10n->t('Organizer:'),
+ $this->getAbsoluteImagePath('caldav/organizer.png'),
+ $organizerText, '', IMipPlugin::IMIP_INDENT);
+ }
+
+ $attendees = $vevent->select('ATTENDEE');
+ if (count($attendees) === 0) {
+ return;
+ }
+
+ $attendeesHTML = [];
+ $attendeesText = [];
+ foreach ($attendees as $attendee) {
+ $attendeeEmail = substr($attendee->getNormalizedValue(), 7);
+ $attendeeName = isset($attendee['CN']) ? $attendee['CN']->getValue() : null;
+ $attendeeHTML = sprintf('<a href="%s">%s</a>',
+ htmlspecialchars($attendee->getNormalizedValue()),
+ htmlspecialchars($attendeeName ?: $attendeeEmail));
+ $attendeeText = sprintf('%s <%s>', $attendeeName, $attendeeEmail);
+ if (isset($attendee['PARTSTAT'])) {
+ /** @var Parameter $partstat */
+ $partstat = $attendee['PARTSTAT'];
+ if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
+ $attendeeHTML .= ' ✔︎';
+ $attendeeText .= ' ✔︎';
+ }
+ }
+ $attendeesHTML[] = $attendeeHTML;
+ $attendeesText[] = $attendeeText;
+ }
+
+ $template->addBodyListItem(implode('<br/>', $attendeesHTML), $this->l10n->t('Attendees:'),
+ $this->getAbsoluteImagePath('caldav/attendees.png'),
+ implode("\n", $attendeesText), '', IMipPlugin::IMIP_INDENT);
+ }
+
+ /**
+ * @param IEMailTemplate $template
+ * @param VEVENT $vevent
+ * @param $data
+ */
+ public function addBulletList(IEMailTemplate $template, VEvent $vevent, $data) {
+ $template->addBodyListItem(
+ $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('When:'),
+ $this->getAbsoluteImagePath('caldav/time.png'), $data['meeting_when'], '', IMipPlugin::IMIP_INDENT);
+ }
+ if ($data['meeting_location'] !== '') {
+ $template->addBodyListItem($data['meeting_location_html'] ?? $data['meeting_location'], $this->l10n->t('Location:'),
+ $this->getAbsoluteImagePath('caldav/location.png'), $data['meeting_location'], '', IMipPlugin::IMIP_INDENT);
+ }
+ if ($data['meeting_url'] !== '') {
+ $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);
+
+ /* Put description last, like an email body, since it can be arbitrarily long */
+ if ($data['meeting_description']) {
+ $template->addBodyListItem($data['meeting_description_html'] ?? $data['meeting_description'], $this->l10n->t('Description:'),
+ $this->getAbsoluteImagePath('caldav/description.png'), $data['meeting_description'], '', IMipPlugin::IMIP_INDENT);
+ }
+ }
+
+ /**
+ * @param Message $iTipMessage
+ * @return null|Property
+ */
+ public function getCurrentAttendee(Message $iTipMessage): ?Property {
+ /** @var VEvent $vevent */
+ $vevent = $iTipMessage->message->VEVENT;
+ $attendees = $vevent->select('ATTENDEE');
+ foreach ($attendees as $attendee) {
+ 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;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @param Message $iTipMessage
+ * @param VEvent $vevent
+ * @param int $lastOccurrence
+ * @return string
+ */
+ public function createInvitationToken(Message $iTipMessage, VEvent $vevent, int $lastOccurrence): string {
+ $token = $this->random->generate(60, ISecureRandom::CHAR_ALPHANUMERIC);
+
+ $attendee = $iTipMessage->recipient;
+ $organizer = $iTipMessage->sender;
+ $sequence = $iTipMessage->sequence;
+ $recurrenceId = isset($vevent->{'RECURRENCE-ID'})
+ ? $vevent->{'RECURRENCE-ID'}->serialize() : null;
+ $uid = $vevent->{'UID'}?->getValue();
+
+ $query = $this->db->getQueryBuilder();
+ $query->insert('calendar_invitations')
+ ->values([
+ 'token' => $query->createNamedParameter($token),
+ 'attendee' => $query->createNamedParameter($attendee),
+ 'organizer' => $query->createNamedParameter($organizer),
+ 'sequence' => $query->createNamedParameter($sequence),
+ 'recurrenceid' => $query->createNamedParameter($recurrenceId),
+ 'expiration' => $query->createNamedParameter($lastOccurrence),
+ 'uid' => $query->createNamedParameter($uid)
+ ])
+ ->executeStatement();
+
+ return $token;
+ }
+
+ /**
+ * @param IEMailTemplate $template
+ * @param $token
+ */
+ public function addResponseButtons(IEMailTemplate $template, $token) {
+ $template->addBodyButtonGroup(
+ $this->l10n->t('Accept'),
+ $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.accept', [
+ 'token' => $token,
+ ]),
+ $this->l10n->t('Decline'),
+ $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.decline', [
+ 'token' => $token,
+ ])
+ );
+ }
+
+ public function addMoreOptionsButton(IEMailTemplate $template, $token) {
+ $moreOptionsURL = $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.options', [
+ 'token' => $token,
+ ]);
+ $html = vsprintf('<small><a href="%s">%s</a></small>', [
+ $moreOptionsURL, $this->l10n->t('More options …')
+ ]);
+ $text = $this->l10n->t('More options at %s', [$moreOptionsURL]);
+
+ $template->addBodyText($html, $text);
+ }
+
+ public function getReplyingAttendee(Message $iTipMessage): ?Property {
+ /** @var VEvent $vevent */
+ $vevent = $iTipMessage->message->VEVENT;
+ $attendees = $vevent->select('ATTENDEE');
+ foreach ($attendees as $attendee) {
+ /** @var Property $attendee */
+ if (strcasecmp($attendee->getValue(), $iTipMessage->sender) === 0) {
+ return $attendee;
+ }
+ }
+ return null;
+ }
+
+ public function isRoomOrResource(Property $attendee): bool {
+ $cuType = $attendee->offsetGet('CUTYPE');
+ if (!$cuType instanceof Parameter) {
+ return false;
+ }
+ $type = $cuType->getValue() ?? 'INDIVIDUAL';
+ 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 96bacce4454..a001df8b2a8 100644
--- a/apps/dav/lib/CalDAV/Schedule/Plugin.php
+++ b/apps/dav/lib/CalDAV/Schedule/Plugin.php
@@ -1,43 +1,30 @@
<?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>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have 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;
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;
use Sabre\DAV\Server;
use Sabre\DAV\Xml\Property\LocalHref;
+use Sabre\DAVACL\IACL;
use Sabre\DAVACL\IPrincipal;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
@@ -47,18 +34,14 @@ use Sabre\VObject\Component\VEvent;
use Sabre\VObject\DateTimeParser;
use Sabre\VObject\FreeBusyGenerator;
use Sabre\VObject\ITip;
+use Sabre\VObject\ITip\SameOrganizerForAllComponentsException;
use Sabre\VObject\Parameter;
use Sabre\VObject\Property;
use Sabre\VObject\Reader;
-use function \Sabre\Uri\split;
+use function Sabre\Uri\split;
class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
- /**
- * @var IConfig
- */
- private $config;
-
/** @var ITip\Message[] */
private $schedulingResponses = [];
@@ -71,8 +54,11 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
/**
* @param IConfig $config
*/
- public function __construct(IConfig $config) {
- $this->config = $config;
+ public function __construct(
+ private IConfig $config,
+ private LoggerInterface $logger,
+ private DefaultCalendarValidator $defaultCalendarValidator,
+ ) {
}
/**
@@ -86,6 +72,20 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
$server->on('propFind', [$this, 'propFindDefaultCalendarUrl'], 90);
$server->on('afterWriteContent', [$this, 'dispatchSchedulingResponses']);
$server->on('afterCreateFile', [$this, 'dispatchSchedulingResponses']);
+
+ // We allow mutating the default calendar URL through the CustomPropertiesBackend
+ // (oc_properties table)
+ $server->protectedProperties = array_filter(
+ $server->protectedProperties,
+ static fn (string $property) => $property !== self::SCHEDULE_DEFAULT_CALENDAR_URL,
+ );
+ }
+
+ /**
+ * Returns an instance of the iTip\Broker.
+ */
+ protected function createITipBroker(): TipBroker {
+ return new TipBroker();
}
/**
@@ -139,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;
}
@@ -156,20 +161,91 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
$this->pathOfCalendarObjectChange = $request->getPath();
}
- parent::calendarObjectChange($request, $response, $vCal, $calendarPath, $modified, $isNew);
+ try {
+
+ // 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);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function beforeUnbind($path): void {
+ try {
+ parent::beforeUnbind($path);
+ } catch (SameOrganizerForAllComponentsException $e) {
+ $node = $this->server->tree->getNodeForPath($path);
+ if (!$node instanceof ICalendarObject || $node instanceof ISchedulingObject) {
+ throw $e;
+ }
+
+ /** @var VCalendar $vCal */
+ $vCal = Reader::read($node->get());
+ $this->handleSameOrganizerException($e, $vCal, $path);
+ }
}
/**
* @inheritDoc
*/
public function scheduleLocalDelivery(ITip\Message $iTipMessage):void {
- parent::scheduleLocalDelivery($iTipMessage);
+ /** @var VEvent|null $vevent */
+ $vevent = $iTipMessage->message->VEVENT ?? null;
+
+ // Strip VALARMs from incoming VEVENT
+ if ($vevent && isset($vevent->VALARM)) {
+ $vevent->remove('VALARM');
+ }
+ parent::scheduleLocalDelivery($iTipMessage);
// We only care when the message was successfully delivered locally
+ // Log all possible codes returned from the parent method that mean something went wrong
+ // 3.7, 3.8, 5.0, 5.2
if ($iTipMessage->scheduleStatus !== '1.2;Message delivered locally') {
+ $this->logger->debug('Message not delivered locally with status: ' . $iTipMessage->scheduleStatus);
return;
}
-
// We only care about request. reply and cancel are properly handled
// by parent::scheduleLocalDelivery already
if (strcasecmp($iTipMessage->method, 'REQUEST') !== 0) {
@@ -178,41 +254,38 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
// If parent::scheduleLocalDelivery set scheduleStatus to 1.2,
// it means that it was successfully delivered locally.
- // Meaning that the ACL plugin is loaded and that a principial
+ // Meaning that the ACL plugin is loaded and that a principal
// exists for the given recipient id, no need to double check
/** @var \Sabre\DAVACL\Plugin $aclPlugin */
$aclPlugin = $this->server->getPlugin('acl');
$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 neither room nor resource, not processing further');
return;
}
$attendee = $this->getCurrentAttendee($iTipMessage);
if (!$attendee) {
+ $this->logger->debug('No attendee set for scheduling message');
return;
}
// We only respond when a response was actually requested
$rsvp = $this->getAttendeeRSVP($attendee);
if (!$rsvp) {
+ $this->logger->debug('No RSVP requested for attendee ' . $attendee->getValue());
return;
}
- if (!isset($iTipMessage->message)) {
- return;
- }
-
- $vcalendar = $iTipMessage->message;
- if (!isset($vcalendar->VEVENT)) {
+ if (!$vevent) {
+ $this->logger->debug('No VEVENT set to process on scheduling message');
return;
}
- /** @var Component $vevent */
- $vevent = $vcalendar->VEVENT;
-
// We don't support autoresponses for recurrencing events for now
if (isset($vevent->RRULE) || isset($vevent->RDATE)) {
+ $this->logger->debug('VEVENT is a recurring event, autoresponding not supported');
return;
}
@@ -299,12 +372,14 @@ EOF;
return null;
}
- if (strpos($principalUrl, 'principals/users') === 0) {
+ $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);
$uri = $this->config->getUserValue($userId, 'dav', 'defaultCalendar', CalDavBackend::PERSONAL_CALENDAR_URI);
$displayName = CalDavBackend::PERSONAL_CALENDAR_NAME;
- } elseif (strpos($principalUrl, 'principals/calendar-resources') === 0 ||
- strpos($principalUrl, 'principals/calendar-rooms') === 0) {
+ } elseif ($isResourceOrRoom) {
$uri = CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI;
$displayName = CalDavBackend::RESOURCE_BOOKING_CALENDAR_NAME;
} else {
@@ -315,10 +390,65 @@ EOF;
/** @var CalendarHome $calendarHome */
$calendarHome = $this->server->tree->getNodeForPath($calendarHomePath);
- if (!$calendarHome->childExists($uri)) {
- $calendarHome->getCalDAVBackend()->createCalendar($principalUrl, $uri, [
- '{DAV:}displayname' => $displayName,
- ]);
+ $currentCalendarDeleted = false;
+ if (!$calendarHome->childExists($uri) || $currentCalendarDeleted = $this->isCalendarDeleted($calendarHome, $uri)) {
+ // If the default calendar doesn't exist
+ if ($isResourceOrRoom) {
+ // Resources or rooms can't be in the trashbin, so we're fine
+ $this->createCalendar($calendarHome, $principalUrl, $uri, $displayName);
+ } else {
+ // And we're not handling scheduling on resource/room booking
+ $userCalendars = [];
+ /**
+ * If the default calendar of the user isn't set and the
+ * fallback doesn't match any of the user's calendar
+ * try to find the first "personal" calendar we can write to
+ * instead of creating a new one.
+ * A appropriate personal calendar to receive invites:
+ * - 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)) {
+ continue;
+ }
+
+ try {
+ $this->defaultCalendarValidator->validateScheduleDefaultCalendar($node);
+ } catch (DavException $e) {
+ continue;
+ }
+
+ $userCalendars[] = $node;
+ }
+
+ if (count($userCalendars) > 0) {
+ // Calendar backend returns calendar by calendarorder property
+ $uri = $userCalendars[0]->getName();
+ } else {
+ // Otherwise if we have really nothing, create a new calendar
+ if ($currentCalendarDeleted) {
+ // 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) {
+ $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);
+ }
+ }
}
$result = $this->server->getPropertiesForPath($calendarHomePath . '/' . $uri, [], 1);
@@ -373,7 +503,7 @@ EOF;
* @param Property|null $attendee
* @return bool
*/
- private function getAttendeeRSVP(Property $attendee = null):bool {
+ private function getAttendeeRSVP(?Property $attendee = null):bool {
if ($attendee !== null) {
$rsvp = $attendee->offsetGet('RSVP');
if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) {
@@ -448,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;
}
@@ -533,7 +665,7 @@ EOF;
}
// If more than one Free-Busy property was returned, it means that an event
- // starts or ends inside this time-range, so it's not availabe and we return false
+ // starts or ends inside this time-range, so it's not available and we return false
if (count($freeBusyProperties) > 1) {
return false;
}
@@ -564,4 +696,63 @@ EOF;
return $email;
}
+
+ private function getCalendar(CalendarHome $calendarHome, string $uri): INode {
+ return $calendarHome->getChild($uri);
+ }
+
+ private function isCalendarDeleted(CalendarHome $calendarHome, string $uri): bool {
+ $calendar = $this->getCalendar($calendarHome, $uri);
+ return $calendar instanceof Calendar && $calendar->isDeleted();
+ }
+
+ private function createCalendar(CalendarHome $calendarHome, string $principalUri, string $uri, string $displayName): void {
+ $calendarHome->getCalDAVBackend()->createCalendar($principalUri, $uri, [
+ '{DAV:}displayname' => $displayName,
+ ]);
+ }
+
+ 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.
+ *
+ * @throws SameOrganizerForAllComponentsException If the exception should not be ignored
+ */
+ private function handleSameOrganizerException(
+ SameOrganizerForAllComponentsException $e,
+ VCalendar $vCal,
+ string $calendarPath,
+ ): void {
+ // This is very hacky! However, we want to allow saving events with multiple
+ // organizers. Those events are not RFC compliant, but sometimes imported from major
+ // external calendar services (e.g. Google). If the current user is not an organizer of
+ // the event we ignore the exception as no scheduling messages will be sent anyway.
+
+ // It would be cleaner to patch Sabre to validate organizers *after* checking if
+ // scheduling messages are necessary. Currently, organizers are validated first and
+ // afterwards the broker checks if messages should be scheduled. So the code will throw
+ // even if the organizers are not relevant. This is to ensure compliance with RFCs but
+ // a bit too strict for real world usage.
+
+ if (!isset($vCal->VEVENT)) {
+ throw $e;
+ }
+
+ $calendarNode = $this->server->tree->getNodeForPath($calendarPath);
+ if (!($calendarNode instanceof IACL)) {
+ // Should always be an instance of IACL but just to be sure
+ throw $e;
+ }
+
+ $addresses = $this->getAddressesForPrincipal($calendarNode->getOwner());
+ foreach ($vCal->VEVENT as $vevent) {
+ if (in_array($vevent->ORGANIZER->getNormalizedValue(), $addresses, true)) {
+ // User is an organizer => throw the exception
+ throw $e;
+ }
+ }
+ }
}
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
new file mode 100644
index 00000000000..311157994e2
--- /dev/null
+++ b/apps/dav/lib/CalDAV/Security/RateLimitingPlugin.php
@@ -0,0 +1,87 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV\Security;
+
+use OC\Security\RateLimiting\Exception\RateLimitExceededException;
+use OC\Security\RateLimiting\Limiter;
+use OCA\DAV\CalDAV\CalDavBackend;
+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 RateLimitingPlugin extends ServerPlugin {
+
+ private Limiter $limiter;
+
+ public function __construct(
+ Limiter $limiter,
+ private IUserManager $userManager,
+ private CalDavBackend $calDavBackend,
+ private LoggerInterface $logger,
+ private IAppConfig $config,
+ private ?string $userId,
+ ) {
+ $this->limiter = $limiter;
+ }
+
+ 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) === 3 && $pathParts[0] === 'calendars') {
+ // Path looks like calendars/username/calendarname so a new calendar or subscription is created
+ try {
+ $this->limiter->registerUserRequest(
+ 'caldav-create-calendar',
+ $this->config->getValueInt('dav', 'rateLimitCalendarCreation', 10),
+ $this->config->getValueInt('dav', 'rateLimitPeriodCalendarCreation', 3600),
+ $user
+ );
+ } catch (RateLimitExceededException $e) {
+ throw new TooManyRequests('Too many calendars created', 0, $e);
+ }
+
+ $calendarLimit = $this->config->getValueInt('dav', 'maximumCalendarsSubscriptions', 30);
+ if ($calendarLimit === -1) {
+ return;
+ }
+ $numCalendars = $this->calDavBackend->getCalendarsForUserCount('principals/users/' . $user->getUID());
+ $numSubscriptions = $this->calDavBackend->getSubscriptionsForUserCount('principals/users/' . $user->getUID());
+
+ if (($numCalendars + $numSubscriptions) >= $calendarLimit) {
+ $this->logger->warning('Maximum number of calendars/subscriptions reached', [
+ 'calendars' => $numCalendars,
+ 'subscription' => $numSubscriptions,
+ 'limit' => $calendarLimit,
+ ]);
+ throw new Forbidden('Calendar limit reached', 0);
+ }
+ }
+ }
+
+}
diff --git a/apps/dav/lib/CalDAV/Sharing/Backend.php b/apps/dav/lib/CalDAV/Sharing/Backend.php
new file mode 100644
index 00000000000..fc5d65b5994
--- /dev/null
+++ b/apps/dav/lib/CalDAV/Sharing/Backend.php
@@ -0,0 +1,30 @@
+<?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\Sharing;
+
+use OCA\DAV\Connector\Sabre\Principal;
+use OCA\DAV\DAV\Sharing\Backend as SharingBackend;
+use OCP\ICacheFactory;
+use OCP\IGroupManager;
+use OCP\IUserManager;
+use Psr\Log\LoggerInterface;
+
+class Backend extends SharingBackend {
+
+ public function __construct(
+ private IUserManager $userManager,
+ private IGroupManager $groupManager,
+ private Principal $principalBackend,
+ private ICacheFactory $cacheFactory,
+ private Service $service,
+ private LoggerInterface $logger,
+ ) {
+ parent::__construct($this->userManager, $this->groupManager, $this->principalBackend, $this->cacheFactory, $this->service, $this->logger);
+ }
+}
diff --git a/apps/dav/lib/CalDAV/Sharing/Service.php b/apps/dav/lib/CalDAV/Sharing/Service.php
new file mode 100644
index 00000000000..4f0554f09bd
--- /dev/null
+++ b/apps/dav/lib/CalDAV/Sharing/Service.php
@@ -0,0 +1,21 @@
+<?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\Sharing;
+
+use OCA\DAV\DAV\Sharing\SharingMapper;
+use OCA\DAV\DAV\Sharing\SharingService;
+
+class Service extends SharingService {
+ protected string $resourceType = 'calendar';
+ 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
new file mode 100644
index 00000000000..9ee0e9bf356
--- /dev/null
+++ b/apps/dav/lib/CalDAV/Status/StatusService.php
@@ -0,0 +1,186 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\CalDAV\Status;
+
+use DateTimeImmutable;
+use OC\Calendar\CalendarQuery;
+use OCA\DAV\CalDAV\CalendarImpl;
+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;
+use OCP\IUserManager;
+use OCP\User\IAvailabilityCoordinator;
+use OCP\UserStatus\IUserStatus;
+use Psr\Log\LoggerInterface;
+use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
+
+class StatusService {
+ private ICache $cache;
+ public function __construct(
+ private ITimeFactory $timeFactory,
+ private IManager $calendarManager,
+ private IUserManager $userManager,
+ private UserStatusService $userStatusService,
+ private IAvailabilityCoordinator $availabilityCoordinator,
+ private ICacheFactory $cacheFactory,
+ private LoggerInterface $logger,
+ ) {
+ $this->cache = $cacheFactory->createLocal('CalendarStatusService');
+ }
+
+ public function processCalendarStatus(string $userId): void {
+ $user = $this->userManager->get($userId);
+ if ($user === null) {
+ return;
+ }
+
+ $availability = $this->availabilityCoordinator->getCurrentOutOfOfficeData($user);
+ 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) {
+ $calendarEvents = $this->getCalendarEvents($user);
+ $this->cache->set($userId, $calendarEvents, 300);
+ }
+
+ 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;
+ }
+
+ try {
+ $currentStatus = $this->userStatusService->findByUserId($userId);
+ // Was the status set by anything other than the calendar automation?
+ $userStatusTimestamp = $currentStatus->getIsUserDefined() && $currentStatus->getMessageId() !== IUserStatus::MESSAGE_CALENDAR_BUSY ? $currentStatus->getStatusTimestamp() : null;
+ } catch (DoesNotExistException) {
+ $userStatusTimestamp = null;
+ $currentStatus = null;
+ }
+
+ 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;
+ }
+
+ // Filter events to see if we have any that apply to the calendar status
+ $applicableEvents = array_filter($calendarEvents, static function (array $calendarEvent) use ($userStatusTimestamp): bool {
+ if (empty($calendarEvent['objects'])) {
+ return false;
+ }
+ $component = $calendarEvent['objects'][0];
+ if (isset($component['X-NEXTCLOUD-OUT-OF-OFFICE'])) {
+ return false;
+ }
+ if (isset($component['DTSTART']) && $userStatusTimestamp !== null) {
+ /** @var DateTimeImmutable $dateTime */
+ $dateTime = $component['DTSTART'][0];
+ if ($dateTime instanceof DateTimeImmutable && $userStatusTimestamp > $dateTime->getTimestamp()) {
+ return false;
+ }
+ }
+ // Ignore events that are transparent
+ if (isset($component['TRANSP']) && strcasecmp($component['TRANSP'][0], 'TRANSPARENT') === 0) {
+ return false;
+ }
+ return true;
+ });
+
+ 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) {
+ // 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
+ // 3. Event is not set to be transparent
+ $count = count($applicableEvents);
+ $this->logger->debug("Found $count applicable event(s), changing user status", ['user' => $userId]);
+ $this->userStatusService->setUserStatus(
+ $userId,
+ IUserStatus::BUSY,
+ IUserStatus::MESSAGE_CALENDAR_BUSY,
+ true
+ );
+ }
+ }
+
+ private function getCalendarEvents(User $user): array {
+ $calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $user->getUID());
+ if (empty($calendars)) {
+ return [];
+ }
+
+ $query = $this->calendarManager->newQuery('principals/users/' . $user->getUID());
+ foreach ($calendars as $calendarObject) {
+ // We can only work with a calendar if it exposes its scheduling information
+ if (!$calendarObject instanceof CalendarImpl) {
+ continue;
+ }
+
+ $sct = $calendarObject->getSchedulingTransparency();
+ if ($sct !== null && strtolower($sct->getValue()) == ScheduleCalendarTransp::TRANSPARENT) {
+ // If a calendar is marked as 'transparent', it means we must
+ // ignore it for free-busy purposes.
+ continue;
+ }
+ $query->addSearchCalendar($calendarObject->getUri());
+ }
+
+ $dtStart = DateTimeImmutable::createFromMutable($this->timeFactory->getDateTime());
+ $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())) {
+ // Query the next hour
+ $query->setTimerangeStart($dtStart);
+ $query->setTimerangeEnd($dtEnd);
+ return $this->calendarManager->searchForPrincipal($query);
+ }
+
+ return [];
+ }
+}
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
new file mode 100644
index 00000000000..a7709bde0f9
--- /dev/null
+++ b/apps/dav/lib/CalDAV/TimezoneService.php
@@ -0,0 +1,93 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use OCA\DAV\Db\PropertyMapper;
+use OCP\Calendar\ICalendar;
+use OCP\Calendar\IManager;
+use OCP\IConfig;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VTimeZone;
+use Sabre\VObject\Reader;
+use function array_reduce;
+
+class TimezoneService {
+
+ public function __construct(
+ private IConfig $config,
+ private PropertyMapper $propertyMapper,
+ private IManager $calendarManager,
+ ) {
+ }
+
+ public function getUserTimezone(string $userId): ?string {
+ $fromConfig = $this->config->getUserValue(
+ $userId,
+ 'core',
+ 'timezone',
+ );
+ if ($fromConfig !== '') {
+ return $fromConfig;
+ }
+
+ $availabilityPropPath = 'calendars/' . $userId . '/inbox';
+ $availabilityProp = '{' . Plugin::NS_CALDAV . '}calendar-availability';
+ $availabilities = $this->propertyMapper->findPropertyByPathAndName($userId, $availabilityPropPath, $availabilityProp);
+ if (!empty($availabilities)) {
+ $availability = $availabilities[0]->getPropertyvalue();
+ /** @var VCalendar $vCalendar */
+ $vCalendar = Reader::read($availability);
+ /** @var VTimeZone $vTimezone */
+ $vTimezone = $vCalendar->VTIMEZONE;
+ // Sabre has a fallback to date_default_timezone_get
+ return $vTimezone->getTimeZone()->getName();
+ }
+
+ $principal = 'principals/users/' . $userId;
+ $uri = $this->config->getUserValue($userId, 'dav', 'defaultCalendar', CalDavBackend::PERSONAL_CALENDAR_URI);
+ $calendars = $this->calendarManager->getCalendarsForPrincipal($principal);
+
+ /** @var ?VTimeZone $personalCalendarTimezone */
+ $personalCalendarTimezone = array_reduce($calendars, function (?VTimeZone $acc, ICalendar $calendar) use ($uri) {
+ if ($acc !== null) {
+ return $acc;
+ }
+ if ($calendar->getUri() === $uri && !$calendar->isDeleted() && $calendar instanceof CalendarImpl) {
+ return $calendar->getSchedulingTimezone();
+ }
+ return null;
+ });
+ if ($personalCalendarTimezone !== null) {
+ return $personalCalendarTimezone->getTimeZone()->getName();
+ }
+
+ // No timezone in the personalCalendarTimezone calendar or no personalCalendarTimezone calendar
+ // Loop through all calendars until we find a timezone.
+ /** @var ?VTimeZone $firstTimezone */
+ $firstTimezone = array_reduce($calendars, function (?VTimeZone $acc, ICalendar $calendar) {
+ if ($acc !== null) {
+ return $acc;
+ }
+ if (!$calendar->isDeleted() && $calendar instanceof CalendarImpl) {
+ return $calendar->getSchedulingTimezone();
+ }
+ return null;
+ });
+ if ($firstTimezone !== null) {
+ return $firstTimezone->getTimeZone()->getName();
+ }
+ return null;
+ }
+
+ public function getDefaultTimezone(): string {
+ return $this->config->getSystemValueString('default_timezone', 'UTC');
+ }
+
+}
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 5730b7a1002..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 20d05c047b1..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;
@@ -31,23 +14,22 @@ use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\Exception\NotImplemented;
+use Sabre\DAVACL\ACLTrait;
+use Sabre\DAVACL\IACL;
use function array_map;
use function implode;
use function preg_match;
-class DeletedCalendarObjectsCollection implements ICalendarObjectContainer {
- public const NAME = 'objects';
-
- /** @var CalDavBackend */
- protected $caldavBackend;
+class DeletedCalendarObjectsCollection implements ICalendarObjectContainer, IACL {
+ use ACLTrait;
- /** @var mixed[] */
- private $principalInfo;
+ public const NAME = 'objects';
- public function __construct(CalDavBackend $caldavBackend,
- array $principalInfo) {
- $this->caldavBackend = $caldavBackend;
- $this->principalInfo = $principalInfo;
+ public function __construct(
+ protected CalDavBackend $caldavBackend,
+ /** @var mixed[] */
+ private array $principalInfo,
+ ) {
}
/**
@@ -64,7 +46,7 @@ class DeletedCalendarObjectsCollection implements ICalendarObjectContainer {
$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
@@ -129,4 +111,23 @@ class DeletedCalendarObjectsCollection implements ICalendarObjectContainer {
[$calendarInfo['id'], 'ics'],
);
}
+
+ public function getOwner() {
+ return $this->principalInfo['uri'];
+ }
+
+ public function getACL(): array {
+ return [
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}unbind',
+ 'principal' => '{DAV:}owner',
+ 'protected' => true,
+ ]
+ ];
+ }
}
diff --git a/apps/dav/lib/CalDAV/Trashbin/Plugin.php b/apps/dav/lib/CalDAV/Trashbin/Plugin.php
index 58ff76beca1..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 34d11e51eb3..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 3dd8a7c81e5..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);
}
/**
@@ -98,21 +89,20 @@ class Plugin extends ServerPlugin {
}
$path = $request->getPath();
+ if (!str_starts_with($path, 'calendars/')) {
+ return;
+ }
+
$pathParts = explode('/', ltrim($path, '/'));
if (\count($pathParts) < 2) {
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 543d15e0179..a0981e6dec1 100644
--- a/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php
+++ b/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php
@@ -3,94 +3,42 @@
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 OCP\ILogger;
-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;
-use Sabre\VObject\Recur\NoInstancesException;
use Sabre\VObject\ParseException;
use Sabre\VObject\Reader;
+use Sabre\VObject\Recur\NoInstancesException;
use Sabre\VObject\Splitter\ICalendar;
use Sabre\VObject\UUIDUtil;
use function count;
class RefreshWebcalService {
- /** @var CalDavBackend */
- private $calDavBackend;
-
- /** @var IClientService */
- private $clientService;
-
- /** @var IConfig */
- private $config;
-
- /** @var ILogger */
- private $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';
- /**
- * RefreshWebcalJob constructor.
- *
- * @param CalDavBackend $calDavBackend
- * @param IClientService $clientService
- * @param IConfig $config
- * @param ILogger $logger
- */
- public function __construct(CalDavBackend $calDavBackend, IClientService $clientService, IConfig $config, ILogger $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,
+ ) {
}
- /**
- * @param string $principalUri
- * @param string $uri
- */
public function refreshSubscription(string $principalUri, string $uri) {
$subscription = $this->getSubscription($principalUri, $uri);
$mutations = [];
@@ -98,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;
@@ -110,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') {
@@ -132,19 +90,66 @@ class RefreshWebcalService {
if ($stripAttachments) {
unset($component->{'ATTACH'});
}
+
+ $uid = $component->{ 'UID' }->getValue();
}
if ($stripTodos && $compName === 'VTODO') {
continue;
}
- $uri = $this->getRandomCalendarObjectUri();
- $calendarData = $vObject->serialize();
+ if (!isset($uid)) {
+ continue;
+ }
+
try {
- $this->calDavBackend->createCalendarObject($subscription['id'], $uri, $calendarData, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
- } catch (NoInstancesException | BadRequest $ex) {
- $this->logger->logException($ex);
+ $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);
@@ -154,21 +159,14 @@ class RefreshWebcalService {
$this->updateSubscription($subscription, $mutations);
} catch (ParseException $ex) {
- $subscriptionId = $subscription['id'];
-
- $this->logger->logException($ex);
- $this->logger->warning("Subscription $subscriptionId could not be refreshed due to a parsing error");
+ $this->logger->error('Subscription {subscriptionId} could not be refreshed due to a parsing error', ['exception' => $ex, 'subscriptionId' => $subscription['id']]);
}
}
/**
* loads subscription from backend
- *
- * @param string $principalUri
- * @param string $uri
- * @return array|null
*/
- public function getSubscription(string $principalUri, string $uri) {
+ public function getSubscription(string $principalUri, string $uri): ?array {
$subscriptions = array_values(array_filter(
$this->calDavBackend->getSubscriptionsForUser($principalUri),
function ($sub) use ($uri) {
@@ -183,117 +181,6 @@ class RefreshWebcalService {
return $subscriptions[0];
}
- /**
- * gets webcal feed from remote server
- *
- * @param array $subscription
- * @param array &$mutations
- * @return null|string
- */
- private function queryWebcalFeed(array $subscription, array &$mutations) {
- $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 Crawler');
- }));
- $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->debug("Subscription $subscriptionId could not be parsed");
- 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->debug("Subscription $subscriptionId could not be parsed");
- 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->debug("Subscription $subscriptionId could not be parsed");
- return null;
- }
- return $vCalendar->serialize();
- }
- } catch (LocalServerException $ex) {
- $this->logger->logException($ex, [
- 'message' => "Subscription $subscriptionId was not refreshed because it violates local access rules",
- 'level' => ILogger::WARN,
- ]);
-
- return null;
- } catch (Exception $ex) {
- $this->logger->logException($ex, [
- 'message' => "Subscription $subscriptionId could not be refreshed due to a network error",
- 'level' => ILogger::WARN,
- ]);
-
- return null;
- }
- }
/**
* check if:
@@ -301,11 +188,8 @@ class RefreshWebcalService {
* - the webcal feed suggests a refreshrate
* - return suggested refreshrate if user didn't set a custom one
*
- * @param array $subscription
- * @param string $webcalData
- * @return string|null
*/
- private function checkWebcalDataForRefreshRate($subscription, $webcalData) {
+ private function checkWebcalDataForRefreshRate(array $subscription, string $webcalData): ?string {
// if there is no refreshrate stored in the database, check the webcal feed
// whether it suggests any refresh rate and store that in the database
if (isset($subscription[self::REFRESH_RATE]) && $subscription[self::REFRESH_RATE] !== null) {
@@ -357,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) {
- $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 41d1b983587..f9bad25bf31 100644
--- a/apps/dav/lib/Capabilities.php
+++ b/apps/dav/lib/Capabilities.php
@@ -1,37 +1,39 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud GmbH
- *
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @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: 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 {
+ public function __construct(
+ private IConfig $config,
+ private IAvailabilityCoordinator $coordinator,
+ ) {
+ }
+
+ /**
+ * @return array{dav: array{chunking: string, public_shares_chunking: bool, bulkupload?: string, absence-supported?: bool, absence-replacement?: bool}}
+ */
public function getCapabilities() {
- return [
+ $capabilities = [
'dav' => [
'chunking' => '1.0',
- // disabled because of https://github.com/nextcloud/desktop/issues/4243
- // 'bulkupload' => '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 b713284e182..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;
@@ -32,32 +15,20 @@ use OCP\App\IAppManager;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IUser;
+use OCP\IUserManager;
use OCP\IUserSession;
use Sabre\CardDAV\Plugin;
use Sabre\VObject\Reader;
class Backend {
- /** @var IActivityManager */
- protected $activityManager;
-
- /** @var IGroupManager */
- protected $groupManager;
-
- /** @var IUserSession */
- protected $userSession;
-
- /** @var IAppManager */
- protected $appManager;
-
- public function __construct(IActivityManager $activityManager,
- IGroupManager $groupManager,
- IUserSession $userSession,
- IAppManager $appManager) {
- $this->activityManager = $activityManager;
- $this->groupManager = $groupManager;
- $this->userSession = $userSession;
- $this->appManager = $appManager;
+ public function __construct(
+ protected IActivityManager $activityManager,
+ protected IGroupManager $groupManager,
+ protected IUserSession $userSession,
+ protected IAppManager $appManager,
+ protected IUserManager $userManager,
+ ) {
}
/**
@@ -103,7 +74,14 @@ class Backend {
return;
}
- $principal = explode('/', $addressbookData['principaluri']);
+ $principalUri = $addressbookData['principaluri'];
+
+ // We are not interested in changes from the system addressbook
+ if ($principalUri === 'principals/system/system') {
+ return;
+ }
+
+ $principal = explode('/', $principalUri);
$owner = array_pop($principal);
$currentUser = $this->userSession->getUser();
@@ -115,8 +93,8 @@ class Backend {
$event = $this->activityManager->generateEvent();
$event->setApp('dav')
- ->setObject('addressbook', (int) $addressbookData['id'])
- ->setType('addressbook')
+ ->setObject('addressbook', (int)$addressbookData['id'])
+ ->setType('contacts')
->setAuthor($currentUser);
$changedVisibleInformation = array_intersect([
@@ -132,13 +110,18 @@ class Backend {
}
foreach ($users as $user) {
+ if ($action === Addressbook::SUBJECT_DELETE && !$this->userManager->userExists($user)) {
+ // Avoid creating addressbook_delete activities for deleted users
+ continue;
+ }
+
$event->setAffectedUser($user)
->setSubject(
$user === $currentUser ? $action . '_self' : $action,
[
'actor' => $currentUser,
'addressbook' => [
- 'id' => (int) $addressbookData['id'],
+ 'id' => (int)$addressbookData['id'],
'uri' => $addressbookData['uri'],
'name' => $addressbookData['{DAV:}displayname'],
],
@@ -169,8 +152,8 @@ class Backend {
$event = $this->activityManager->generateEvent();
$event->setApp('dav')
- ->setObject('addressbook', (int) $addressbookData['id'])
- ->setType('addressbook')
+ ->setObject('addressbook', (int)$addressbookData['id'])
+ ->setType('contacts')
->setAuthor($currentUser);
foreach ($remove as $principal) {
@@ -194,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'],
],
@@ -223,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'],
],
@@ -265,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'],
],
@@ -292,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'],
],
@@ -370,7 +353,7 @@ class Backend {
[
'actor' => $event->getAuthor(),
'addressbook' => [
- 'id' => (int) $properties['id'],
+ 'id' => (int)$properties['id'],
'uri' => $properties['uri'],
'name' => $properties['{DAV:}displayname'],
],
@@ -393,7 +376,14 @@ class Backend {
return;
}
- $principal = explode('/', $addressbookData['principaluri']);
+ $principalUri = $addressbookData['principaluri'];
+
+ // We are not interested in changes from the system addressbook
+ if ($principalUri === 'principals/system/system') {
+ return;
+ }
+
+ $principal = explode('/', $principalUri);
$owner = array_pop($principal);
$currentUser = $this->userSession->getUser();
@@ -407,8 +397,8 @@ class Backend {
$event = $this->activityManager->generateEvent();
$event->setApp('dav')
- ->setObject('addressbook', (int) $addressbookData['id'])
- ->setType('card')
+ ->setObject('addressbook', (int)$addressbookData['id'])
+ ->setType('contacts')
->setAuthor($currentUser);
$users = $this->getUsersForShares($shares);
@@ -419,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'],
],
@@ -446,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 be18fba96cc..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,
- 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;
}
/**
@@ -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() !== 'addressbook') {
- throw new \InvalidArgumentException();
+ public function parse($language, IEvent $event, ?IEvent $previousEvent = null): IEvent {
+ if ($event->getApp() !== 'dav' || $event->getType() !== 'contacts') {
+ 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 2f6de31de15..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;
@@ -32,48 +15,24 @@ use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IURLGenerator;
-use OCP\IUser;
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,
+ ) {
}
- /**
- * @param IEvent $event
- * @param string $subject
- * @param array $parameters
- */
protected function setSubjects(IEvent $event, string $subject, array $parameters): void {
- $placeholders = $replacements = [];
- foreach ($parameters as $placeholder => $parameter) {
- $placeholders[] = '{' . $placeholder . '}';
- $replacements[] = $parameter['name'];
- }
-
- $event->setParsedSubject(str_replace($placeholders, $replacements, $subject))
- ->setRichSubject($subject, $parameters);
+ $event->setRichSubject($subject, $parameters);
}
/**
@@ -82,51 +41,31 @@ 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'],
];
}
- /**
- * @param string $uid
- * @return array
- */
protected function generateUserParameter(string $uid): array {
- if (!isset($this->userDisplayNames[$uid])) {
- $this->userDisplayNames[$uid] = $this->getUserDisplayName($uid);
- }
-
return [
'type' => 'user',
'id' => $uid,
- 'name' => $this->userDisplayNames[$uid],
+ 'name' => $this->userManager->getDisplayName($uid) ?? $uid,
];
}
/**
- * @param string $uid
- * @return string
- */
- protected function getUserDisplayName(string $uid): string {
- $user = $this->userManager->get($uid);
- if ($user instanceof IUser) {
- return $user->getDisplayName();
- }
- return $uid;
- }
-
- /**
* @param string $gid
* @return array
*/
diff --git a/apps/dav/lib/CardDAV/Activity/Provider/Card.php b/apps/dav/lib/CardDAV/Activity/Provider/Card.php
index 9c909ae9bcd..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,
- 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;
}
/**
@@ -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() !== 'card') {
- throw new \InvalidArgumentException();
+ public function parse($language, IEvent $event, ?IEvent $previousEvent = null): IEvent {
+ if ($event->getApp() !== 'dav' || $event->getType() !== 'contacts') {
+ 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 9bd24bedbac..4d30d507a7d 100644
--- a/apps/dav/lib/CardDAV/AddressBook.php
+++ b/apps/dav/lib/CardDAV/AddressBook.php
@@ -1,47 +1,31 @@
<?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 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;
+use Psr\Log\LoggerInterface;
use Sabre\CardDAV\Backend\BackendInterface;
-use Sabre\CardDAV\Card;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\IMoveTarget;
+use Sabre\DAV\INode;
use Sabre\DAV\PropPatch;
/**
* Class AddressBook
*
* @package OCA\DAV\CardDAV
- * @property BackendInterface|CardDavBackend $carddavBackend
+ * @property CardDavBackend $carddavBackend
*/
-class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable {
-
+class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable, IMoveTarget {
/**
* AddressBook constructor.
*
@@ -52,8 +36,9 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable {
public function __construct(BackendInterface $carddavBackend, array $addressBookInfo, IL10N $l10n) {
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');
}
}
@@ -67,17 +52,15 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable {
* Every element in the add array has the following properties:
* * href - A url. Usually a mailto: address
* * commonName - Usually a first and last name, or false
- * * summary - A description of the share, can also be false
* * readOnly - A boolean value
*
* Every element in the remove array is just the address string.
*
- * @param array $add
- * @param array $remove
- * @return void
+ * @param list<array{href: string, commonName: string, readOnly: bool}> $add
+ * @param list<string> $remove
* @throws Forbidden
*/
- public function updateShares(array $add, array $remove) {
+ public function updateShares(array $add, array $remove): void {
if ($this->isShared()) {
throw new Forbidden();
}
@@ -92,11 +75,10 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable {
* * commonName - Optional, for example a first + last name
* * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
* * readOnly - boolean
- * * summary - Optional, a description for the share
*
- * @return array
+ * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}>
*/
- public function getShares() {
+ public function getShares(): array {
if ($this->isShared()) {
return [];
}
@@ -113,7 +95,12 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable {
'privilege' => '{DAV:}write',
'principal' => $this->getOwner(),
'protected' => true,
- ]
+ ],
+ [
+ 'privilege' => '{DAV:}write-properties',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ],
];
if ($this->getOwner() === 'principals/system/system') {
@@ -122,6 +109,11 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable {
'principal' => '{DAV:}authenticated',
'protected' => true,
];
+ $acl[] = [
+ 'privilege' => '{DAV:}write-properties',
+ 'principal' => '{DAV:}authenticated',
+ 'protected' => true,
+ ];
}
if (!$this->isShared()) {
@@ -144,7 +136,7 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable {
}
$acl = $this->carddavBackend->applyShareAcl($this->getResourceId(), $acl);
- $allowedPrincipals = [$this->getOwner(), parent::getOwner(), 'principals/system/system'];
+ $allowedPrincipals = [$this->getOwner(), parent::getOwner(), 'principals/system/system', '{DAV:}authenticated'];
return array_filter($acl, function ($rule) use ($allowedPrincipals) {
return \in_array($rule['principal'], $allowedPrincipals, true);
});
@@ -163,14 +155,33 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable {
return new Card($this->carddavBackend, $this->addressBookInfo, $obj);
}
- /**
- * @return int
- */
- public function getResourceId() {
+ public function getChildren() {
+ $objs = $this->carddavBackend->getCards($this->addressBookInfo['id']);
+ $children = [];
+ foreach ($objs as $obj) {
+ $obj['acl'] = $this->getChildACL();
+ $children[] = new Card($this->carddavBackend, $this->addressBookInfo, $obj);
+ }
+
+ return $children;
+ }
+
+ public function getMultipleChildren(array $paths) {
+ $objs = $this->carddavBackend->getMultipleCards($this->addressBookInfo['id'], $paths);
+ $children = [];
+ foreach ($objs as $obj) {
+ $obj['acl'] = $this->getChildACL();
+ $children[] = new Card($this->carddavBackend, $this->addressBookInfo, $obj);
+ }
+
+ return $children;
+ }
+
+ public function getResourceId(): int {
return $this->addressBookInfo['id'];
}
- public function getOwner() {
+ public function getOwner(): ?string {
if (isset($this->addressBookInfo['{http://owncloud.org/ns}owner-principal'])) {
return $this->addressBookInfo['{http://owncloud.org/ns}owner-principal'];
}
@@ -197,17 +208,16 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable {
}
public function propPatch(PropPatch $propPatch) {
- if (isset($this->addressBookInfo['{http://owncloud.org/ns}owner-principal'])) {
- throw new Forbidden();
+ if (!isset($this->addressBookInfo['{http://owncloud.org/ns}owner-principal'])) {
+ parent::propPatch($propPatch);
}
- parent::propPatch($propPatch);
}
public function getContactsGroups() {
return $this->carddavBackend->collectCardProperties($this->getResourceId(), 'CATEGORIES');
}
- private function isShared() {
+ private function isShared(): bool {
if (!isset($this->addressBookInfo['{http://owncloud.org/ns}owner-principal'])) {
return false;
}
@@ -215,7 +225,7 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable {
return $this->addressBookInfo['{http://owncloud.org/ns}owner-principal'] !== $this->addressBookInfo['principaluri'];
}
- private function canWrite() {
+ private function canWrite(): bool {
if (isset($this->addressBookInfo['{http://owncloud.org/ns}read-only'])) {
return !$this->addressBookInfo['{http://owncloud.org/ns}read-only'];
}
@@ -223,10 +233,29 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable {
}
public function getChanges($syncToken, $syncLevel, $limit = null) {
- if (!$syncToken && $limit) {
- throw new UnsupportedLimitOnInitialSyncException();
- }
return parent::getChanges($syncToken, $syncLevel, $limit);
}
+
+ /**
+ * @inheritDoc
+ */
+ public function moveInto($targetName, $sourcePath, INode $sourceNode) {
+ if (!($sourceNode instanceof Card)) {
+ return false;
+ }
+
+ try {
+ 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]);
+ return false;
+ }
+ }
}
diff --git a/apps/dav/lib/CardDAV/AddressBookImpl.php b/apps/dav/lib/CardDAV/AddressBookImpl.php
index 3db20cb4220..ae77498539b 100644
--- a/apps/dav/lib/CardDAV/AddressBookImpl.php
+++ b/apps/dav/lib/CardDAV/AddressBookImpl.php
@@ -1,57 +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 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.
@@ -62,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,
+ ) {
}
/**
@@ -102,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) {
@@ -155,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);
@@ -188,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;
@@ -206,7 +174,7 @@ class AddressBookImpl implements IAddressBook {
}
/**
- * @param object $id the unique identifier to a contact
+ * @param int $id the unique identifier to a contact
* @return bool successful or not
* @since 5.0.0
*/
@@ -272,7 +240,7 @@ class AddressBookImpl implements IAddressBook {
];
foreach ($vCard->children() as $property) {
- if ($property->name === 'PHOTO' && $property->getValueType() === 'BINARY') {
+ if ($property->name === 'PHOTO' && in_array($property->getValueType(), ['BINARY', 'URI'])) {
$url = $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->linkTo('', 'remote.php') . '/dav/');
$url .= implode('/', [
@@ -343,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 897ed819071..5679a03545e 100644
--- a/apps/dav/lib/CardDAV/AddressBookRoot.php
+++ b/apps/dav/lib/CardDAV/AddressBookRoot.php
@@ -1,46 +1,32 @@
<?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\CardDAV;
use OCA\DAV\AppInfo\PluginManager;
+use OCP\IGroupManager;
+use OCP\IUser;
class AddressBookRoot extends \Sabre\CardDAV\AddressBookRoot {
- /** @var PluginManager */
- private $pluginManager;
-
/**
* @param \Sabre\DAVACL\PrincipalBackend\BackendInterface $principalBackend
* @param \Sabre\CardDAV\Backend\BackendInterface $carddavBackend
* @param string $principalPrefix
*/
- public function __construct(\Sabre\DAVACL\PrincipalBackend\BackendInterface $principalBackend,
- \Sabre\CardDAV\Backend\BackendInterface $carddavBackend,
- PluginManager $pluginManager,
- $principalPrefix = 'principals') {
+ public function __construct(
+ \Sabre\DAVACL\PrincipalBackend\BackendInterface $principalBackend,
+ \Sabre\CardDAV\Backend\BackendInterface $carddavBackend,
+ private PluginManager $pluginManager,
+ private ?IUser $user,
+ private ?IGroupManager $groupManager,
+ string $principalPrefix = 'principals',
+ ) {
parent::__construct($principalBackend, $carddavBackend, $principalPrefix);
- $this->pluginManager = $pluginManager;
}
/**
@@ -55,7 +41,7 @@ class AddressBookRoot extends \Sabre\CardDAV\AddressBookRoot {
* @return \Sabre\DAV\INode
*/
public function getChildForPrincipal(array $principal) {
- return new UserAddressBooks($this->carddavBackend, $principal['uri'], $this->pluginManager);
+ return new UserAddressBooks($this->carddavBackend, $principal['uri'], $this->pluginManager, $this->user, $this->groupManager);
}
public function getName() {
diff --git a/apps/dav/lib/CardDAV/Card.php b/apps/dav/lib/CardDAV/Card.php
new file mode 100644
index 00000000000..8cd4fd7e5ee
--- /dev/null
+++ b/apps/dav/lib/CardDAV/Card.php
@@ -0,0 +1,42 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * 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'];
+ }
+
+ public function getUri(): string {
+ return $this->cardData['uri'];
+ }
+
+ protected function isShared(): bool {
+ if (!isset($this->cardData['{http://owncloud.org/ns}owner-principal'])) {
+ return false;
+ }
+
+ return $this->cardData['{http://owncloud.org/ns}owner-principal'] !== $this->cardData['principaluri'];
+ }
+
+ public function getAddressbookId(): int {
+ return (int)$this->cardData['addressbookid'];
+ }
+
+ public function getPrincipalUri(): string {
+ return $this->addressBookInfo['principaluri'];
+ }
+
+ public function getOwner(): ?string {
+ if (isset($this->addressBookInfo['{http://owncloud.org/ns}owner-principal'])) {
+ return $this->addressBookInfo['{http://owncloud.org/ns}owner-principal'];
+ }
+ return parent::getOwner();
+ }
+}
diff --git a/apps/dav/lib/CardDAV/CardDavBackend.php b/apps/dav/lib/CardDAV/CardDavBackend.php
index 1c1754ff752..a78686eb61d 100644
--- a/apps/dav/lib/CardDAV/CardDavBackend.php
+++ b/apps/dav/lib/CardDAV/CardDavBackend.php
@@ -1,40 +1,13 @@
<?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;
+use OC\Search\Filter\DateTimeFilter;
use OCA\DAV\Connector\Sabre\Principal;
use OCA\DAV\DAV\Sharing\Backend;
use OCA\DAV\DAV\Sharing\IShareable;
@@ -44,12 +17,14 @@ use OCA\DAV\Events\AddressBookShareUpdatedEvent;
use OCA\DAV\Events\AddressBookUpdatedEvent;
use OCA\DAV\Events\CardCreatedEvent;
use OCA\DAV\Events\CardDeletedEvent;
+use OCA\DAV\Events\CardMovedEvent;
use OCA\DAV\Events\CardUpdatedEvent;
+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\IGroupManager;
-use OCP\IUser;
use OCP\IUserManager;
use PDO;
use Sabre\CardDAV\Backend\BackendInterface;
@@ -58,30 +33,17 @@ use Sabre\CardDAV\Plugin;
use Sabre\DAV\Exception\BadRequest;
use Sabre\VObject\Component\VCard;
use Sabre\VObject\Reader;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
-use Symfony\Component\EventDispatcher\GenericEvent;
class CardDavBackend implements BackendInterface, SyncSupport {
+ use TTransactional;
public const PERSONAL_ADDRESSBOOK_URI = 'contacts';
public const PERSONAL_ADDRESSBOOK_NAME = 'Contacts';
- /** @var Principal */
- private $principalBackend;
-
- /** @var string */
- private $dbCardsTable = 'cards';
-
- /** @var string */
- private $dbCardsPropertiesTable = 'cards_properties';
-
- /** @var IDBConnection */
- private $db;
-
- /** @var Backend */
- private $sharingBackend;
+ private string $dbCardsTable = 'cards';
+ private string $dbCardsPropertiesTable = 'cards_properties';
/** @var array properties to index */
- public static $indexProperties = [
+ public static array $indexProperties = [
'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME',
'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO',
'CLOUD', 'X-SOCIALPROFILE'];
@@ -89,41 +51,17 @@ class CardDavBackend implements BackendInterface, SyncSupport {
/**
* @var string[] Map of uid => display name
*/
- protected $userDisplayNames;
-
- /** @var IUserManager */
- private $userManager;
-
- /** @var IEventDispatcher */
- private $dispatcher;
-
- /** @var EventDispatcherInterface */
- private $legacyDispatcher;
-
- private $etagCache = [];
-
- /**
- * CardDavBackend constructor.
- *
- * @param IDBConnection $db
- * @param Principal $principalBackend
- * @param IUserManager $userManager
- * @param IGroupManager $groupManager
- * @param IEventDispatcher $dispatcher
- * @param EventDispatcherInterface $legacyDispatcher
- */
- public function __construct(IDBConnection $db,
- Principal $principalBackend,
- IUserManager $userManager,
- IGroupManager $groupManager,
- IEventDispatcher $dispatcher,
- EventDispatcherInterface $legacyDispatcher) {
- $this->db = $db;
- $this->principalBackend = $principalBackend;
- $this->userManager = $userManager;
- $this->dispatcher = $dispatcher;
- $this->legacyDispatcher = $legacyDispatcher;
- $this->sharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'addressbook');
+ protected array $userDisplayNames;
+ private array $etagCache = [];
+
+ public function __construct(
+ private IDBConnection $db,
+ private Principal $principalBackend,
+ private IUserManager $userManager,
+ private IEventDispatcher $dispatcher,
+ private Sharing\Backend $sharingBackend,
+ private IConfig $config,
+ ) {
}
/**
@@ -140,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;
}
@@ -163,87 +101,95 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* @return array
*/
public function getAddressBooksForUser($principalUri) {
- $principalUriOriginal = $principalUri;
- $principalUri = $this->convertPrincipal($principalUri, true);
- $query = $this->db->getQueryBuilder();
- $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
- ->from('addressbooks')
- ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
+ return $this->atomic(function () use ($principalUri) {
+ $principalUriOriginal = $principalUri;
+ $principalUri = $this->convertPrincipal($principalUri, true);
+ $select = $this->db->getQueryBuilder();
+ $select->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
+ ->from('addressbooks')
+ ->where($select->expr()->eq('principaluri', $select->createNamedParameter($principalUri)));
- $addressBooks = [];
+ $addressBooks = [];
- $result = $query->execute();
- while ($row = $result->fetch()) {
- $addressBooks[$row['id']] = [
- 'id' => $row['id'],
- 'uri' => $row['uri'],
- 'principaluri' => $this->convertPrincipal($row['principaluri'], false),
- '{DAV:}displayname' => $row['displayname'],
- '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
- '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
- '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
- ];
+ $result = $select->executeQuery();
+ while ($row = $result->fetch()) {
+ $addressBooks[$row['id']] = [
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'principaluri' => $this->convertPrincipal($row['principaluri'], false),
+ '{DAV:}displayname' => $row['displayname'],
+ '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
+ '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
+ ];
+
+ $this->addOwnerPrincipal($addressBooks[$row['id']]);
+ }
+ $result->closeCursor();
- $this->addOwnerPrincipal($addressBooks[$row['id']]);
- }
- $result->closeCursor();
+ // query for shared addressbooks
+ $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
- // query for shared addressbooks
- $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
- $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
+ $principals[] = $principalUri;
- $principals[] = $principalUri;
+ $select = $this->db->getQueryBuilder();
+ $subSelect = $this->db->getQueryBuilder();
- $query = $this->db->getQueryBuilder();
- $result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access'])
- ->from('dav_shares', 's')
- ->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
- ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
- ->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
- ->setParameter('type', 'addressbook')
- ->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY)
- ->execute();
-
- $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
- while ($row = $result->fetch()) {
- if ($row['principaluri'] === $principalUri) {
- continue;
- }
+ $subSelect->select('id')
+ ->from('dav_shares', 'd')
+ ->where($subSelect->expr()->eq('d.access', $select->createNamedParameter(\OCA\DAV\CardDAV\Sharing\Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
+ ->andWhere($subSelect->expr()->in('d.principaluri', $select->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY));
- $readOnly = (int)$row['access'] === Backend::ACCESS_READ;
- if (isset($addressBooks[$row['id']])) {
- if ($readOnly) {
- // New share can not have more permissions then the old one.
- continue;
- }
- if (isset($addressBooks[$row['id']][$readOnlyPropertyName]) &&
- $addressBooks[$row['id']][$readOnlyPropertyName] === 0) {
- // Old share is already read-write, no more permissions can be gained
+
+ $select->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access'])
+ ->from('dav_shares', 's')
+ ->join('s', 'addressbooks', 'a', $select->expr()->eq('s.resourceid', 'a.id'))
+ ->where($select->expr()->in('s.principaluri', $select->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY)))
+ ->andWhere($select->expr()->eq('s.type', $select->createNamedParameter('addressbook', IQueryBuilder::PARAM_STR)))
+ ->andWhere($select->expr()->notIn('s.id', $select->createFunction($subSelect->getSQL()), IQueryBuilder::PARAM_INT_ARRAY));
+ $result = $select->executeQuery();
+
+ $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
+ while ($row = $result->fetch()) {
+ if ($row['principaluri'] === $principalUri) {
continue;
}
- }
- [, $name] = \Sabre\Uri\split($row['principaluri']);
- $uri = $row['uri'] . '_shared_by_' . $name;
- $displayName = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
-
- $addressBooks[$row['id']] = [
- 'id' => $row['id'],
- 'uri' => $uri,
- 'principaluri' => $principalUriOriginal,
- '{DAV:}displayname' => $displayName,
- '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
- '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
- '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
- '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
- $readOnlyPropertyName => $readOnly,
- ];
+ $readOnly = (int)$row['access'] === Backend::ACCESS_READ;
+ if (isset($addressBooks[$row['id']])) {
+ if ($readOnly) {
+ // New share can not have more permissions then the old one.
+ continue;
+ }
+ if (isset($addressBooks[$row['id']][$readOnlyPropertyName])
+ && $addressBooks[$row['id']][$readOnlyPropertyName] === 0) {
+ // Old share is already read-write, no more permissions can be gained
+ continue;
+ }
+ }
- $this->addOwnerPrincipal($addressBooks[$row['id']]);
- }
- $result->closeCursor();
+ [, $name] = \Sabre\Uri\split($row['principaluri']);
+ $uri = $row['uri'] . '_shared_by_' . $name;
+ $displayName = $row['displayname'] . ' (' . ($this->userManager->getDisplayName($name) ?? $name ?? '') . ')';
+
+ $addressBooks[$row['id']] = [
+ 'id' => $row['id'],
+ 'uri' => $uri,
+ 'principaluri' => $principalUriOriginal,
+ '{DAV:}displayname' => $displayName,
+ '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
+ '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
+ '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
+ $readOnlyPropertyName => $readOnly,
+ ];
+
+ $this->addOwnerPrincipal($addressBooks[$row['id']]);
+ }
+ $result->closeCursor();
- return array_values($addressBooks);
+ return array_values($addressBooks);
+ }, $this->db);
}
public function getUsersOwnAddressBooks($principalUri) {
@@ -255,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'],
@@ -274,33 +220,18 @@ class CardDavBackend implements BackendInterface, SyncSupport {
return array_values($addressBooks);
}
- private function getUserDisplayName($uid) {
- if (!isset($this->userDisplayNames[$uid])) {
- $user = $this->userManager->get($uid);
-
- if ($user instanceof IUser) {
- $this->userDisplayNames[$uid] = $user->getDisplayName();
- } else {
- $this->userDisplayNames[$uid] = $uid;
- }
- }
-
- return $this->userDisplayNames[$uid];
- }
-
/**
* @param int $addressBookId
*/
- public function getAddressBookById($addressBookId) {
+ public function getAddressBookById(int $addressBookId): ?array {
$query = $this->db->getQueryBuilder();
$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
->from('addressbooks')
- ->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
- ->execute();
-
+ ->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId, IQueryBuilder::PARAM_INT)))
+ ->executeQuery();
$row = $result->fetch();
$result->closeCursor();
- if ($row === false) {
+ if (!$row) {
return null;
}
@@ -319,18 +250,14 @@ class CardDavBackend implements BackendInterface, SyncSupport {
return $addressBook;
}
- /**
- * @param $addressBookUri
- * @return array|null
- */
- public function getAddressBooksByUri($principal, $addressBookUri) {
+ public function getAddressBooksByUri(string $principal, string $addressBookUri): ?array {
$query = $this->db->getQueryBuilder();
$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
->from('addressbooks')
->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri)))
->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
->setMaxResults(1)
- ->execute();
+ ->executeQuery();
$row = $result->fetch();
$result->closeCursor();
@@ -346,8 +273,15 @@ class CardDavBackend implements BackendInterface, SyncSupport {
'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
+
];
+ // system address books are always read only
+ if ($principal === 'principals/system/system') {
+ $addressBook['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal'] = $row['principaluri'];
+ $addressBook['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only'] = true;
+ }
+
$this->addOwnerPrincipal($addressBook);
return $addressBook;
@@ -387,19 +321,23 @@ class CardDavBackend implements BackendInterface, SyncSupport {
break;
}
}
- $query = $this->db->getQueryBuilder();
- $query->update('addressbooks');
+ [$addressBookRow, $shares] = $this->atomic(function () use ($addressBookId, $updates) {
+ $query = $this->db->getQueryBuilder();
+ $query->update('addressbooks');
- foreach ($updates as $key => $value) {
- $query->set($key, $query->createNamedParameter($value));
- }
- $query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
- ->execute();
+ foreach ($updates as $key => $value) {
+ $query->set($key, $query->createNamedParameter($value));
+ }
+ $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);
+ return [$addressBookRow, $shares];
+ }, $this->db);
- $addressBookRow = $this->getAddressBookById((int)$addressBookId);
- $shares = $this->getShares($addressBookId);
$this->dispatcher->dispatchTyped(new AddressBookUpdatedEvent((int)$addressBookId, $addressBookRow, $shares, $mutations));
return true;
@@ -414,8 +352,13 @@ 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) {
+ throw new BadRequest('URI too long. Address book not created');
+ }
+
$values = [
'displayname' => null,
'description' => null,
@@ -443,21 +386,27 @@ class CardDavBackend implements BackendInterface, SyncSupport {
$values['displayname'] = $url;
}
- $query = $this->db->getQueryBuilder();
- $query->insert('addressbooks')
- ->values([
- 'uri' => $query->createParameter('uri'),
- 'displayname' => $query->createParameter('displayname'),
- 'description' => $query->createParameter('description'),
- 'principaluri' => $query->createParameter('principaluri'),
- 'synctoken' => $query->createParameter('synctoken'),
- ])
- ->setParameters($values)
- ->execute();
-
- $addressBookId = $query->getLastInsertId();
- $addressBookRow = $this->getAddressBookById($addressBookId);
- $this->dispatcher->dispatchTyped(new AddressBookCreatedEvent((int)$addressBookId, $addressBookRow));
+ [$addressBookId, $addressBookRow] = $this->atomic(function () use ($values) {
+ $query = $this->db->getQueryBuilder();
+ $query->insert('addressbooks')
+ ->values([
+ 'uri' => $query->createParameter('uri'),
+ 'displayname' => $query->createParameter('displayname'),
+ 'description' => $query->createParameter('description'),
+ 'principaluri' => $query->createParameter('principaluri'),
+ 'synctoken' => $query->createParameter('synctoken'),
+ ])
+ ->setParameters($values)
+ ->executeStatement();
+
+ $addressBookId = $query->getLastInsertId();
+ return [
+ $addressBookId,
+ $this->getAddressBookById($addressBookId),
+ ];
+ }, $this->db);
+
+ $this->dispatcher->dispatchTyped(new AddressBookCreatedEvent($addressBookId, $addressBookRow));
return $addressBookId;
}
@@ -469,34 +418,40 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* @return void
*/
public function deleteAddressBook($addressBookId) {
- $addressBookData = $this->getAddressBookById($addressBookId);
- $shares = $this->getShares($addressBookId);
+ $this->atomic(function () use ($addressBookId): void {
+ $addressBookId = (int)$addressBookId;
+ $addressBookData = $this->getAddressBookById($addressBookId);
+ $shares = $this->getShares($addressBookId);
- $query = $this->db->getQueryBuilder();
- $query->delete($this->dbCardsTable)
- ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
- ->setParameter('addressbookid', $addressBookId)
- ->execute();
+ $query = $this->db->getQueryBuilder();
+ $query->delete($this->dbCardsTable)
+ ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
+ ->setParameter('addressbookid', $addressBookId, IQueryBuilder::PARAM_INT)
+ ->executeStatement();
- $query->delete('addressbookchanges')
- ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
- ->setParameter('addressbookid', $addressBookId)
- ->execute();
+ $query = $this->db->getQueryBuilder();
+ $query->delete('addressbookchanges')
+ ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
+ ->setParameter('addressbookid', $addressBookId, IQueryBuilder::PARAM_INT)
+ ->executeStatement();
- $query->delete('addressbooks')
- ->where($query->expr()->eq('id', $query->createParameter('id')))
- ->setParameter('id', $addressBookId)
- ->execute();
+ $query = $this->db->getQueryBuilder();
+ $query->delete('addressbooks')
+ ->where($query->expr()->eq('id', $query->createParameter('id')))
+ ->setParameter('id', $addressBookId, IQueryBuilder::PARAM_INT)
+ ->executeStatement();
- $this->sharingBackend->deleteAllShares($addressBookId);
+ $this->sharingBackend->deleteAllShares($addressBookId);
- $query->delete($this->dbCardsPropertiesTable)
- ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
- ->execute();
+ $query = $this->db->getQueryBuilder();
+ $query->delete($this->dbCardsPropertiesTable)
+ ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId, IQueryBuilder::PARAM_INT)))
+ ->executeStatement();
- if ($addressBookData) {
- $this->dispatcher->dispatchTyped(new AddressBookDeletedEvent((int) $addressBookId, $addressBookData, $shares));
- }
+ if ($addressBookData) {
+ $this->dispatcher->dispatchTyped(new AddressBookDeletedEvent($addressBookId, $addressBookData, $shares));
+ }
+ }, $this->db);
}
/**
@@ -512,21 +467,21 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* * size - The size of the card in bytes.
*
* If these last two properties are provided, less time will be spent
- * calculating them. If they are specified, you can also ommit carddata.
+ * calculating them. If they are specified, you can also omit carddata.
* This may speed up certain requests, especially with large cards.
*
- * @param mixed $addressBookId
+ * @param mixed $addressbookId
* @return array
*/
- public function getCards($addressBookId) {
+ 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)));
+ ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressbookId)));
$cards = [];
- $result = $query->execute();
+ $result = $query->executeQuery();
while ($row = $result->fetch()) {
$row['etag'] = '"' . $row['etag'] . '"';
@@ -557,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;
@@ -588,7 +543,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* If the backend supports this, it may allow for some speed-ups.
*
* @param mixed $addressBookId
- * @param string[] $uris
+ * @param array $uris
* @return array
*/
public function getMultipleCards($addressBookId, array $uris) {
@@ -600,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'] . '"';
@@ -648,55 +603,54 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* @param mixed $addressBookId
* @param string $cardUri
* @param string $cardData
+ * @param bool $checkAlreadyExists
* @return string
*/
- public function createCard($addressBookId, $cardUri, $cardData) {
+ public function createCard($addressBookId, $cardUri, $cardData, bool $checkAlreadyExists = true) {
$etag = md5($cardData);
$uid = $this->getUID($cardData);
+ return $this->atomic(function () use ($addressBookId, $cardUri, $cardData, $checkAlreadyExists, $etag, $uid) {
+ if ($checkAlreadyExists) {
+ $q = $this->db->getQueryBuilder();
+ $q->select('uid')
+ ->from($this->dbCardsTable)
+ ->where($q->expr()->eq('addressbookid', $q->createNamedParameter($addressBookId)))
+ ->andWhere($q->expr()->eq('uid', $q->createNamedParameter($uid)))
+ ->setMaxResults(1);
+ $result = $q->executeQuery();
+ $count = (bool)$result->fetchOne();
+ $result->closeCursor();
+ if ($count) {
+ throw new \Sabre\DAV\Exception\BadRequest('VCard object with uid already exists in this addressbook collection.');
+ }
+ }
- $q = $this->db->getQueryBuilder();
- $q->select('uid')
- ->from($this->dbCardsTable)
- ->where($q->expr()->eq('addressbookid', $q->createNamedParameter($addressBookId)))
- ->andWhere($q->expr()->eq('uid', $q->createNamedParameter($uid)))
- ->setMaxResults(1);
- $result = $q->execute();
- $count = (bool)$result->fetchOne();
- $result->closeCursor();
- if ($count) {
- throw new \Sabre\DAV\Exception\BadRequest('VCard object with uid already exists in this addressbook collection.');
- }
+ $query = $this->db->getQueryBuilder();
+ $query->insert('cards')
+ ->values([
+ 'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB),
+ 'uri' => $query->createNamedParameter($cardUri),
+ 'lastmodified' => $query->createNamedParameter(time()),
+ 'addressbookid' => $query->createNamedParameter($addressBookId),
+ 'size' => $query->createNamedParameter(strlen($cardData)),
+ 'etag' => $query->createNamedParameter($etag),
+ 'uid' => $query->createNamedParameter($uid),
+ ])
+ ->executeStatement();
- $query = $this->db->getQueryBuilder();
- $query->insert('cards')
- ->values([
- 'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB),
- 'uri' => $query->createNamedParameter($cardUri),
- 'lastmodified' => $query->createNamedParameter(time()),
- 'addressbookid' => $query->createNamedParameter($addressBookId),
- 'size' => $query->createNamedParameter(strlen($cardData)),
- 'etag' => $query->createNamedParameter($etag),
- 'uid' => $query->createNamedParameter($uid),
- ])
- ->execute();
-
- $etagCacheKey = "$addressBookId#$cardUri";
- $this->etagCache[$etagCacheKey] = $etag;
-
- $this->addChange($addressBookId, $cardUri, 1);
- $this->updateProperties($addressBookId, $cardUri, $cardData);
-
- $addressBookData = $this->getAddressBookById($addressBookId);
- $shares = $this->getShares($addressBookId);
- $objectRow = $this->getCard($addressBookId, $cardUri);
- $this->dispatcher->dispatchTyped(new CardCreatedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
- $this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard',
- new GenericEvent(null, [
- 'addressBookId' => $addressBookId,
- 'cardUri' => $cardUri,
- 'cardData' => $cardData]));
-
- return '"' . $etag . '"';
+ $etagCacheKey = "$addressBookId#$cardUri";
+ $this->etagCache[$etagCacheKey] = $etag;
+
+ $this->addChange($addressBookId, $cardUri, 1);
+ $this->updateProperties($addressBookId, $cardUri, $cardData);
+
+ $addressBookData = $this->getAddressBookById($addressBookId);
+ $shares = $this->getShares($addressBookId);
+ $objectRow = $this->getCard($addressBookId, $cardUri);
+ $this->dispatcher->dispatchTyped(new CardCreatedEvent($addressBookId, $addressBookData, $shares, $objectRow));
+
+ return '"' . $etag . '"';
+ }, $this->db);
}
/**
@@ -727,40 +681,81 @@ class CardDavBackend implements BackendInterface, SyncSupport {
public function updateCard($addressBookId, $cardUri, $cardData) {
$uid = $this->getUID($cardData);
$etag = md5($cardData);
- $query = $this->db->getQueryBuilder();
- // check for recently stored etag and stop if it is the same
- $etagCacheKey = "$addressBookId#$cardUri";
- if (isset($this->etagCache[$etagCacheKey]) && $this->etagCache[$etagCacheKey] === $etag) {
+ return $this->atomic(function () use ($addressBookId, $cardUri, $cardData, $uid, $etag) {
+ $query = $this->db->getQueryBuilder();
+
+ // check for recently stored etag and stop if it is the same
+ $etagCacheKey = "$addressBookId#$cardUri";
+ if (isset($this->etagCache[$etagCacheKey]) && $this->etagCache[$etagCacheKey] === $etag) {
+ return '"' . $etag . '"';
+ }
+
+ $query->update($this->dbCardsTable)
+ ->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB))
+ ->set('lastmodified', $query->createNamedParameter(time()))
+ ->set('size', $query->createNamedParameter(strlen($cardData)))
+ ->set('etag', $query->createNamedParameter($etag))
+ ->set('uid', $query->createNamedParameter($uid))
+ ->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
+ ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
+ ->executeStatement();
+
+ $this->etagCache[$etagCacheKey] = $etag;
+
+ $this->addChange($addressBookId, $cardUri, 2);
+ $this->updateProperties($addressBookId, $cardUri, $cardData);
+
+ $addressBookData = $this->getAddressBookById($addressBookId);
+ $shares = $this->getShares($addressBookId);
+ $objectRow = $this->getCard($addressBookId, $cardUri);
+ $this->dispatcher->dispatchTyped(new CardUpdatedEvent($addressBookId, $addressBookData, $shares, $objectRow));
return '"' . $etag . '"';
- }
+ }, $this->db);
+ }
- $query->update($this->dbCardsTable)
- ->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB))
- ->set('lastmodified', $query->createNamedParameter(time()))
- ->set('size', $query->createNamedParameter(strlen($cardData)))
- ->set('etag', $query->createNamedParameter($etag))
- ->set('uid', $query->createNamedParameter($uid))
- ->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
- ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
- ->execute();
-
- $this->etagCache[$etagCacheKey] = $etag;
-
- $this->addChange($addressBookId, $cardUri, 2);
- $this->updateProperties($addressBookId, $cardUri, $cardData);
-
- $addressBookData = $this->getAddressBookById($addressBookId);
- $shares = $this->getShares($addressBookId);
- $objectRow = $this->getCard($addressBookId, $cardUri);
- $this->dispatcher->dispatchTyped(new CardUpdatedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
- $this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard',
- new GenericEvent(null, [
- 'addressBookId' => $addressBookId,
- 'cardUri' => $cardUri,
- 'cardData' => $cardData]));
-
- return '"' . $etag . '"';
+ /**
+ * @throws Exception
+ */
+ 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))
+ ->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, $sourceObjectId);
+ $this->updateProperties($targetAddressBookId, $tragetObjectUri, $card['carddata']);
+
+ $this->addChange($sourceAddressBookId, $sourceObjectUri, 3);
+ $this->addChange($targetAddressBookId, $tragetObjectUri, 1);
+
+ $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)) {
+ return false;
+ }
+
+ $sourceShares = $this->getShares($sourceAddressBookId);
+ $targetShares = $this->getShares($targetAddressBookId);
+ $sourceAddressBookRow = $this->getAddressBookById($sourceAddressBookId);
+ $this->dispatcher->dispatchTyped(new CardMovedEvent($sourceAddressBookId, $sourceAddressBookRow, $targetAddressBookId, $targetAddressBookRow, $sourceShares, $targetShares, $card));
+ return true;
+ }, $this->db);
}
/**
@@ -771,37 +766,34 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* @return bool
*/
public function deleteCard($addressBookId, $cardUri) {
- $addressBookData = $this->getAddressBookById($addressBookId);
- $shares = $this->getShares($addressBookId);
- $objectRow = $this->getCard($addressBookId, $cardUri);
-
- try {
- $cardId = $this->getCardId($addressBookId, $cardUri);
- } catch (\InvalidArgumentException $e) {
- $cardId = null;
- }
- $query = $this->db->getQueryBuilder();
- $ret = $query->delete($this->dbCardsTable)
- ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
- ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
- ->execute();
+ return $this->atomic(function () use ($addressBookId, $cardUri) {
+ $addressBookData = $this->getAddressBookById($addressBookId);
+ $shares = $this->getShares($addressBookId);
+ $objectRow = $this->getCard($addressBookId, $cardUri);
- $this->addChange($addressBookId, $cardUri, 3);
+ try {
+ $cardId = $this->getCardId($addressBookId, $cardUri);
+ } catch (\InvalidArgumentException $e) {
+ $cardId = null;
+ }
+ $query = $this->db->getQueryBuilder();
+ $ret = $query->delete($this->dbCardsTable)
+ ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
+ ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
+ ->executeStatement();
- if ($ret === 1) {
- if ($cardId !== null) {
- $this->dispatcher->dispatchTyped(new CardDeletedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
- $this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard',
- new GenericEvent(null, [
- 'addressBookId' => $addressBookId,
- 'cardUri' => $cardUri]));
+ $this->addChange($addressBookId, $cardUri, 3);
- $this->purgeProperties($addressBookId, $cardId);
+ if ($ret === 1) {
+ if ($cardId !== null) {
+ $this->dispatcher->dispatchTyped(new CardDeletedEvent($addressBookId, $addressBookData, $shares, $objectRow));
+ $this->purgeProperties($addressBookId, $cardId);
+ }
+ return true;
}
- return true;
- }
- return false;
+ return false;
+ }, $this->db);
}
/**
@@ -861,82 +853,147 @@ 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
- $qb = $this->db->getQueryBuilder();
- $qb->select('synctoken')
- ->from('addressbooks')
- ->where(
- $qb->expr()->eq('id', $qb->createNamedParameter($addressBookId))
- );
- $stmt = $qb->execute();
- $currentToken = $stmt->fetchOne();
- $stmt->closeCursor();
-
- if (is_null($currentToken)) {
- return null;
- }
-
- $result = [
- 'syncToken' => $currentToken,
- 'added' => [],
- 'modified' => [],
- 'deleted' => [],
- ];
-
- if ($syncToken) {
+ return $this->atomic(function () use ($addressBookId, $syncToken, $syncLevel, $limit) {
$qb = $this->db->getQueryBuilder();
- $qb->select('uri', 'operation')
- ->from('addressbookchanges')
+ $qb->select('synctoken')
+ ->from('addressbooks')
->where(
- $qb->expr()->andX(
- $qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)),
- $qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)),
- $qb->expr()->eq('addressbookid', $qb->createNamedParameter($addressBookId))
- )
- )->orderBy('synctoken');
+ $qb->expr()->eq('id', $qb->createNamedParameter($addressBookId))
+ );
+ $stmt = $qb->executeQuery();
+ $currentToken = $stmt->fetchOne();
+ $stmt->closeCursor();
- if (is_int($limit) && $limit > 0) {
- $qb->setMaxResults($limit);
+ if (is_null($currentToken)) {
+ return [];
}
- // Fetching all changes
- $stmt = $qb->execute();
+ $result = [
+ 'syncToken' => $currentToken,
+ 'added' => [],
+ 'modified' => [],
+ 'deleted' => [],
+ ];
+ 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', 'synctoken')
+ ->from('addressbookchanges')
+ ->where(
+ $qb->expr()->andX(
+ $qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)),
+ $qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)),
+ $qb->expr()->eq('addressbookid', $qb->createNamedParameter($addressBookId))
+ )
+ )->orderBy('synctoken');
+
+ if ($limit > 0) {
+ $qb->setMaxResults($limit);
+ }
- $changes = [];
+ // Fetching all changes
+ $stmt = $qb->executeQuery();
+ $rowCount = $stmt->rowCount();
- // 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'];
- }
- $stmt->closeCursor();
+ $changes = [];
+ $highestSyncToken = 0;
- 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;
+ // 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:
+ $result['added'][] = $uri;
+ break;
+ case 2:
+ $result['modified'][] = $uri;
+ break;
+ case 3:
+ $result['deleted'][] = $uri;
+ 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('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();
+ $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();
}
- } else {
- $qb = $this->db->getQueryBuilder();
- $qb->select('uri')
- ->from('cards')
- ->where(
- $qb->expr()->eq('addressbookid', $qb->createNamedParameter($addressBookId))
- );
- // No synctoken supplied, this is the initial sync.
- $stmt = $qb->execute();
- $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
- $stmt->closeCursor();
- }
- return $result;
+ return $result;
+ }, $this->db);
}
/**
@@ -947,19 +1004,33 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* @param int $operation 1 = add, 2 = modify, 3 = delete
* @return void
*/
- protected function addChange($addressBookId, $objectUri, $operation) {
- $sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?';
- $stmt = $this->db->prepare($sql);
- $stmt->execute([
- $objectUri,
- $addressBookId,
- $operation,
- $addressBookId
- ]);
- $stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
- $stmt->execute([
- $addressBookId
- ]);
+ protected function addChange(int $addressBookId, string $objectUri, int $operation): void {
+ $this->atomic(function () use ($addressBookId, $objectUri, $operation): void {
+ $query = $this->db->getQueryBuilder();
+ $query->select('synctoken')
+ ->from('addressbooks')
+ ->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)));
+ $result = $query->executeQuery();
+ $syncToken = (int)$result->fetchOne();
+ $result->closeCursor();
+
+ $query = $this->db->getQueryBuilder();
+ $query->insert('addressbookchanges')
+ ->values([
+ 'uri' => $query->createNamedParameter($objectUri),
+ 'synctoken' => $query->createNamedParameter($syncToken),
+ 'addressbookid' => $query->createNamedParameter($addressBookId),
+ 'operation' => $query->createNamedParameter($operation),
+ 'created_at' => time(),
+ ])
+ ->executeStatement();
+
+ $query = $this->db->getQueryBuilder();
+ $query->update('addressbooks')
+ ->set('synctoken', $query->createNamedParameter($syncToken + 1, IQueryBuilder::PARAM_INT))
+ ->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
+ ->executeStatement();
+ }, $this->db);
}
/**
@@ -972,13 +1043,19 @@ class CardDavBackend implements BackendInterface, SyncSupport {
$cardData = stream_get_contents($cardData);
}
+ // Micro optimisation
+ // don't loop through
+ if (str_starts_with($cardData, 'PHOTO:data:')) {
+ return $cardData;
+ }
+
$cardDataArray = explode("\r\n", $cardData);
$cardDataFiltered = [];
$removingPhoto = false;
foreach ($cardDataArray as $line) {
- if (strpos($line, 'PHOTO:data:') === 0
- && strpos($line, 'PHOTO:data:image/') !== 0) {
+ if (str_starts_with($line, 'PHOTO:data:')
+ && !str_starts_with($line, 'PHOTO:data:image/')) {
// Filter out PHOTO data of non-images
$removingPhoto = true;
$modified = true;
@@ -986,7 +1063,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
}
if ($removingPhoto) {
- if (strpos($line, ' ') === 0) {
+ if (str_starts_with($line, ' ')) {
continue;
}
// No leading space means this is a new property
@@ -995,23 +1072,23 @@ class CardDavBackend implements BackendInterface, SyncSupport {
$cardDataFiltered[] = $line;
}
-
return implode("\r\n", $cardDataFiltered);
}
/**
- * @param IShareable $shareable
- * @param string[] $add
- * @param string[] $remove
+ * @param list<array{href: string, commonName: string, readOnly: bool}> $add
+ * @param list<string> $remove
*/
- public function updateShares(IShareable $shareable, $add, $remove) {
- $addressBookId = $shareable->getResourceId();
- $addressBookData = $this->getAddressBookById($addressBookId);
- $oldShares = $this->getShares($addressBookId);
+ public function updateShares(IShareable $shareable, array $add, array $remove): void {
+ $this->atomic(function () use ($shareable, $add, $remove): void {
+ $addressBookId = $shareable->getResourceId();
+ $addressBookData = $this->getAddressBookById($addressBookId);
+ $oldShares = $this->getShares($addressBookId);
- $this->sharingBackend->updateShares($shareable, $add, $remove);
+ $this->sharingBackend->updateShares($shareable, $add, $remove, $oldShares);
- $this->dispatcher->dispatchTyped(new AddressBookShareUpdatedEvent($addressBookId, $addressBookData, $oldShares, $add, $remove));
+ $this->dispatcher->dispatchTyped(new AddressBookShareUpdatedEvent($addressBookId, $addressBookData, $oldShares, $add, $remove));
+ }, $this->db);
}
/**
@@ -1021,15 +1098,18 @@ 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
- * - '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{escape_like_param?: bool, limit?: int, offset?: int, wildcard?: bool} $options
+ * - '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
*/
public function search($addressBookId, $pattern, $searchProperties, $options = []): array {
- return $this->searchByAddressBookIds([$addressBookId], $pattern, $searchProperties, $options);
+ return $this->atomic(function () use ($addressBookId, $pattern, $searchProperties, $options) {
+ return $this->searchByAddressBookIds([$addressBookId], $pattern, $searchProperties, $options);
+ }, $this->db);
}
/**
@@ -1042,71 +1122,74 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* @return array
*/
public function searchPrincipalUri(string $principalUri,
- string $pattern,
- array $searchProperties,
- array $options = []): array {
- $addressBookIds = array_map(static function ($row):int {
- return (int) $row['id'];
- }, $this->getAddressBooksForUser($principalUri));
-
- return $this->searchByAddressBookIds($addressBookIds, $pattern, $searchProperties, $options);
+ string $pattern,
+ array $searchProperties,
+ array $options = []): array {
+ return $this->atomic(function () use ($principalUri, $pattern, $searchProperties, $options) {
+ $addressBookIds = array_map(static function ($row):int {
+ return (int)$row['id'];
+ }, $this->getAddressBooksForUser($principalUri));
+
+ return $this->searchByAddressBookIds($addressBookIds, $pattern, $searchProperties, $options);
+ }, $this->db);
}
/**
- * @param array $addressBookIds
+ * @param int[] $addressBookIds
* @param string $pattern
* @param array $searchProperties
* @param array $options
- * @psalm-param array{types?: bool, escape_like_param?: bool, limit?: int, offset?: int, wildcard?: bool} $options
+ * @psalm-param array{
+ * types?: bool,
+ * escape_like_param?: bool,
+ * limit?: int,
+ * offset?: int,
+ * wildcard?: bool,
+ * since?: DateTimeFilter|null,
+ * until?: DateTimeFilter|null,
+ * person?: string
+ * } $options
* @return array
*/
private function searchByAddressBookIds(array $addressBookIds,
- string $pattern,
- array $searchProperties,
- array $options = []): array {
- $escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
- $useWildcards = !\array_key_exists('wildcard', $options) || $options['wildcard'] !== false;
-
- $query2 = $this->db->getQueryBuilder();
-
- $addressBookOr = $query2->expr()->orX();
- foreach ($addressBookIds as $addressBookId) {
- $addressBookOr->add($query2->expr()->eq('cp.addressbookid', $query2->createNamedParameter($addressBookId)));
- }
-
- if ($addressBookOr->count() === 0) {
+ string $pattern,
+ array $searchProperties,
+ array $options = []): array {
+ if (empty($addressBookIds)) {
return [];
}
+ $escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
+ $useWildcards = !\array_key_exists('wildcard', $options) || $options['wildcard'] !== false;
- $propertyOr = $query2->expr()->orX();
- foreach ($searchProperties as $property) {
- if ($escapePattern) {
- if ($property === 'EMAIL' && strpos($pattern, ' ') !== false) {
+ if ($escapePattern) {
+ $searchProperties = array_filter($searchProperties, function ($property) use ($pattern) {
+ if ($property === 'EMAIL' && str_contains($pattern, ' ')) {
// There can be no spaces in emails
- continue;
+ return false;
}
if ($property === 'CLOUD' && preg_match('/[^a-zA-Z0-9 :_.@\/\-\']/', $pattern) === 1) {
// There can be no chars in cloud ids which are not valid for user ids plus :/
// worst case: CA61590A-BBBC-423E-84AF-E6DF01455A53@https://my.nxt/srv/
- continue;
+ return false;
}
- }
- $propertyOr->add($query2->expr()->eq('cp.name', $query2->createNamedParameter($property)));
+ return true;
+ });
}
- if ($propertyOr->count() === 0) {
+ if (empty($searchProperties)) {
return [];
}
+ $query2 = $this->db->getQueryBuilder();
$query2->selectDistinct('cp.cardid')
->from($this->dbCardsPropertiesTable, 'cp')
- ->andWhere($addressBookOr)
- ->andWhere($propertyOr);
+ ->where($query2->expr()->in('cp.addressbookid', $query2->createNamedParameter($addressBookIds, IQueryBuilder::PARAM_INT_ARRAY), IQueryBuilder::PARAM_INT_ARRAY))
+ ->andWhere($query2->expr()->in('cp.name', $query2->createNamedParameter($searchProperties, IQueryBuilder::PARAM_STR_ARRAY)));
// No need for like when the pattern is empty
- if ('' !== $pattern) {
+ if ($pattern !== '') {
if (!$useWildcards) {
$query2->andWhere($query2->expr()->eq('cp.value', $query2->createNamedParameter($pattern)));
} elseif (!$escapePattern) {
@@ -1115,7 +1198,6 @@ class CardDavBackend implements BackendInterface, SyncSupport {
$query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
}
}
-
if (isset($options['limit'])) {
$query2->setMaxResults($options['limit']);
}
@@ -1123,7 +1205,33 @@ class CardDavBackend implements BackendInterface, SyncSupport {
$query2->setFirstResult($options['offset']);
}
- $result = $query2->execute();
+ if (isset($options['person'])) {
+ $query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter('%' . $this->db->escapeLikeParameter($options['person']) . '%')));
+ }
+ if (isset($options['since']) || isset($options['until'])) {
+ $query2->join('cp', $this->dbCardsPropertiesTable, 'cp_bday', 'cp.cardid = cp_bday.cardid');
+ $query2->andWhere($query2->expr()->eq('cp_bday.name', $query2->createNamedParameter('BDAY')));
+ /**
+ * FIXME Find a way to match only 4 last digits
+ * BDAY can be --1018 without year or 20001019 with it
+ * $bDayOr = [];
+ * if ($options['since'] instanceof DateTimeFilter) {
+ * $bDayOr[] =
+ * $query2->expr()->gte('SUBSTR(cp_bday.value, -4)',
+ * $query2->createNamedParameter($options['since']->get()->format('md'))
+ * );
+ * }
+ * if ($options['until'] instanceof DateTimeFilter) {
+ * $bDayOr[] =
+ * $query2->expr()->lte('SUBSTR(cp_bday.value, -4)',
+ * $query2->createNamedParameter($options['until']->get()->format('md'))
+ * );
+ * }
+ * $query2->andWhere($query2->expr()->orX(...$bDayOr));
+ */
+ }
+
+ $result = $query2->executeQuery();
$matches = $result->fetchAll();
$result->closeCursor();
$matches = array_map(function ($match) {
@@ -1144,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) {
@@ -1165,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();
@@ -1185,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();
@@ -1209,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();
@@ -1235,11 +1343,10 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* * commonName - Optional, for example a first + last name
* * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
* * readOnly - boolean
- * * summary - Optional, a description for the share
*
- * @return array
+ * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}>
*/
- public function getShares($addressBookId) {
+ public function getShares(int $addressBookId): array {
return $this->sharingBackend->getShares($addressBookId);
}
@@ -1251,39 +1358,41 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* @param string $vCardSerialized
*/
protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) {
- $cardId = $this->getCardId($addressBookId, $cardUri);
- $vCard = $this->readCard($vCardSerialized);
+ $this->atomic(function () use ($addressBookId, $cardUri, $vCardSerialized): void {
+ $cardId = $this->getCardId($addressBookId, $cardUri);
+ $vCard = $this->readCard($vCardSerialized);
- $this->purgeProperties($addressBookId, $cardId);
+ $this->purgeProperties($addressBookId, $cardId);
- $query = $this->db->getQueryBuilder();
- $query->insert($this->dbCardsPropertiesTable)
- ->values(
- [
- 'addressbookid' => $query->createNamedParameter($addressBookId),
- 'cardid' => $query->createNamedParameter($cardId),
- 'name' => $query->createParameter('name'),
- 'value' => $query->createParameter('value'),
- 'preferred' => $query->createParameter('preferred')
- ]
- );
+ $query = $this->db->getQueryBuilder();
+ $query->insert($this->dbCardsPropertiesTable)
+ ->values(
+ [
+ 'addressbookid' => $query->createNamedParameter($addressBookId),
+ 'cardid' => $query->createNamedParameter($cardId),
+ 'name' => $query->createParameter('name'),
+ 'value' => $query->createParameter('value'),
+ 'preferred' => $query->createParameter('preferred')
+ ]
+ );
- foreach ($vCard->children() as $property) {
- if (!in_array($property->name, self::$indexProperties)) {
- continue;
- }
- $preferred = 0;
- foreach ($property->parameters as $parameter) {
- if ($parameter->name === 'TYPE' && strtoupper($parameter->getValue()) === 'PREF') {
- $preferred = 1;
- break;
+ foreach ($vCard->children() as $property) {
+ if (!in_array($property->name, self::$indexProperties)) {
+ continue;
+ }
+ $preferred = 0;
+ foreach ($property->parameters as $parameter) {
+ if ($parameter->name === 'TYPE' && strtoupper($parameter->getValue()) === 'PREF') {
+ $preferred = 1;
+ break;
+ }
}
+ $query->setParameter('name', $property->name);
+ $query->setParameter('value', mb_strcut($property->getValue(), 0, 254));
+ $query->setParameter('preferred', $preferred);
+ $query->executeStatement();
}
- $query->setParameter('name', $property->name);
- $query->setParameter('value', mb_strcut($property->getValue(), 0, 254));
- $query->setParameter('preferred', $preferred);
- $query->execute();
- }
+ }, $this->db);
}
/**
@@ -1307,23 +1416,19 @@ 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();
}
/**
- * get ID from a given contact
- *
- * @param int $addressBookId
- * @param string $uri
- * @return int
+ * Get ID from a given contact
*/
- protected function getCardId($addressBookId, $uri) {
+ protected function getCardId(int $addressBookId, string $uri): int {
$query = $this->db->getQueryBuilder();
$query->select('id')->from($this->dbCardsTable)
->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();
@@ -1337,15 +1442,44 @@ class CardDavBackend implements BackendInterface, SyncSupport {
/**
* For shared address books the sharee is set in the ACL of the address book
*
- * @param $addressBookId
- * @param $acl
- * @return array
+ * @param int $addressBookId
+ * @param list<array{privilege: string, principal: string, protected: bool}> $acl
+ * @return list<array{privilege: string, principal: string, protected: bool}>
*/
- public function applyShareAcl($addressBookId, $acl) {
- return $this->sharingBackend->applyShareAcl($addressBookId, $acl);
+ public function applyShareAcl(int $addressBookId, array $acl): array {
+ $shares = $this->sharingBackend->getShares($addressBookId);
+ return $this->sharingBackend->applyShareAcl($shares, $acl);
+ }
+
+ /**
+ * @throws \InvalidArgumentException
+ */
+ public function pruneOutdatedSyncTokens(int $keep, int $retention): int {
+ if ($keep < 0) {
+ throw new \InvalidArgumentException();
+ }
+
+ $query = $this->db->getQueryBuilder();
+ $query->select($query->func()->max('id'))
+ ->from('addressbookchanges');
+
+ $result = $query->executeQuery();
+ $maxId = (int)$result->fetchOne();
+ $result->closeCursor();
+ if (!$maxId || $maxId < $keep) {
+ return 0;
+ }
+
+ $query = $this->db->getQueryBuilder();
+ $query->delete('addressbookchanges')
+ ->where(
+ $query->expr()->lte('id', $query->createNamedParameter($maxId - $keep, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
+ $query->expr()->lte('created_at', $query->createNamedParameter($retention)),
+ );
+ return $query->executeStatement();
}
- private function convertPrincipal($principalUri, $toV2) {
+ private function convertPrincipal(string $principalUri, bool $toV2): string {
if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
[, $name] = \Sabre\Uri\split($principalUri);
if ($toV2 === true) {
@@ -1356,7 +1490,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
return $principalUri;
}
- private function addOwnerPrincipal(&$addressbookInfo) {
+ private function addOwnerPrincipal(array &$addressbookInfo): void {
$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
if (isset($addressbookInfo[$ownerPrincipalKey])) {
@@ -1376,10 +1510,10 @@ class CardDavBackend implements BackendInterface, SyncSupport {
*
* @param string $cardData the vcard raw data
* @return string the uid
- * @throws BadRequest if no UID is available
+ * @throws BadRequest if no UID is available or vcard is empty
*/
- private function getUID($cardData) {
- if ($cardData != '') {
+ private function getUID(string $cardData): string {
+ if ($cardData !== '') {
$vCard = Reader::read($cardData);
if ($vCard->UID) {
$uid = $vCard->UID->getValue();
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 340e3127f0a..30dba99839e 100644
--- a/apps/dav/lib/CardDAV/Converter.php
+++ b/apps/dav/lib/CardDAV/Converter.php
@@ -1,50 +1,35 @@
<?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 IAccountManager */
- private $accountManager;
-
- public function __construct(IAccountManager $accountManager) {
- $this->accountManager = $accountManager;
+ public function __construct(
+ private IAccountManager $accountManager,
+ private IUserManager $userManager,
+ private IURLGenerator $urlGenerator,
+ private LoggerInterface $logger,
+ ) {
}
public function createCardFromUser(IUser $user): ?VCard {
- $userProperties = $this->accountManager->getAccount($user)->getProperties();
+ $userProperties = $this->accountManager->getAccount($user)->getAllProperties();
$uid = $user->getUID();
$cloudId = $user->getCloudId();
@@ -57,40 +42,108 @@ class Converter {
$publish = false;
foreach ($userProperties as $property) {
- $shareWithTrustedServers =
- $property->getScope() === IAccountManager::SCOPE_FEDERATED ||
- $property->getScope() === IAccountManager::SCOPE_PUBLISHED;
-
- $emptyValue = $property->getValue() === '';
-
- if ($shareWithTrustedServers && !$emptyValue) {
- $publish = true;
- switch ($property->getName()) {
- case IAccountManager::PROPERTY_DISPLAYNAME:
- $vCard->add(new Text($vCard, 'FN', $property->getValue()));
- $vCard->add(new Text($vCard, 'N', $this->splitFullName($property->getValue())));
- break;
- case IAccountManager::PROPERTY_AVATAR:
- if ($image !== null) {
- $vCard->add('PHOTO', $image->data(), ['ENCODING' => 'b', 'TYPE' => $image->mimeType()]);
- }
- break;
- case IAccountManager::PROPERTY_EMAIL:
- $vCard->add(new Text($vCard, 'EMAIL', $property->getValue(), ['TYPE' => 'OTHER']));
- break;
- case IAccountManager::PROPERTY_WEBSITE:
- $vCard->add(new Text($vCard, 'URL', $property->getValue()));
- break;
- case IAccountManager::PROPERTY_PHONE:
- $vCard->add(new Text($vCard, 'TEL', $property->getValue(), ['TYPE' => 'OTHER']));
- break;
- case IAccountManager::PROPERTY_ADDRESS:
- $vCard->add(new Text($vCard, 'ADR', $property->getValue(), ['TYPE' => 'OTHER']));
- break;
- case IAccountManager::PROPERTY_TWITTER:
- $vCard->add(new Text($vCard, 'X-SOCIALPROFILE', $property->getValue(), ['TYPE' => 'TWITTER']));
+ if ($property->getName() !== IAccountManager::PROPERTY_AVATAR && empty($property->getValue())) {
+ continue;
+ }
+
+ $scope = $property->getScope();
+ // Do not write private data to the system address book at all
+ if ($scope === IAccountManager::SCOPE_PRIVATE || empty($scope)) {
+ continue;
+ }
+
+ $publish = true;
+ switch ($property->getName()) {
+ case IAccountManager::PROPERTY_DISPLAYNAME:
+ $vCard->add(new Text($vCard, 'FN', $property->getValue(), ['X-NC-SCOPE' => $scope]));
+ $vCard->add(new Text($vCard, 'N', $this->splitFullName($property->getValue()), ['X-NC-SCOPE' => $scope]));
+ break;
+ case IAccountManager::PROPERTY_AVATAR:
+ if ($image !== null) {
+ $vCard->add('PHOTO', $image->data(), ['ENCODING' => 'b', 'TYPE' => $image->mimeType(), ['X-NC-SCOPE' => $scope]]);
+ }
+ break;
+ case IAccountManager::COLLECTION_EMAIL:
+ case IAccountManager::PROPERTY_EMAIL:
+ $vCard->add(new Text($vCard, 'EMAIL', $property->getValue(), ['TYPE' => 'OTHER', 'X-NC-SCOPE' => $scope]));
+ break;
+ case IAccountManager::PROPERTY_WEBSITE:
+ $vCard->add(new Text($vCard, 'URL', $property->getValue(), ['X-NC-SCOPE' => $scope]));
+ break;
+ case IAccountManager::PROPERTY_PROFILE_ENABLED:
+ if ($property->getValue()) {
+ $vCard->add(
+ new Text(
+ $vCard,
+ 'X-SOCIALPROFILE',
+ $this->urlGenerator->linkToRouteAbsolute('profile.ProfilePage.index', ['targetUserId' => $user->getUID()]),
+ [
+ 'TYPE' => 'NEXTCLOUD',
+ 'X-NC-SCOPE' => IAccountManager::SCOPE_PUBLISHED
+ ]
+ )
+ );
+ }
+ break;
+ case IAccountManager::PROPERTY_PHONE:
+ $vCard->add(new Text($vCard, 'TEL', $property->getValue(), ['TYPE' => 'VOICE', 'X-NC-SCOPE' => $scope]));
+ break;
+ case IAccountManager::PROPERTY_ADDRESS:
+ // structured prop: https://www.rfc-editor.org/rfc/rfc6350.html#section-6.3.1
+ // post office box;extended address;street address;locality;region;postal code;country
+ $vCard->add(
+ new Text(
+ $vCard,
+ 'ADR',
+ [ '', '', '', $property->getValue(), '', '', '' ],
+ [
+ 'TYPE' => 'OTHER',
+ 'X-NC-SCOPE' => $scope,
+ ]
+ )
+ );
+ break;
+ case IAccountManager::PROPERTY_TWITTER:
+ $vCard->add(new Text($vCard, 'X-SOCIALPROFILE', $property->getValue(), ['TYPE' => 'TWITTER', 'X-NC-SCOPE' => $scope]));
+ break;
+ case IAccountManager::PROPERTY_ORGANISATION:
+ $vCard->add(new Text($vCard, 'ORG', $property->getValue(), ['X-NC-SCOPE' => $scope]));
+ break;
+ 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;
+ }
+ }
+
+ // Local properties
+ $managers = $user->getManagerUids();
+ // X-MANAGERSNAME only allows a single value, so we take the first manager
+ if (isset($managers[0])) {
+ $displayName = $this->userManager->getDisplayName($managers[0]);
+ // Only set the manager if a user object is found
+ if ($displayName !== null) {
+ $vCard->add(new Text($vCard, 'X-MANAGERSNAME', $displayName, [
+ 'uid' => $managers[0],
+ 'X-NC-SCOPE' => IAccountManager::SCOPE_LOCAL,
+ ]));
}
}
@@ -125,7 +178,7 @@ class Converter {
private function getAvatarImage(IUser $user): ?IImage {
try {
- return $user->getAvatarImage(-1);
+ return $user->getAvatarImage(512);
} catch (Exception $ex) {
return null;
}
diff --git a/apps/dav/lib/CardDAV/HasPhotoPlugin.php b/apps/dav/lib/CardDAV/HasPhotoPlugin.php
index 528fcb36bf5..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;
@@ -66,8 +47,8 @@ class HasPhotoPlugin extends ServerPlugin {
return $vcard instanceof VCard
&& $vcard->PHOTO
// Either the PHOTO is a url (doesn't start with data:) or the mimetype has to start with image/
- && (strpos($vcard->PHOTO->getValue(), 'data:') !== 0
- || strpos($vcard->PHOTO->getValue(), 'data:image/') === 0)
+ && (!str_starts_with($vcard->PHOTO->getValue(), 'data:')
+ || str_starts_with($vcard->PHOTO->getValue(), 'data:image/'))
;
});
}
diff --git a/apps/dav/lib/CardDAV/ImageExportPlugin.php b/apps/dav/lib/CardDAV/ImageExportPlugin.php
index 4caf234e346..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;
}
@@ -98,18 +81,17 @@ class ImageExportPlugin extends ServerPlugin {
$response->setHeader('Cache-Control', 'private, max-age=3600, must-revalidate');
$response->setHeader('Etag', $node->getETag());
- $response->setHeader('Pragma', 'public');
try {
$file = $this->cache->get($addressbook->getResourceId(), $node->getName(), $size, $node);
$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 b1deb638f3d..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,19 +32,10 @@ abstract class ExternalAddressBook implements IAddressBook, DAV\IProperties {
*/
private const DELIMITER = '--';
- /** @var string */
- private $appId;
-
- /** @var string */
- private $uri;
-
- /**
- * @param string $appId
- * @param string $uri
- */
- public function __construct(string $appId, string $uri) {
- $this->appId = $appId;
- $this->uri = $uri;
+ public function __construct(
+ private string $appId,
+ private string $uri,
+ ) {
}
/**
@@ -98,7 +71,7 @@ abstract class ExternalAddressBook implements IAddressBook, DAV\IProperties {
* @return bool
*/
public static function isAppGeneratedAddressBook(string $uri): bool {
- return strpos($uri, self::PREFIX) === 0 && substr_count($uri, self::DELIMITER) >= 2;
+ return str_starts_with($uri, self::PREFIX) && substr_count($uri, self::DELIMITER) >= 2;
}
/**
@@ -128,6 +101,6 @@ abstract class ExternalAddressBook implements IAddressBook, DAV\IProperties {
* @return bool
*/
public static function doesViolateReservedName(string $uri): bool {
- return strpos($uri, self::PREFIX) === 0;
+ return str_starts_with($uri, self::PREFIX);
}
}
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 62c4f7e8178..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,8 +44,8 @@ class MultiGetExportPlugin extends DAV\ServerPlugin {
}
// Only handling xml
- $contentType = $response->getHeader('Content-Type');
- if (strpos($contentType, 'application/xml') === false && strpos($contentType, 'text/xml') === false) {
+ $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 d3e4b2450d3..03c71f7e4a3 100644
--- a/apps/dav/lib/CardDAV/PhotoCache.php
+++ b/apps/dav/lib/CardDAV/PhotoCache.php
@@ -1,40 +1,20 @@
<?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\ILogger;
+use OCP\Image;
+use Psr\Log\LoggerInterface;
use Sabre\CardDAV\Card;
use Sabre\VObject\Document;
use Sabre\VObject\Parameter;
@@ -42,42 +22,28 @@ 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',
];
- /** @var IAppData */
- protected $appData;
-
- /** @var ILogger */
- protected $logger;
-
- /**
- * PhotoCache constructor.
- *
- * @param IAppData $appData
- * @param ILogger $logger
- */
- public function __construct(IAppData $appData, ILogger $logger) {
- $this->appData = $appData;
- $this->logger = $logger;
+ public function __construct(
+ private IAppDataFactory $appDataFactory,
+ private LoggerInterface $logger,
+ ) {
}
/**
- * @param int $addressBookId
- * @param string $cardUri
- * @param int $size
- * @param Card $card
- *
- * @return ISimpleFile
* @throws NotFoundException
*/
- public function get($addressBookId, $cardUri, $size, Card $card) {
+ public function get(int $addressBookId, string $cardUri, int $size, Card $card): ISimpleFile {
$folder = $this->getFolder($addressBookId, $cardUri);
if ($this->isEmpty($folder)) {
@@ -95,17 +61,11 @@ class PhotoCache {
return $this->getFile($folder, $size);
}
- /**
- * @param ISimpleFolder $folder
- * @return bool
- */
- private function isEmpty(ISimpleFolder $folder) {
+ private function isEmpty(ISimpleFolder $folder): bool {
return $folder->getDirectoryListing() === [];
}
/**
- * @param ISimpleFolder $folder
- * @param Card $card
* @throws NotPermittedException
*/
private function init(ISimpleFolder $folder, Card $card): void {
@@ -128,11 +88,14 @@ class PhotoCache {
$file->putContent($data['body']);
}
- private function hasPhoto(ISimpleFolder $folder) {
+ private function hasPhoto(ISimpleFolder $folder): bool {
return !$folder->fileExists('nophoto');
}
- private function getFile(ISimpleFolder $folder, $size) {
+ /**
+ * @param float|-1 $size
+ */
+ private function getFile(ISimpleFolder $folder, $size): ISimpleFile {
$ext = $this->getExtension($folder);
if ($size === -1) {
@@ -148,7 +111,7 @@ class PhotoCache {
throw new NotFoundException;
}
- $photo = new \OC_Image();
+ $photo = new Image();
/** @var ISimpleFile $file */
$file = $folder->getFile('photo.' . $ext);
$photo->loadFromData($file->getContent());
@@ -158,7 +121,7 @@ class PhotoCache {
$ratio = 1 / $ratio;
}
- $size = (int) ($size * $ratio);
+ $size = (int)($size * $ratio);
if ($size !== -1) {
$photo->resize($size);
}
@@ -180,21 +143,18 @@ 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;
}
}
/**
* Get the extension of the avatar. If there is no avatar throw Exception
*
- * @param ISimpleFolder $folder
- * @return string
* @throws NotFoundException
*/
private function getExtension(ISimpleFolder $folder): string {
@@ -209,23 +169,22 @@ class PhotoCache {
/**
* @param Card $node
- * @return bool|array{body: string, Content-Type: string}
+ * @return false|array{body: string, Content-Type: string}
*/
private function getPhoto(Card $node) {
try {
$vObject = $this->readCard($node->get());
return $this->getPhotoFromVObject($vObject);
} catch (\Exception $e) {
- $this->logger->logException($e, [
- 'message' => 'Exception during vcard photo parsing'
+ $this->logger->error('Exception during vcard photo parsing', [
+ 'exception' => $e
]);
}
return false;
}
/**
- * @param Document $vObject
- * @return bool|array{body: string, Content-Type: string}
+ * @return false|array{body: string, Content-Type: string}
*/
public function getPhotoFromVObject(Document $vObject) {
try {
@@ -262,18 +221,14 @@ class PhotoCache {
'body' => $val
];
} catch (\Exception $e) {
- $this->logger->logException($e, [
- 'message' => 'Exception during vcard photo parsing'
+ $this->logger->error('Exception during vcard photo parsing', [
+ 'exception' => $e
]);
}
return false;
}
- /**
- * @param string $cardData
- * @return \Sabre\VObject\Document
- */
- private function readCard($cardData) {
+ private function readCard(string $cardData): Document {
return Reader::read($cardData);
}
@@ -286,9 +241,9 @@ class PhotoCache {
if (isset($params['TYPE']) || isset($params['MEDIATYPE'])) {
/** @var Parameter $typeParam */
$typeParam = isset($params['TYPE']) ? $params['TYPE'] : $params['MEDIATYPE'];
- $type = $typeParam->getValue();
+ $type = (string)$typeParam->getValue();
- if (strpos($type, 'image/') === 0) {
+ if (str_starts_with($type, 'image/')) {
return $type;
} else {
return 'image/' . strtolower($type);
@@ -310,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
new file mode 100644
index 00000000000..557115762fc
--- /dev/null
+++ b/apps/dav/lib/CardDAV/Sharing/Backend.php
@@ -0,0 +1,29 @@
+<?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\Sharing;
+
+use OCA\DAV\Connector\Sabre\Principal;
+use OCA\DAV\DAV\Sharing\Backend as SharingBackend;
+use OCP\ICacheFactory;
+use OCP\IGroupManager;
+use OCP\IUserManager;
+use Psr\Log\LoggerInterface;
+
+class Backend extends SharingBackend {
+ public function __construct(
+ private IUserManager $userManager,
+ private IGroupManager $groupManager,
+ private Principal $principalBackend,
+ private ICacheFactory $cacheFactory,
+ private Service $service,
+ private LoggerInterface $logger,
+ ) {
+ parent::__construct($this->userManager, $this->groupManager, $this->principalBackend, $this->cacheFactory, $this->service, $this->logger);
+ }
+}
diff --git a/apps/dav/lib/CardDAV/Sharing/Service.php b/apps/dav/lib/CardDAV/Sharing/Service.php
new file mode 100644
index 00000000000..1ab208f7ec3
--- /dev/null
+++ b/apps/dav/lib/CardDAV/Sharing/Service.php
@@ -0,0 +1,21 @@
+<?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\Sharing;
+
+use OCA\DAV\DAV\Sharing\SharingMapper;
+use OCA\DAV\DAV\Sharing\SharingService;
+
+class Service extends SharingService {
+ protected string $resourceType = 'addressbook';
+ 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 73bfaf01b60..e6da3ed5923 100644
--- a/apps/dav/lib/CardDAV/SyncService.php
+++ b/apps/dav/lib/CardDAV/SyncService.php
@@ -1,107 +1,69 @@
<?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>
- *
- * @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 OC\Accounts\AccountManager;
+use OCP\AppFramework\Db\TTransactional;
use OCP\AppFramework\Http;
-use OCP\ILogger;
+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 Sabre\DAV\Client;
+use Psr\Http\Client\ClientExceptionInterface;
+use Psr\Log\LoggerInterface;
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 {
- /** @var CardDavBackend */
- private $backend;
-
- /** @var IUserManager */
- private $userManager;
-
- /** @var ILogger */
- private $logger;
-
- /** @var array */
- private $localSystemAddressBook;
-
- /** @var Converter */
- private $converter;
-
- /** @var string */
- protected $certPath;
-
- /**
- * SyncService constructor.
- *
- * @param CardDavBackend $backend
- * @param IUserManager $userManager
- * @param ILogger $logger
- * @param AccountManager $accountManager
- */
- public function __construct(CardDavBackend $backend, IUserManager $userManager, ILogger $logger, Converter $converter) {
- $this->backend = $backend;
- $this->userManager = $userManager;
- $this->logger = $logger;
- $this->converter = $converter;
+ use TTransactional;
+ private ?array $localSystemAddressBook = null;
+ protected string $certPath;
+
+ 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 = '';
}
/**
- * @param string $url
- * @param string $userName
- * @param string $addressBookUrl
- * @param string $sharedSecret
- * @param string $syncToken
- * @param int $targetBookId
- * @param string $targetPrincipal
- * @param array $targetProperties
- * @return string
+ * @psalm-return list{0: ?string, 1: boolean}
* @throws \Exception
*/
- public function syncRemoteAddressBook($url, $userName, $addressBookUrl, $sharedSecret, $syncToken, $targetBookId, $targetPrincipal, $targetProperties) {
+ 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, $targetBookId, $targetProperties);
+ $book = $this->ensureSystemAddressBookExists($targetPrincipal, $targetBookHash, $targetProperties);
$addressBookId = $book['id'];
// 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);
- $this->logger->info('Authorization failed, remove address book: ' . $url, ['app' => 'dav']);
+ $this->logger->error('Authorization failed, remove address book: ' . $url, ['app' => 'dav']);
throw $ex;
}
+ $this->logger->error('Client exception:', ['app' => 'dav', 'exception' => $ex]);
+ throw $ex;
}
// 3. apply changes
@@ -110,123 +72,147 @@ class SyncService {
$cardUri = basename($resource);
if (isset($status[200])) {
$vCard = $this->download($url, $userName, $sharedSecret, $resource);
- $existingCard = $this->backend->getCard($addressBookId, $cardUri);
- if ($existingCard === false) {
- $this->backend->createCard($addressBookId, $cardUri, $vCard['body']);
- } else {
- $this->backend->updateCard($addressBookId, $cardUri, $vCard['body']);
- }
+ $this->atomic(function () use ($addressBookId, $cardUri, $vCard): void {
+ $existingCard = $this->backend->getCard($addressBookId, $cardUri);
+ if ($existingCard === false) {
+ $this->backend->createCard($addressBookId, $cardUri, $vCard);
+ } else {
+ $this->backend->updateCard($addressBookId, $cardUri, $vCard);
+ }
+ }, $this->dbConnection);
} else {
$this->backend->deleteCard($addressBookId, $cardUri);
}
}
- return $response['token'];
+ return [
+ $response['token'],
+ $response['truncated'],
+ ];
}
/**
- * @param string $principal
- * @param string $id
- * @param array $properties
- * @return array|null
* @throws \Sabre\DAV\Exception\BadRequest
*/
- public function ensureSystemAddressBookExists($principal, $id, $properties) {
- $book = $this->backend->getAddressBooksByUri($principal, $id);
- if (!is_null($book)) {
- return $book;
- }
- $this->backend->createAddressBook($principal, $id, $properties);
-
- return $this->backend->getAddressBooksByUri($principal, $id);
- }
-
- /**
- * Check if there is a valid certPath we should use
- *
- * @return string
- */
- protected function getCertPath() {
+ public function ensureSystemAddressBookExists(string $principal, string $uri, array $properties): ?array {
+ 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);
+
+ 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;
}
/**
- * @param string $url
- * @param string $userName
- * @param string $addressBookUrl
- * @param string $sharedSecret
- * @return Client
+ * @return array{response: array<string, array<array-key, mixed>>, token: ?string, truncated: bool}
+ * @throws ClientExceptionInterface
+ * @throws ParseException
*/
- protected function getClient($url, $userName, $sharedSecret) {
- $settings = [
- 'baseUri' => $url . '/',
- 'userName' => $userName,
- 'password' => $sharedSecret,
+ 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 !== '' && strpos($url, 'http://') !== 0) {
- $client->addCurlSetting(CURLOPT_CAINFO, $this->certPath);
- }
+ $response = $client->request(
+ 'REPORT',
+ $uri,
+ $options
+ );
- return $client;
- }
+ $body = $response->getBody();
+ assert(is_string($body));
- /**
- * @param string $url
- * @param string $userName
- * @param string $addressBookUrl
- * @param string $sharedSecret
- * @param string $syncToken
- * @return array
- */
- protected function requestSyncReport($url, $userName, $addressBookUrl, $sharedSecret, $syncToken) {
- $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
+ );
- /**
- * @param string $url
- * @param string $userName
- * @param string $sharedSecret
- * @param string $resourcePath
- * @return array
- */
- protected function download($url, $userName, $sharedSecret, $resourcePath) {
- $client = $this->getClient($url, $userName, $sharedSecret);
- return $client->request('GET', $resourcePath);
+ return (string)$response->getBody();
}
- /**
- * @param string|null $syncToken
- * @return string
- */
- private function buildSyncCollectionRequestBody($syncToken) {
+ 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');
@@ -240,49 +226,77 @@ 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, '/')
+ );
}
/**
* @param IUser $user
*/
- public function updateUser(IUser $user) {
+ public function updateUser(IUser $user): void {
$systemAddressBook = $this->getLocalSystemAddressBook();
$addressBookId = $systemAddressBook['id'];
- $name = $user->getBackendClassName();
- $userId = $user->getUID();
- $cardId = "$name:$userId.vcf";
- $card = $this->backend->getCard($addressBookId, $cardId);
+ $cardId = self::getCardUri($user);
if ($user->isEnabled()) {
- if ($card === false) {
- $vCard = $this->converter->createCardFromUser($user);
- if ($vCard !== null) {
- $this->backend->createCard($addressBookId, $cardId, $vCard->serialize());
- }
- } else {
- $vCard = $this->converter->createCardFromUser($user);
- if (is_null($vCard)) {
- $this->backend->deleteCard($addressBookId, $cardId);
+ $this->atomic(function () use ($addressBookId, $cardId, $user): void {
+ $card = $this->backend->getCard($addressBookId, $cardId);
+ if ($card === false) {
+ $vCard = $this->converter->createCardFromUser($user);
+ if ($vCard !== null) {
+ $this->backend->createCard($addressBookId, $cardId, $vCard->serialize(), false);
+ }
} else {
- $this->backend->updateCard($addressBookId, $cardId, $vCard->serialize());
+ $vCard = $this->converter->createCardFromUser($user);
+ if (is_null($vCard)) {
+ $this->backend->deleteCard($addressBookId, $cardId);
+ } else {
+ $this->backend->updateCard($addressBookId, $cardId, $vCard->serialize());
+ }
}
- }
+ }, $this->dbConnection);
} else {
$this->backend->deleteCard($addressBookId, $cardId);
}
@@ -294,10 +308,7 @@ class SyncService {
public function deleteUser($userOrCardId) {
$systemAddressBook = $this->getLocalSystemAddressBook();
if ($userOrCardId instanceof IUser) {
- $name = $userOrCardId->getBackendClassName();
- $userId = $userOrCardId->getUID();
-
- $userOrCardId = "$name:$userId.vcf";
+ $userOrCardId = self::getCardUri($userOrCardId);
}
$this->backend->deleteCard($systemAddressBook['id'], $userOrCardId);
}
@@ -307,18 +318,18 @@ 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;
}
- public function syncInstance(\Closure $progressCallback = null) {
+ /**
+ * @return void
+ */
+ 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();
@@ -336,4 +347,12 @@ class SyncService {
}
}
}
+
+ /**
+ * @param IUser $user
+ * @return string
+ */
+ public static function getCardUri(IUser $user): string {
+ return $user->getBackendClassName() . ':' . $user->getUID() . '.vcf';
+ }
}
diff --git a/apps/dav/lib/CardDAV/SystemAddressbook.php b/apps/dav/lib/CardDAV/SystemAddressbook.php
index 502e353acb3..912a2f1dcee 100644
--- a/apps/dav/lib/CardDAV/SystemAddressbook.php
+++ b/apps/dav/lib/CardDAV/SystemAddressbook.php
@@ -3,51 +3,335 @@
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>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have 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\Federation\TrustedServers;
+use OCP\Accounts\IAccountManager;
use OCP\IConfig;
+use OCP\IGroupManager;
use OCP\IL10N;
+use OCP\IRequest;
+use OCP\IUserSession;
use Sabre\CardDAV\Backend\BackendInterface;
+use Sabre\CardDAV\Backend\SyncSupport;
+use Sabre\CardDAV\Card;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\VObject\Component\VCard;
+use Sabre\VObject\Reader;
+use function array_filter;
+use function array_intersect;
+use function array_unique;
+use function in_array;
class SystemAddressbook extends AddressBook {
- /** @var IConfig */
- private $config;
+ public const URI_SHARED = 'z-server-generated--system';
- public function __construct(BackendInterface $carddavBackend, array $addressBookInfo, IL10N $l10n, IConfig $config) {
+ public function __construct(
+ BackendInterface $carddavBackend,
+ array $addressBookInfo,
+ IL10N $l10n,
+ 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->addressBookInfo['{DAV:}displayname'] = $l10n->t('Accounts');
+ $this->addressBookInfo['{' . Plugin::NS_CARDDAV . '}addressbook-description'] = $l10n->t('System address book which holds all accounts');
}
+ /**
+ * No checkbox checked -> Show only the same user
+ * 'Allow username autocompletion in share dialog' -> show everyone
+ * 'Allow username autocompletion in share dialog' + 'Allow username autocompletion to users within the same groups' -> show only users in intersecting groups
+ * 'Allow username autocompletion in share dialog' + 'Allow username autocompletion to users based on phone number integration' -> show only the same user
+ * 'Allow username autocompletion in share dialog' + 'Allow username autocompletion to users within the same groups' + 'Allow username autocompletion to users based on phone number integration' -> show only users in intersecting groups
+ */
public function getChildren() {
$shareEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
$shareEnumerationGroup = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
$shareEnumerationPhone = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
- if (!$shareEnumeration || $shareEnumerationGroup || $shareEnumerationPhone) {
+ $user = $this->userSession->getUser();
+ if (!$user) {
+ // Should never happen because we don't allow anonymous access
return [];
}
+ if ($user->getBackendClassName() === 'Guests' || !$shareEnumeration || (!$shareEnumerationGroup && $shareEnumerationPhone)) {
+ $name = SyncService::getCardUri($user);
+ try {
+ return [parent::getChild($name)];
+ } catch (NotFound $e) {
+ return [];
+ }
+ }
+ if ($shareEnumerationGroup) {
+ if ($this->groupManager === null) {
+ // Group manager is not available, so we can't determine which data is safe
+ return [];
+ }
+ $groups = $this->groupManager->getUserGroups($user);
+ $names = [];
+ foreach ($groups as $group) {
+ $users = $group->getUsers();
+ foreach ($users as $groupUser) {
+ if ($groupUser->getBackendClassName() === 'Guests') {
+ continue;
+ }
+ $names[] = SyncService::getCardUri($groupUser);
+ }
+ }
+ return parent::getMultipleChildren(array_unique($names));
+ }
+
+ $children = parent::getChildren();
+ return array_filter($children, function (Card $child) {
+ // check only for URIs that begin with Guests:
+ return !str_starts_with($child->getName(), 'Guests:');
+ });
+ }
+
+ /**
+ * @param array $paths
+ * @return Card[]
+ * @throws NotFound
+ */
+ public function getMultipleChildren($paths): array {
+ $shareEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
+ $shareEnumerationGroup = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
+ $shareEnumerationPhone = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
+ $user = $this->userSession->getUser();
+ if (($user !== null && $user->getBackendClassName() === 'Guests') || !$shareEnumeration || (!$shareEnumerationGroup && $shareEnumerationPhone)) {
+ // No user or cards with no access
+ if ($user === null || !in_array(SyncService::getCardUri($user), $paths, true)) {
+ return [];
+ }
+ // Only return the own card
+ try {
+ return [parent::getChild(SyncService::getCardUri($user))];
+ } catch (NotFound $e) {
+ return [];
+ }
+ }
+ if ($shareEnumerationGroup) {
+ if ($this->groupManager === null || $user === null) {
+ // Group manager or user is not available, so we can't determine which data is safe
+ return [];
+ }
+ $groups = $this->groupManager->getUserGroups($user);
+ $allowedNames = [];
+ foreach ($groups as $group) {
+ $users = $group->getUsers();
+ foreach ($users as $groupUser) {
+ if ($groupUser->getBackendClassName() === 'Guests') {
+ continue;
+ }
+ $allowedNames[] = SyncService::getCardUri($groupUser);
+ }
+ }
+ return parent::getMultipleChildren(array_intersect($paths, $allowedNames));
+ }
+ if (!$this->isFederation()) {
+ return parent::getMultipleChildren($paths);
+ }
+
+ $objs = $this->carddavBackend->getMultipleCards($this->addressBookInfo['id'], $paths);
+ $children = [];
+ /** @var array $obj */
+ foreach ($objs as $obj) {
+ if (empty($obj)) {
+ continue;
+ }
+ $carddata = $this->extractCarddata($obj);
+ if (empty($carddata)) {
+ continue;
+ } else {
+ $obj['carddata'] = $carddata;
+ }
+ $children[] = new Card($this->carddavBackend, $this->addressBookInfo, $obj);
+ }
+ return $children;
+ }
+
+ /**
+ * @param string $name
+ * @return Card
+ * @throws NotFound
+ * @throws Forbidden
+ */
+ public function getChild($name): Card {
+ $user = $this->userSession->getUser();
+ $shareEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
+ $shareEnumerationGroup = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
+ $shareEnumerationPhone = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
+ if (($user !== null && $user->getBackendClassName() === 'Guests') || !$shareEnumeration || (!$shareEnumerationGroup && $shareEnumerationPhone)) {
+ $ownName = $user !== null ? SyncService::getCardUri($user) : null;
+ if ($ownName === $name) {
+ return parent::getChild($name);
+ }
+ throw new Forbidden();
+ }
+ if ($shareEnumerationGroup) {
+ if ($user === null || $this->groupManager === null) {
+ // Group manager is not available, so we can't determine which data is safe
+ throw new Forbidden();
+ }
+ $groups = $this->groupManager->getUserGroups($user);
+ foreach ($groups as $group) {
+ foreach ($group->getUsers() as $groupUser) {
+ if ($groupUser->getBackendClassName() === 'Guests') {
+ continue;
+ }
+ $otherName = SyncService::getCardUri($groupUser);
+ if ($otherName === $name) {
+ return parent::getChild($name);
+ }
+ }
+ }
+ throw new Forbidden();
+ }
+ if (!$this->isFederation()) {
+ return parent::getChild($name);
+ }
+
+ $obj = $this->carddavBackend->getCard($this->addressBookInfo['id'], $name);
+ if (!$obj) {
+ throw new NotFound('Card not found');
+ }
+ $carddata = $this->extractCarddata($obj);
+ if (empty($carddata)) {
+ throw new Forbidden();
+ } else {
+ $obj['carddata'] = $carddata;
+ }
+ return new Card($this->carddavBackend, $this->addressBookInfo, $obj);
+ }
+ public function getChanges($syncToken, $syncLevel, $limit = null) {
+
+ if (!$this->carddavBackend instanceof SyncSupport) {
+ return null;
+ }
+
+ if (!$this->isFederation()) {
+ return parent::getChanges($syncToken, $syncLevel, $limit);
+ }
+
+ $changed = $this->carddavBackend->getChangesForAddressBook(
+ $this->addressBookInfo['id'],
+ $syncToken,
+ $syncLevel,
+ $limit
+ );
+
+ if (empty($changed)) {
+ return $changed;
+ }
+
+ $added = $modified = $deleted = [];
+ foreach ($changed['added'] as $uri) {
+ try {
+ $this->getChild($uri);
+ $added[] = $uri;
+ } catch (NotFound|Forbidden $e) {
+ $deleted[] = $uri;
+ }
+ }
+ foreach ($changed['modified'] as $uri) {
+ try {
+ $this->getChild($uri);
+ $modified[] = $uri;
+ } catch (NotFound|Forbidden $e) {
+ $deleted[] = $uri;
+ }
+ }
+ $changed['added'] = $added;
+ $changed['modified'] = $modified;
+ $changed['deleted'] = $deleted;
+ return $changed;
+ }
+
+ private function isFederation(): bool {
+ if ($this->trustedServers === null || $this->request === null) {
+ return false;
+ }
+
+ /** @psalm-suppress NoInterfaceProperties */
+ $server = $this->request->server;
+ if (!isset($server['PHP_AUTH_USER']) || $server['PHP_AUTH_USER'] !== 'system') {
+ return false;
+ }
+
+ /** @psalm-suppress NoInterfaceProperties */
+ $sharedSecret = $server['PHP_AUTH_PW'] ?? null;
+ if ($sharedSecret === null) {
+ return false;
+ }
+
+ $servers = $this->trustedServers->getServers();
+ $trusted = array_filter($servers, function ($trustedServer) use ($sharedSecret) {
+ return $trustedServer['shared_secret'] === $sharedSecret;
+ });
+ // Authentication is fine, but it's not for a federated share
+ if (empty($trusted)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * If the validation doesn't work the card is "not found" so we
+ * return empty carddata even if the carddata might exist in the local backend.
+ * This can happen when a user sets the required properties
+ * FN, N to a local scope only but the request is from
+ * a federated share.
+ *
+ * @see https://github.com/nextcloud/server/issues/38042
+ *
+ * @param array $obj
+ * @return string|null
+ */
+ private function extractCarddata(array $obj): ?string {
+ $obj['acl'] = $this->getChildACL();
+ $cardData = $obj['carddata'];
+ /** @var VCard $vCard */
+ $vCard = Reader::read($cardData);
+ foreach ($vCard->children() as $child) {
+ $scope = $child->offsetGet('X-NC-SCOPE');
+ if ($scope !== null && $scope->getValue() === IAccountManager::SCOPE_LOCAL) {
+ $vCard->remove($child);
+ }
+ }
+ $messages = $vCard->validate();
+ if (!empty($messages)) {
+ return null;
+ }
+
+ return $vCard->serialize();
+ }
+
+ /**
+ * @return mixed
+ * @throws Forbidden
+ */
+ public function delete() {
+ if ($this->isFederation()) {
+ parent::delete();
+ }
+ throw new Forbidden();
+ }
- return parent::getChildren();
+ public function getACL() {
+ return array_filter(parent::getACL(), function ($acl) {
+ if (in_array($acl['privilege'], ['{DAV:}write', '{DAV:}all'], true)) {
+ return false;
+ }
+ return true;
+ });
}
}
diff --git a/apps/dav/lib/CardDAV/UserAddressBooks.php b/apps/dav/lib/CardDAV/UserAddressBooks.php
index 98957301120..e29e52e77df 100644
--- a/apps/dav/lib/CardDAV/UserAddressBooks.php
+++ b/apps/dav/lib/CardDAV/UserAddressBooks.php
@@ -3,57 +3,47 @@
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>
- *
- * @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\AppInfo\PluginManager;
-use OCA\DAV\CardDAV\Integration\IAddressBookProvider;
use OCA\DAV\CardDAV\Integration\ExternalAddressBook;
+use OCA\DAV\CardDAV\Integration\IAddressBookProvider;
+use OCA\Federation\TrustedServers;
+use OCP\AppFramework\QueryException;
use OCP\IConfig;
+use OCP\IGroupManager;
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;
-use Sabre\DAV\Exception\MethodNotAllowed;
use Sabre\CardDAV\IAddressBook;
-use function array_map;
+use Sabre\DAV\Exception\MethodNotAllowed;
use Sabre\DAV\MkCol;
+use function array_map;
class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome {
-
/** @var IL10N */
protected $l10n;
/** @var IConfig */
protected $config;
- /** @var PluginManager */
- private $pluginManager;
-
- public function __construct(Backend\BackendInterface $carddavBackend,
- string $principalUri,
- PluginManager $pluginManager) {
+ public function __construct(
+ Backend\BackendInterface $carddavBackend,
+ string $principalUri,
+ private PluginManager $pluginManager,
+ private ?IUser $user,
+ private ?IGroupManager $groupManager,
+ ) {
parent::__construct($carddavBackend, $principalUri);
- $this->pluginManager = $pluginManager;
}
/**
@@ -66,18 +56,53 @@ 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 */
+ $principal = $this->principalUri;
$addressBooks = $this->carddavBackend->getAddressBooksForUser($this->principalUri);
- /** @var IAddressBook[] $objects */
- $objects = array_map(function (array $addressBook) {
- if ($addressBook['principaluri'] === 'principals/system/system') {
- return new SystemAddressbook($this->carddavBackend, $addressBook, $this->l10n, $this->config);
+ // add the system address book
+ $systemAddressBook = null;
+ $systemAddressBookExposed = $this->config->getAppValue('dav', 'system_addressbook_exposed', 'yes') === 'yes';
+ if ($systemAddressBookExposed && is_string($principal) && $principal !== 'principals/system/system' && $this->carddavBackend instanceof CardDavBackend) {
+ $systemAddressBook = $this->carddavBackend->getAddressBooksByUri('principals/system/system', 'system');
+ if ($systemAddressBook !== null) {
+ $systemAddressBook['uri'] = SystemAddressbook::URI_SHARED;
}
+ }
+ if (!is_null($systemAddressBook)) {
+ $addressBooks[] = $systemAddressBook;
+ }
- return new AddressBook($this->carddavBackend, $addressBook, $this->l10n);
- }, $addressBooks);
+ $objects = [];
+ if (!empty($addressBooks)) {
+ /** @var IAddressBook[] $objects */
+ $objects = array_map(function (array $addressBook) {
+ $trustedServers = null;
+ $request = null;
+ try {
+ $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') {
+ return new SystemAddressbook(
+ $this->carddavBackend,
+ $addressBook,
+ $this->l10n,
+ $this->config,
+ Server::get(IUserSession::class),
+ $request,
+ $trustedServers,
+ $this->groupManager
+ );
+ }
+
+ return new AddressBook($this->carddavBackend, $addressBook, $this->l10n);
+ }, $addressBooks);
+ }
/** @var IAddressBook[][] $objectsFromPlugins */
$objectsFromPlugins = array_map(function (IAddressBookProvider $plugin): array {
return $plugin->fetchAllForAddressBookHome($this->principalUri);
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 3d56d95868d..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;
@@ -32,35 +15,23 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class CreateAddressBook extends Command {
-
- /** @var IUserManager */
- private $userManager;
-
- /** @var CardDavBackend */
- private $cardDavBackend;
-
- /**
- * @param IUserManager $userManager
- * @param CardDavBackend $cardDavBackend
- */
- public function __construct(IUserManager $userManager,
- CardDavBackend $cardDavBackend
+ public function __construct(
+ private IUserManager $userManager,
+ private CardDavBackend $cardDavBackend,
) {
parent::__construct();
- $this->userManager = $userManager;
- $this->cardDavBackend = $cardDavBackend;
}
- protected function configure() {
+ protected function configure(): void {
$this
- ->setName('dav:create-addressbook')
- ->setDescription('Create a dav addressbook')
- ->addArgument('user',
- InputArgument::REQUIRED,
- 'User for whom the addressbook will be created')
- ->addArgument('name',
- InputArgument::REQUIRED,
- 'Name of the addressbook');
+ ->setName('dav:create-addressbook')
+ ->setDescription('Create a dav addressbook')
+ ->addArgument('user',
+ InputArgument::REQUIRED,
+ 'User for whom the addressbook will be created')
+ ->addArgument('name',
+ InputArgument::REQUIRED,
+ 'Name of the addressbook');
}
protected function execute(InputInterface $input, OutputInterface $output): int {
@@ -71,6 +42,6 @@ class CreateAddressBook extends Command {
$name = $input->getArgument('name');
$this->cardDavBackend->createAddressBook("principals/users/$user", $name, []);
- return 0;
+ return self::SUCCESS;
}
}
diff --git a/apps/dav/lib/Command/CreateCalendar.php b/apps/dav/lib/Command/CreateCalendar.php
index 1d818809245..033b5f8d347 100644
--- a/apps/dav/lib/Command/CreateCalendar.php
+++ b/apps/dav/lib/Command/CreateCalendar.php
@@ -1,68 +1,43 @@
<?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;
use OC\KnownUser\KnownUserService;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
+use OCA\DAV\CalDAV\Sharing\Backend;
use OCA\DAV\Connector\Sabre\Principal;
+use OCP\Accounts\IAccountManager;
+use OCP\App\IAppManager;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IUserManager;
+use OCP\IUserSession;
+use OCP\Security\ISecureRandom;
+use OCP\Server;
+use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class CreateCalendar extends Command {
-
- /** @var IUserManager */
- protected $userManager;
-
- /** @var IGroupManager $groupManager */
- private $groupManager;
-
- /** @var \OCP\IDBConnection */
- protected $dbConnection;
-
- /**
- * @param IUserManager $userManager
- * @param IGroupManager $groupManager
- * @param IDBConnection $dbConnection
- */
- public function __construct(IUserManager $userManager, IGroupManager $groupManager, IDBConnection $dbConnection) {
+ public function __construct(
+ protected IUserManager $userManager,
+ private IGroupManager $groupManager,
+ protected IDBConnection $dbConnection,
+ ) {
parent::__construct();
- $this->userManager = $userManager;
- $this->groupManager = $groupManager;
- $this->dbConnection = $dbConnection;
}
- protected function configure() {
+ protected function configure(): void {
$this
->setName('dav:create-calendar')
->setDescription('Create a dav calendar')
@@ -82,33 +57,31 @@ class CreateCalendar extends Command {
$principalBackend = new Principal(
$this->userManager,
$this->groupManager,
- \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->getLogger();
- $dispatcher = \OC::$server->get(IEventDispatcher::class);
- $legacyDispatcher = \OC::$server->getEventDispatcher();
- $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,
$principalBackend,
$this->userManager,
- $this->groupManager,
$random,
$logger,
$dispatcher,
- $legacyDispatcher,
- $config
+ $config,
+ Server::get(Backend::class),
);
$caldav->createCalendar("principals/users/$user", $name, []);
- return 0;
+ 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 dd5f11c740f..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;
@@ -38,40 +22,14 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class DeleteCalendar extends Command {
- /** @var CalDavBackend */
- private $calDav;
-
- /** @var IConfig */
- private $config;
-
- /** @var IL10N */
- private $l10n;
-
- /** @var IUserManager */
- private $userManager;
-
- /** @var LoggerInterface */
- private $logger;
-
- /**
- * @param CalDavBackend $calDav
- * @param IConfig $config
- * @param IL10N $l10n
- * @param IUserManager $userManager
- */
public function __construct(
- CalDavBackend $calDav,
- IConfig $config,
- IL10N $l10n,
- IUserManager $userManager,
- LoggerInterface $logger
+ private CalDavBackend $calDav,
+ private IConfig $config,
+ private IL10N $l10n,
+ private IUserManager $userManager,
+ private LoggerInterface $logger,
) {
parent::__construct();
- $this->calDav = $calDav;
- $this->config = $config;
- $this->l10n = $l10n;
- $this->userManager = $userManager;
- $this->logger = $logger;
}
protected function configure(): void {
@@ -96,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(
@@ -109,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(
@@ -140,6 +98,6 @@ class DeleteCalendar extends Command {
$calendar->delete();
- return 0;
+ return self::SUCCESS;
}
}
diff --git a/apps/dav/lib/Command/DeleteSubscription.php b/apps/dav/lib/Command/DeleteSubscription.php
new file mode 100644
index 00000000000..db0cb6295c9
--- /dev/null
+++ b/apps/dav/lib/Command/DeleteSubscription.php
@@ -0,0 +1,79 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Command;
+
+use OCA\DAV\CalDAV\CachedSubscription;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCP\IUserManager;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+#[AsCommand(
+ name: 'dav:delete-subscription',
+ description: 'Delete a calendar subscription for a user',
+ hidden: false,
+)]
+class DeleteSubscription extends Command {
+ public function __construct(
+ private CalDavBackend $calDavBackend,
+ private IUserManager $userManager,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->addArgument(
+ 'uid',
+ InputArgument::REQUIRED,
+ 'User who owns the calendar subscription'
+ )
+ ->addArgument(
+ 'uri',
+ InputArgument::REQUIRED,
+ 'URI of the calendar to be deleted'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $user = (string)$input->getArgument('uid');
+ if (!$this->userManager->userExists($user)) {
+ throw new \InvalidArgumentException("User $user is unknown");
+ }
+
+ $uri = (string)$input->getArgument('uri');
+ if ($uri === '') {
+ throw new \InvalidArgumentException('Specify the URI of the calendar to be deleted');
+ }
+
+ $subscriptionInfo = $this->calDavBackend->getSubscriptionByUri(
+ 'principals/users/' . $user,
+ $uri
+ );
+
+ if ($subscriptionInfo === null) {
+ throw new \InvalidArgumentException("User $user has no calendar subscription with the URI $uri");
+ }
+
+ $subscription = new CachedSubscription(
+ $this->calDavBackend,
+ $subscriptionInfo,
+ );
+
+ $subscription->delete();
+
+ $output->writeln("Calendar subscription with the URI $uri for user $user deleted");
+
+ return self::SUCCESS;
+ }
+}
diff --git a/apps/dav/lib/Command/ExportCalendar.php b/apps/dav/lib/Command/ExportCalendar.php
new file mode 100644
index 00000000000..6ed8aa2cfe4
--- /dev/null
+++ b/apps/dav/lib/Command/ExportCalendar.php
@@ -0,0 +1,95 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Command;
+
+use InvalidArgumentException;
+use OCA\DAV\CalDAV\Export\ExportService;
+use OCP\Calendar\CalendarExportOptions;
+use OCP\Calendar\ICalendarExport;
+use OCP\Calendar\IManager;
+use OCP\IUserManager;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Calendar Export Command
+ *
+ * Used to export data from supported calendars to disk or stdout
+ */
+#[AsCommand(
+ name: 'calendar:export',
+ description: 'Export calendar data from supported calendars to disk or stdout',
+ hidden: false
+)]
+class ExportCalendar extends Command {
+ public function __construct(
+ private IUserManager $userManager,
+ private IManager $calendarManager,
+ private ExportService $exportService,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this->setName('calendar:export')
+ ->setDescription('Export calendar data from supported calendars to disk or stdout')
+ ->addArgument('uid', InputArgument::REQUIRED, 'Id of system user')
+ ->addArgument('uri', InputArgument::REQUIRED, 'Uri of calendar')
+ ->addOption('format', null, InputOption::VALUE_REQUIRED, 'Format of output (ical, jcal, xcal) defaults to ical', 'ical')
+ ->addOption('location', null, InputOption::VALUE_REQUIRED, 'Location of where to write the output. defaults to stdout');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $userId = $input->getArgument('uid');
+ $calendarId = $input->getArgument('uri');
+ $format = $input->getOption('format');
+ $location = $input->getOption('location');
+
+ if (!$this->userManager->userExists($userId)) {
+ throw new InvalidArgumentException("User <$userId> not found.");
+ }
+ // retrieve calendar and evaluate if export is supported
+ $calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $userId, [$calendarId]);
+ if ($calendars === []) {
+ throw new InvalidArgumentException("Calendar <$calendarId> not found.");
+ }
+ $calendar = $calendars[0];
+ if (!$calendar instanceof ICalendarExport) {
+ throw new InvalidArgumentException("Calendar <$calendarId> does not support exporting");
+ }
+ // construct options object
+ $options = new CalendarExportOptions();
+ // evaluate if provided format is supported
+ if (!in_array($format, ExportService::FORMATS, true)) {
+ throw new InvalidArgumentException("Format <$format> is not valid.");
+ }
+ $options->setFormat($format);
+ // evaluate is a valid location was given and is usable otherwise output to stdout
+ if ($location !== null) {
+ $handle = fopen($location, 'wb');
+ if ($handle === false) {
+ throw new InvalidArgumentException("Location <$location> is not valid. Can not open location for write operation.");
+ }
+
+ foreach ($this->exportService->export($calendar, $options) as $chunk) {
+ fwrite($handle, $chunk);
+ }
+ fclose($handle);
+ } else {
+ foreach ($this->exportService->export($calendar, $options) as $chunk) {
+ $output->writeln($chunk);
+ }
+ }
+
+ return self::SUCCESS;
+ }
+}
diff --git a/apps/dav/lib/Command/FixCalendarSyncCommand.php b/apps/dav/lib/Command/FixCalendarSyncCommand.php
new file mode 100644
index 00000000000..cb31355c10d
--- /dev/null
+++ b/apps/dav/lib/Command/FixCalendarSyncCommand.php
@@ -0,0 +1,73 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Command;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCP\IUser;
+use OCP\IUserManager;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\ProgressBar;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class FixCalendarSyncCommand extends Command {
+
+ public function __construct(
+ private IUserManager $userManager,
+ private CalDavBackend $calDavBackend,
+ ) {
+ parent::__construct('dav:fix-missing-caldav-changes');
+ }
+
+ protected function configure(): void {
+ $this->setDescription('Insert missing calendarchanges rows for existing events');
+ $this->addArgument(
+ 'user',
+ InputArgument::OPTIONAL,
+ 'User to fix calendar sync tokens for, if omitted all users will be fixed',
+ null,
+ );
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ $userArg = $input->getArgument('user');
+ if ($userArg !== null) {
+ $user = $this->userManager->get($userArg);
+ if ($user === null) {
+ $output->writeln("<error>User $userArg does not exist</error>");
+ return self::FAILURE;
+ }
+
+ $this->fixUserCalendars($user);
+ } else {
+ $progress = new ProgressBar($output);
+ $this->userManager->callForSeenUsers(function (IUser $user) use ($progress): void {
+ $this->fixUserCalendars($user, $progress);
+ });
+ $progress->finish();
+ }
+ $output->writeln('');
+ return self::SUCCESS;
+ }
+
+ private function fixUserCalendars(IUser $user, ?ProgressBar $progress = null): void {
+ $calendars = $this->calDavBackend->getCalendarsForUser('principals/users/' . $user->getUID());
+
+ foreach ($calendars as $calendar) {
+ $this->calDavBackend->restoreChanges($calendar['id']);
+ }
+
+ if ($progress !== null) {
+ $progress->advance();
+ }
+ }
+
+}
diff --git a/apps/dav/lib/Command/GetAbsenceCommand.php b/apps/dav/lib/Command/GetAbsenceCommand.php
new file mode 100644
index 00000000000..50d8df4ab38
--- /dev/null
+++ b/apps/dav/lib/Command/GetAbsenceCommand.php
@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace OCA\DAV\Command;
+
+use OCA\DAV\Service\AbsenceService;
+use OCP\IUserManager;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class GetAbsenceCommand extends Command {
+
+ public function __construct(
+ private IUserManager $userManager,
+ private AbsenceService $absenceService,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this->setName('dav:absence:get');
+ $this->addArgument(
+ 'user-id',
+ InputArgument::REQUIRED,
+ 'User ID of the affected account'
+ );
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ $userId = $input->getArgument('user-id');
+
+ $user = $this->userManager->get($userId);
+ if ($user === null) {
+ $output->writeln('<error>User not found</error>');
+ return 1;
+ }
+
+ $absence = $this->absenceService->getAbsence($userId);
+ if ($absence === null) {
+ $output->writeln('<info>No absence set</info>');
+ return 0;
+ }
+
+ $output->writeln('<info>Start day:</info> ' . $absence->getFirstDay());
+ $output->writeln('<info>End day:</info> ' . $absence->getLastDay());
+ $output->writeln('<info>Short message:</info> ' . $absence->getStatus());
+ $output->writeln('<info>Message:</info> ' . $absence->getMessage());
+ $output->writeln('<info>Replacement user:</info> ' . ($absence->getReplacementUserId() ?? 'none'));
+ $output->writeln('<info>Replacement display name:</info> ' . ($absence->getReplacementUserDisplayName() ?? 'none'));
+
+ return 0;
+ }
+
+}
diff --git a/apps/dav/lib/Command/ListAddressbooks.php b/apps/dav/lib/Command/ListAddressbooks.php
new file mode 100644
index 00000000000..c0b6e63ccb8
--- /dev/null
+++ b/apps/dav/lib/Command/ListAddressbooks.php
@@ -0,0 +1,76 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Command;
+
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\CardDAV\SystemAddressbook;
+use OCP\IUserManager;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\Table;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ListAddressbooks extends Command {
+ public function __construct(
+ protected IUserManager $userManager,
+ private CardDavBackend $cardDavBackend,
+ ) {
+ parent::__construct('dav:list-addressbooks');
+ }
+
+ protected function configure(): void {
+ $this
+ ->setDescription('List all addressbooks of a user')
+ ->addArgument('uid',
+ InputArgument::REQUIRED,
+ 'User for whom all addressbooks will be listed');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $user = $input->getArgument('uid');
+ if (!$this->userManager->userExists($user)) {
+ throw new \InvalidArgumentException("User <$user> is unknown.");
+ }
+
+ $addressBooks = $this->cardDavBackend->getAddressBooksForUser("principals/users/$user");
+
+ $addressBookTableData = [];
+ foreach ($addressBooks as $book) {
+ // skip system / contacts integration address book
+ if ($book['uri'] === SystemAddressbook::URI_SHARED) {
+ continue;
+ }
+
+ $readOnly = false;
+ $readOnlyIndex = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
+ if (isset($book[$readOnlyIndex])) {
+ $readOnly = $book[$readOnlyIndex];
+ }
+
+ $addressBookTableData[] = [
+ $book['uri'],
+ $book['{DAV:}displayname'],
+ $book['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal'] ?? $book['principaluri'],
+ $book['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname'],
+ $readOnly ? ' x ' : ' ✓ ',
+ ];
+ }
+
+ if (count($addressBookTableData) > 0) {
+ $table = new Table($output);
+ $table->setHeaders(['Database ID', 'URI', 'Displayname', 'Owner principal', 'Owner displayname', 'Writable'])
+ ->setRows($addressBookTableData);
+
+ $table->render();
+ } else {
+ $output->writeln("<info>User <$user> has no addressbooks</info>");
+ }
+ return self::SUCCESS;
+ }
+}
diff --git a/apps/dav/lib/Command/ListCalendarShares.php b/apps/dav/lib/Command/ListCalendarShares.php
new file mode 100644
index 00000000000..2729bc80530
--- /dev/null
+++ b/apps/dav/lib/Command/ListCalendarShares.php
@@ -0,0 +1,131 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Command;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\Sharing\Backend;
+use OCA\DAV\Connector\Sabre\Principal;
+use OCA\DAV\DAV\Sharing\SharingMapper;
+use OCP\IUserManager;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\Table;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+#[AsCommand(
+ name: 'dav:list-calendar-shares',
+ description: 'List all calendar shares for a user',
+ hidden: false,
+)]
+class ListCalendarShares extends Command {
+ public function __construct(
+ private IUserManager $userManager,
+ private Principal $principal,
+ private CalDavBackend $caldav,
+ private SharingMapper $mapper,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this->addArgument(
+ 'uid',
+ InputArgument::REQUIRED,
+ 'User whose calendar shares will be listed'
+ );
+ $this->addOption(
+ 'calendar-id',
+ '',
+ InputOption::VALUE_REQUIRED,
+ 'List only shares for the given calendar id id',
+ null,
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $user = (string)$input->getArgument('uid');
+ if (!$this->userManager->userExists($user)) {
+ throw new \InvalidArgumentException("User $user is unknown");
+ }
+
+ $principal = $this->principal->getPrincipalByPath('principals/users/' . $user);
+ if ($principal === null) {
+ throw new \InvalidArgumentException("Unable to fetch principal for user $user");
+ }
+
+ $memberships = array_merge(
+ [$principal['uri']],
+ $this->principal->getGroupMembership($principal['uri']),
+ $this->principal->getCircleMembership($principal['uri']),
+ );
+
+ $shares = $this->mapper->getSharesByPrincipals($memberships, 'calendar');
+
+ $calendarId = $input->getOption('calendar-id');
+ if ($calendarId !== null) {
+ $shares = array_filter($shares, fn ($share) => $share['resourceid'] === (int)$calendarId);
+ }
+
+ $rows = array_map(fn ($share) => $this->formatCalendarShare($share), $shares);
+
+ if (count($rows) > 0) {
+ $table = new Table($output);
+ $table
+ ->setHeaders(['Share Id', 'Calendar Id', 'Calendar URI', 'Calendar Name', 'Calendar Owner', 'Access By', 'Permissions'])
+ ->setRows($rows)
+ ->render();
+ } else {
+ $output->writeln("User $user has no calendar shares");
+ }
+
+ return self::SUCCESS;
+ }
+
+ private function formatCalendarShare(array $share): array {
+ $calendarInfo = $this->caldav->getCalendarById($share['resourceid']);
+
+ $calendarUri = 'Resource not found';
+ $calendarName = '';
+ $calendarOwner = '';
+
+ if ($calendarInfo !== null) {
+ $calendarUri = $calendarInfo['uri'];
+ $calendarName = $calendarInfo['{DAV:}displayname'];
+ $calendarOwner = $calendarInfo['{http://nextcloud.com/ns}owner-displayname'] . ' (' . $calendarInfo['principaluri'] . ')';
+ }
+
+ $accessBy = match (true) {
+ str_starts_with($share['principaluri'], 'principals/users/') => 'Individual',
+ str_starts_with($share['principaluri'], 'principals/groups/') => 'Group (' . $share['principaluri'] . ')',
+ str_starts_with($share['principaluri'], 'principals/circles/') => 'Team (' . $share['principaluri'] . ')',
+ default => $share['principaluri'],
+ };
+
+ $permissions = match ($share['access']) {
+ Backend::ACCESS_READ => 'Read',
+ Backend::ACCESS_READ_WRITE => 'Read/Write',
+ Backend::ACCESS_UNSHARED => 'Unshare',
+ default => $share['access'],
+ };
+
+ return [
+ $share['id'],
+ $share['resourceid'],
+ $calendarUri,
+ $calendarName,
+ $calendarOwner,
+ $accessBy,
+ $permissions,
+ ];
+ }
+}
diff --git a/apps/dav/lib/Command/ListCalendars.php b/apps/dav/lib/Command/ListCalendars.php
index 35581c2d4b2..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;
@@ -35,24 +16,14 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ListCalendars extends Command {
-
- /** @var IUserManager */
- protected $userManager;
-
- /** @var CalDavBackend */
- private $caldav;
-
- /**
- * @param IUserManager $userManager
- * @param CalDavBackend $caldav
- */
- public function __construct(IUserManager $userManager, CalDavBackend $caldav) {
+ public function __construct(
+ protected IUserManager $userManager,
+ private CalDavBackend $caldav,
+ ) {
parent::__construct();
- $this->userManager = $userManager;
- $this->caldav = $caldav;
}
- protected function configure() {
+ protected function configure(): void {
$this
->setName('dav:list-calendars')
->setDescription('List all calendars of a user')
@@ -93,13 +64,13 @@ 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();
} else {
$output->writeln("<info>User <$user> has no calendars</info>");
}
- return 0;
+ return self::SUCCESS;
}
}
diff --git a/apps/dav/lib/Command/ListSubscriptions.php b/apps/dav/lib/Command/ListSubscriptions.php
new file mode 100644
index 00000000000..67753f25973
--- /dev/null
+++ b/apps/dav/lib/Command/ListSubscriptions.php
@@ -0,0 +1,77 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Command;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCP\IAppConfig;
+use OCP\IUserManager;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\Table;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+#[AsCommand(
+ name: 'dav:list-subscriptions',
+ description: 'List all calendar subscriptions for a user',
+ hidden: false,
+)]
+class ListSubscriptions extends Command {
+ public function __construct(
+ private IUserManager $userManager,
+ private IAppConfig $appConfig,
+ private CalDavBackend $caldav,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this->addArgument(
+ 'uid',
+ InputArgument::REQUIRED,
+ 'User whose calendar subscriptions will be listed'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $user = (string)$input->getArgument('uid');
+ if (!$this->userManager->userExists($user)) {
+ throw new \InvalidArgumentException("User $user is unknown");
+ }
+
+ $defaultRefreshRate = $this->appConfig->getValueString('dav', 'calendarSubscriptionRefreshRate', 'P1D');
+ $subscriptions = $this->caldav->getSubscriptionsForUser("principals/users/$user");
+ $rows = [];
+
+ foreach ($subscriptions as $subscription) {
+ $rows[] = [
+ $subscription['uri'],
+ $subscription['{DAV:}displayname'],
+ $subscription['{http://apple.com/ns/ical/}refreshrate'] ?? ($defaultRefreshRate . ' (default)'),
+ $subscription['source'],
+ ];
+ }
+
+ usort($rows, static fn (array $a, array $b) => $a[0] <=> $b[0]);
+
+ if (count($rows) > 0) {
+ $table = new Table($output);
+ $table
+ ->setHeaders(['URI', 'Displayname', 'Refresh rate', 'Source'])
+ ->setRows($rows)
+ ->render();
+ } else {
+ $output->writeln("User $user has no subscriptions");
+ }
+
+ return self::SUCCESS;
+ }
+}
diff --git a/apps/dav/lib/Command/MoveCalendar.php b/apps/dav/lib/Command/MoveCalendar.php
index 320fe8aeac6..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;
@@ -42,61 +22,23 @@ use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class MoveCalendar extends Command {
-
- /** @var IUserManager */
- private $userManager;
-
- /** @var IGroupManager */
- private $groupManager;
-
- /** @var IShareManager */
- private $shareManager;
-
- /** @var IConfig $config */
- private $config;
-
- /** @var IL10N */
- private $l10n;
-
- /** @var SymfonyStyle */
- private $io;
-
- /** @var CalDavBackend */
- private $calDav;
-
- /** @var LoggerInterface */
- private $logger;
+ private ?SymfonyStyle $io = null;
public const URI_USERS = 'principals/users/';
- /**
- * @param IUserManager $userManager
- * @param IGroupManager $groupManager
- * @param IShareManager $shareManager
- * @param IConfig $config
- * @param IL10N $l10n
- * @param CalDavBackend $calDav
- */
public function __construct(
- IUserManager $userManager,
- IGroupManager $groupManager,
- IShareManager $shareManager,
- IConfig $config,
- IL10N $l10n,
- CalDavBackend $calDav,
- LoggerInterface $logger
+ private IUserManager $userManager,
+ private IGroupManager $groupManager,
+ private IShareManager $shareManager,
+ private IConfig $config,
+ private IL10N $l10n,
+ private CalDavBackend $calDav,
+ private LoggerInterface $logger,
) {
parent::__construct();
- $this->userManager = $userManager;
- $this->groupManager = $groupManager;
- $this->shareManager = $shareManager;
- $this->config = $config;
- $this->l10n = $l10n;
- $this->calDav = $calDav;
- $this->logger = $logger;
}
- protected function configure() {
+ protected function configure(): void {
$this
->setName('dav:move-calendar')
->setDescription('Move a calendar from an user to another')
@@ -109,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 {
@@ -131,7 +73,7 @@ class MoveCalendar extends Command {
$calendar = $this->calDav->getCalendarByUri(self::URI_USERS . $userOrigin, $name);
- if (null === $calendar) {
+ if ($calendar === null) {
throw new \InvalidArgumentException("User <$userOrigin> has no calendar named <$name>. You can run occ dav:list-calendars to list calendars URIs for this user.");
}
@@ -156,35 +98,27 @@ 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\""
]);
}
$this->calDav->moveCalendar($name, self::URI_USERS . $userOrigin, self::URI_USERS . $userDestination, $newName);
$this->io->success("Calendar <$name> was moved from user <$userOrigin> to <$userDestination>" . ($newName ? " as <$newName>" : ''));
- return 0;
+ return self::SUCCESS;
}
/**
* Check if the calendar exists for user
- *
- * @param string $userDestination
- * @param string $name
- * @return bool
*/
protected function calendarExists(string $userDestination, string $name): bool {
- return null !== $this->calDav->getCalendarByUri(self::URI_USERS . $userDestination, $name);
+ return $this->calDav->getCalendarByUri(self::URI_USERS . $userDestination, $name) !== null;
}
/**
* Try to find a suitable new calendar name that
- * doesn't exists for the provided user
- *
- * @param string $userDestination
- * @param string $name
- * @return string
+ * doesn't exist for the provided user
*/
protected function getNewCalendarName(string $userDestination, string $name): string {
$increment = 1;
@@ -206,10 +140,6 @@ class MoveCalendar extends Command {
/**
* Check that moving the calendar won't break shares
*
- * @param array $calendar
- * @param string $userOrigin
- * @param string $userDestination
- * @param bool $force
* @return bool had any shares or not
* @throws \InvalidArgumentException
*/
@@ -222,11 +152,11 @@ class MoveCalendar extends Command {
* Check that user destination is member of the groups which whom the calendar was shared
* If we ask to force the migration, the share with the group is dropped
*/
- if ($this->shareManager->shareWithGroupMembersOnly() === true && 'groups' === $prefix && !$this->groupManager->isInGroup($userDestination, $userOrGroup)) {
+ if ($this->shareManager->shareWithGroupMembersOnly() === true && $prefix === 'groups' && !$this->groupManager->isInGroup($userDestination, $userOrGroup)) {
if ($force) {
- $this->calDav->updateShares(new Calendar($this->calDav, $calendar, $this->l10n, $this->config, $this->logger), [], ['href' => 'principal:principals/groups/' . $userOrGroup]);
+ $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.');
}
}
@@ -235,9 +165,9 @@ class MoveCalendar extends Command {
*/
if ($userOrGroup === $userDestination) {
if ($force) {
- $this->calDav->updateShares(new Calendar($this->calDav, $calendar, $this->l10n, $this->config, $this->logger), [], ['href' => 'principal:principals/users/' . $userOrGroup]);
+ $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 4f9e4836a72..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;
@@ -37,21 +21,14 @@ use Symfony\Component\Console\Output\OutputInterface;
* have no matching principal. Happened because of a bug in the calendar app.
*/
class RemoveInvalidShares extends Command {
-
- /** @var IDBConnection */
- private $connection;
- /** @var Principal */
- private $principalBackend;
-
- public function __construct(IDBConnection $connection,
- Principal $principalBackend) {
+ public function __construct(
+ private IDBConnection $connection,
+ private Principal $principalBackend,
+ ) {
parent::__construct();
-
- $this->connection = $connection;
- $this->principalBackend = $principalBackend;
}
- protected function configure() {
+ protected function configure(): void {
$this
->setName('dav:remove-invalid-shares')
->setDescription('Remove invalid dav shares');
@@ -61,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'];
@@ -72,16 +49,16 @@ class RemoveInvalidShares extends Command {
}
$result->closeCursor();
- return 0;
+ return self::SUCCESS;
}
/**
* @param string $principaluri
*/
- private function deleteSharesForPrincipal($principaluri) {
+ private function deleteSharesForPrincipal($principaluri): void {
$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 c9beabc974a..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;
@@ -31,18 +14,15 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class RetentionCleanupCommand extends Command {
- /** @var RetentionService */
- private $service;
-
- public function __construct(RetentionService $service) {
+ public function __construct(
+ private RetentionService $service,
+ ) {
parent::__construct('dav:retention:clean-up');
-
- $this->service = $service;
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$this->service->cleanUp();
- return 0;
+ return self::SUCCESS;
}
}
diff --git a/apps/dav/lib/Command/SendEventReminders.php b/apps/dav/lib/Command/SendEventReminders.php
index 697248d71a0..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;
@@ -35,22 +18,11 @@ use Symfony\Component\Console\Output\OutputInterface;
* @package OCA\DAV\Command
*/
class SendEventReminders extends Command {
-
- /** @var ReminderService */
- protected $reminderService;
-
- /** @var IConfig */
- protected $config;
-
- /**
- * @param ReminderService $reminderService
- * @param IConfig $config
- */
- public function __construct(ReminderService $reminderService,
- IConfig $config) {
+ public function __construct(
+ protected ReminderService $reminderService,
+ protected IConfig $config,
+ ) {
parent::__construct();
- $this->reminderService = $reminderService;
- $this->config = $config;
}
/**
@@ -62,24 +34,20 @@ class SendEventReminders extends Command {
->setDescription('Sends event reminders');
}
- /**
- * @param InputInterface $input
- * @param OutputInterface $output
- */
protected function execute(InputInterface $input, OutputInterface $output): int {
if ($this->config->getAppValue('dav', 'sendEventReminders', 'yes') !== 'yes') {
$output->writeln('<error>Sending event reminders disabled!</error>');
$output->writeln('<info>Please run "php occ config:app:set dav sendEventReminders --value yes"');
- return 1;
+ return self::FAILURE;
}
if ($this->config->getAppValue('dav', 'sendEventRemindersMode', 'backgroundjob') !== 'occ') {
$output->writeln('<error>Sending event reminders mode set to background-job!</error>');
$output->writeln('<info>Please run "php occ config:app:set dav sendEventRemindersMode --value occ"');
- return 1;
+ return self::FAILURE;
}
$this->reminderService->processReminders();
- return 0;
+ return self::SUCCESS;
}
}
diff --git a/apps/dav/lib/Command/SetAbsenceCommand.php b/apps/dav/lib/Command/SetAbsenceCommand.php
new file mode 100644
index 00000000000..bf91a163f95
--- /dev/null
+++ b/apps/dav/lib/Command/SetAbsenceCommand.php
@@ -0,0 +1,95 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace OCA\DAV\Command;
+
+use OCA\DAV\Service\AbsenceService;
+use OCP\IUserManager;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class SetAbsenceCommand extends Command {
+
+ public function __construct(
+ private IUserManager $userManager,
+ private AbsenceService $absenceService,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this->setName('dav:absence:set');
+ $this->addArgument(
+ 'user-id',
+ InputArgument::REQUIRED,
+ 'User ID of the affected account'
+ );
+ $this->addArgument(
+ 'first-day',
+ InputArgument::REQUIRED,
+ 'Inclusive start day formatted as YYYY-MM-DD'
+ );
+ $this->addArgument(
+ 'last-day',
+ InputArgument::REQUIRED,
+ 'Inclusive end day formatted as YYYY-MM-DD'
+ );
+ $this->addArgument(
+ 'short-message',
+ InputArgument::REQUIRED,
+ 'Short message'
+ );
+ $this->addArgument(
+ 'message',
+ InputArgument::REQUIRED,
+ 'Message'
+ );
+ $this->addArgument(
+ 'replacement-user-id',
+ InputArgument::OPTIONAL,
+ 'Replacement user id'
+ );
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ $userId = $input->getArgument('user-id');
+
+ $user = $this->userManager->get($userId);
+ if ($user === null) {
+ $output->writeln('<error>User not found</error>');
+ return 1;
+ }
+
+ $replacementUserId = $input->getArgument('replacement-user-id');
+ if ($replacementUserId === null) {
+ $replacementUser = null;
+ } else {
+ $replacementUser = $this->userManager->get($replacementUserId);
+ if ($replacementUser === null) {
+ $output->writeln('<error>Replacement user not found</error>');
+ return 2;
+ }
+ }
+
+ $this->absenceService->createOrUpdateAbsence(
+ $user,
+ $input->getArgument('first-day'),
+ $input->getArgument('last-day'),
+ $input->getArgument('short-message'),
+ $input->getArgument('message'),
+ $replacementUser?->getUID(),
+ $replacementUser?->getDisplayName(),
+ );
+
+ return 0;
+ }
+
+}
diff --git a/apps/dav/lib/Command/SyncBirthdayCalendar.php b/apps/dav/lib/Command/SyncBirthdayCalendar.php
index 6de5357bfde..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;
@@ -35,30 +18,15 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class SyncBirthdayCalendar extends Command {
-
- /** @var BirthdayService */
- private $birthdayService;
-
- /** @var IConfig */
- private $config;
-
- /** @var IUserManager */
- private $userManager;
-
- /**
- * @param IUserManager $userManager
- * @param IConfig $config
- * @param BirthdayService $birthdayService
- */
- public function __construct(IUserManager $userManager, IConfig $config,
- BirthdayService $birthdayService) {
+ public function __construct(
+ private IUserManager $userManager,
+ private IConfig $config,
+ private BirthdayService $birthdayService,
+ ) {
parent::__construct();
- $this->birthdayService = $birthdayService;
- $this->config = $config;
- $this->userManager = $userManager;
}
- protected function configure() {
+ protected function configure(): void {
$this
->setName('dav:sync-birthday-calendar')
->setDescription('Synchronizes the birthday calendar')
@@ -67,10 +35,6 @@ class SyncBirthdayCalendar extends Command {
'User for whom the birthday calendar will be synchronized');
}
- /**
- * @param InputInterface $input
- * @param OutputInterface $output
- */
protected function execute(InputInterface $input, OutputInterface $output): int {
$this->verifyEnabled();
@@ -89,12 +53,12 @@ class SyncBirthdayCalendar extends Command {
$output->writeln("Start birthday calendar sync for $user");
$this->birthdayService->syncUser($user);
- return 0;
+ 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();
@@ -109,10 +73,10 @@ class SyncBirthdayCalendar extends Command {
$p->finish();
$output->writeln('');
- return 0;
+ return self::SUCCESS;
}
- protected function verifyEnabled() {
+ protected function verifyEnabled(): void {
$isEnabled = $this->config->getAppValue('dav', 'generateBirthdayCalendar', 'yes');
if ($isEnabled !== 'yes') {
diff --git a/apps/dav/lib/Command/SyncSystemAddressBook.php b/apps/dav/lib/Command/SyncSystemAddressBook.php
index 272cca5a08e..54edba01e05 100644
--- a/apps/dav/lib/Command/SyncSystemAddressBook.php
+++ b/apps/dav/lib/Command/SyncSystemAddressBook.php
@@ -1,67 +1,44 @@
<?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;
use OCA\DAV\CardDAV\SyncService;
+use OCP\IConfig;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class SyncSystemAddressBook extends Command {
-
- /** @var SyncService */
- private $syncService;
-
- /**
- * @param SyncService $syncService
- */
- public function __construct(SyncService $syncService) {
+ public function __construct(
+ private SyncService $syncService,
+ private IConfig $config,
+ ) {
parent::__construct();
- $this->syncService = $syncService;
}
- protected function configure() {
+ protected function configure(): void {
$this
->setName('dav:sync-system-addressbook')
->setDescription('Synchronizes users to the system addressbook');
}
- /**
- * @param InputInterface $input
- * @param OutputInterface $output
- */
protected function execute(InputInterface $input, OutputInterface $output): int {
$output->writeln('Syncing users ...');
$progress = new ProgressBar($output);
$progress->start();
- $this->syncService->syncInstance(function () use ($progress) {
+ $this->syncService->syncInstance(function () use ($progress): void {
$progress->advance();
});
$progress->finish();
$output->writeln('');
- return 0;
+ $this->config->setAppValue('dav', 'needs_system_address_book_sync', 'no');
+ return self::SUCCESS;
}
}
diff --git a/apps/dav/lib/Comments/CommentNode.php b/apps/dav/lib/Comments/CommentNode.php
index af76027671e..5dbefa82d93 100644
--- a/apps/dav/lib/Comments/CommentNode.php
+++ b/apps/dav/lib/Comments/CommentNode.php
@@ -1,34 +1,18 @@
<?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;
use OCP\Comments\IComment;
use OCP\Comments\ICommentsManager;
use OCP\Comments\MessageTooLongException;
-use OCP\ILogger;
use OCP\IUserManager;
use OCP\IUserSession;
+use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\MethodNotAllowed;
@@ -46,57 +30,30 @@ 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;
-
- /** @var ILogger */
- protected $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.
- *
- * @param ICommentsManager $commentsManager
- * @param IComment $comment
- * @param IUserManager $userManager
- * @param IUserSession $userSession
- * @param ILogger $logger
*/
public function __construct(
- ICommentsManager $commentsManager,
- IComment $comment,
- IUserManager $userManager,
- IUserSession $userSession,
- ILogger $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 strpos($name, 'get') === 0;
+ return str_starts_with($name, 'get');
});
foreach ($methods as $getter) {
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;
}
/**
@@ -172,10 +129,8 @@ class CommentNode implements \Sabre\DAV\INode, \Sabre\DAV\IProperties {
/**
* Returns the last modification time, as a unix timestamp
- *
- * @return int
*/
- public function getLastModified() {
+ public function getLastModified(): ?int {
return null;
}
@@ -194,7 +149,7 @@ class CommentNode implements \Sabre\DAV\INode, \Sabre\DAV\IProperties {
$this->commentsManager->save($this->comment);
return true;
} catch (\Exception $e) {
- $this->logger->logException($e, ['app' => 'dav/comments']);
+ $this->logger->error($e->getMessage(), ['app' => 'dav/comments', 'exception' => $e]);
if ($e instanceof MessageTooLongException) {
$msg = 'Message exceeds allowed character limit of ';
throw new BadRequest($msg . IComment::MAX_MESSAGE_LENGTH, 0, $e);
@@ -287,7 +242,7 @@ class CommentNode implements \Sabre\DAV\INode, \Sabre\DAV\IProperties {
try {
$displayName = $this->commentsManager->resolveDisplayName($mention['type'], $mention['id']);
} catch (\OutOfBoundsException $e) {
- $this->logger->logException($e);
+ $this->logger->error($e->getMessage(), ['exception' => $e]);
// No displayname, upon client's discretion what to display.
$displayName = '';
}
diff --git a/apps/dav/lib/Comments/CommentsPlugin.php b/apps/dav/lib/Comments/CommentsPlugin.php
index a4932751897..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,
+ ) {
}
/**
@@ -85,13 +64,13 @@ class CommentsPlugin extends ServerPlugin {
*/
public function initialize(Server $server) {
$this->server = $server;
- if (strpos($this->server->getRequestUri(), 'comments/') !== 0) {
+ if (!str_starts_with($this->server->getRequestUri(), 'comments/')) {
return;
}
$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;
}
@@ -176,7 +155,7 @@ class CommentsPlugin extends ServerPlugin {
}
if (!is_null($args['datetime'])) {
- $args['datetime'] = new \DateTime($args['datetime']);
+ $args['datetime'] = new \DateTime((string)$args['datetime']);
}
$results = $node->findChildren($args['limit'], $args['offset'], $args['datetime']);
@@ -189,7 +168,7 @@ class CommentsPlugin extends ServerPlugin {
$responses[] = new Response(
$this->server->getBaseUri() . $nodePath,
[200 => $resultSet[0][200]],
- 200
+ '200'
);
}
}
@@ -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);
@@ -220,7 +199,7 @@ class CommentsPlugin extends ServerPlugin {
*/
private function createComment($objectType, $objectId, $data, $contentType = 'application/json') {
if (explode(';', $contentType)[0] === 'application/json') {
- $data = json_decode($data, true);
+ $data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
} else {
throw new UnsupportedMediaType();
}
@@ -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 d9b06e1240c..33c58ee44d2 100644
--- a/apps/dav/lib/Comments/EntityCollection.php
+++ b/apps/dav/lib/Comments/EntityCollection.php
@@ -1,33 +1,17 @@
<?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;
use OCP\Comments\ICommentsManager;
use OCP\Comments\NotFoundException;
-use OCP\ILogger;
use OCP\IUserManager;
use OCP\IUserSession;
+use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\IProperties;
use Sabre\DAV\PropPatch;
@@ -43,27 +27,21 @@ 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;
-
- /** @var ILogger */
- protected $logger;
-
/**
* @param string $id
* @param string $name
* @param ICommentsManager $commentsManager
* @param IUserManager $userManager
* @param IUserSession $userSession
- * @param ILogger $logger
+ * @param LoggerInterface $logger
*/
public function __construct(
- $id,
+ protected $id,
$name,
ICommentsManager $commentsManager,
IUserManager $userManager,
IUserSession $userSession,
- ILogger $logger
+ protected LoggerInterface $logger,
) {
foreach (['id', 'name'] as $property) {
$$property = trim($$property);
@@ -71,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;
}
@@ -131,7 +107,7 @@ class EntityCollection extends RootCollection implements IProperties {
* @param \DateTime|null $datetime
* @return CommentNode[]
*/
- public function findChildren($limit = 0, $offset = 0, \DateTime $datetime = null) {
+ public function findChildren($limit = 0, $offset = 0, ?\DateTime $datetime = null) {
$comments = $this->commentsManager->getForObject($this->name, $this->id, $limit, $offset, $datetime);
$result = [];
foreach ($comments as $comment) {
@@ -163,12 +139,9 @@ class EntityCollection extends RootCollection implements IProperties {
/**
* Sets the read marker to the specified date for the logged in user
- *
- * @param \DateTime $value
- * @return bool
*/
- public function setReadMarker($value) {
- $dateTime = new \DateTime($value);
+ public function setReadMarker(?string $value): bool {
+ $dateTime = new \DateTime($value ?? 'now');
$user = $this->userSession->getUser();
$this->commentsManager->setReadMark($this->name, $this->id, $dateTime, $user);
return true;
diff --git a/apps/dav/lib/Comments/EntityTypeCollection.php b/apps/dav/lib/Comments/EntityTypeCollection.php
index c9df2a068d7..1c8533ca375 100644
--- a/apps/dav/lib/Comments/EntityTypeCollection.php
+++ b/apps/dav/lib/Comments/EntityTypeCollection.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>
- *
- * @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\Comments\ICommentsManager;
-use OCP\ILogger;
use OCP\IUserManager;
use OCP\IUserSession;
+use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\MethodNotAllowed;
use Sabre\DAV\Exception\NotFound;
@@ -42,42 +26,21 @@ use Sabre\DAV\Exception\NotFound;
* @package OCA\DAV\Comments
*/
class EntityTypeCollection extends RootCollection {
-
- /** @var ILogger */
- protected $logger;
-
- /** @var IUserManager */
- protected $userManager;
-
- /** @var \Closure */
- protected $childExistsFunction;
-
- /**
- * @param string $name
- * @param ICommentsManager $commentsManager
- * @param IUserManager $userManager
- * @param IUserSession $userSession
- * @param ILogger $logger
- * @param \Closure $childExistsFunction
- */
public function __construct(
- $name,
+ string $name,
ICommentsManager $commentsManager,
- IUserManager $userManager,
+ protected IUserManager $userManager,
IUserSession $userSession,
- ILogger $logger,
- \Closure $childExistsFunction
+ protected LoggerInterface $logger,
+ protected \Closure $childExistsFunction,
) {
$name = trim($name);
- if (empty($name) || !is_string($name)) {
+ if (empty($name)) {
throw new \InvalidArgumentException('"name" parameter must be non-empty string');
}
$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 e8e890696eb..493d73ec531 100644
--- a/apps/dav/lib/Comments/RootCollection.php
+++ b/apps/dav/lib/Comments/RootCollection.php
@@ -1,81 +1,35 @@
<?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;
use OCP\Comments\CommentsEntityEvent;
use OCP\Comments\ICommentsManager;
-use OCP\ILogger;
+use OCP\EventDispatcher\IEventDispatcher;
use OCP\IUserManager;
use OCP\IUserSession;
+use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotAuthenticated;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\ICollection;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class RootCollection implements ICollection {
-
/** @var EntityTypeCollection[]|null */
- private $entityTypeCollections;
-
- /** @var ICommentsManager */
- protected $commentsManager;
-
- /** @var string */
- protected $name = 'comments';
-
- /** @var ILogger */
- protected $logger;
+ private ?array $entityTypeCollections = null;
+ protected string $name = 'comments';
- /** @var IUserManager */
- protected $userManager;
-
- /** @var IUserSession */
- protected $userSession;
-
- /** @var EventDispatcherInterface */
- protected $dispatcher;
-
- /**
- * @param ICommentsManager $commentsManager
- * @param IUserManager $userManager
- * @param IUserSession $userSession
- * @param EventDispatcherInterface $dispatcher
- * @param ILogger $logger
- */
public function __construct(
- ICommentsManager $commentsManager,
- IUserManager $userManager,
- IUserSession $userSession,
- EventDispatcherInterface $dispatcher,
- ILogger $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,
+ ) {
}
/**
@@ -94,7 +48,8 @@ class RootCollection implements ICollection {
throw new NotAuthenticated();
}
- $event = new CommentsEntityEvent(CommentsEntityEvent::EVENT_ENTITY);
+ $event = new CommentsEntityEvent();
+ $this->dispatcher->dispatchTyped($event);
$this->dispatcher->dispatch(CommentsEntityEvent::EVENT_ENTITY, $event);
$this->entityTypeCollections = [];
@@ -157,6 +112,7 @@ class RootCollection implements ICollection {
*/
public function getChildren() {
$this->initCollections();
+ assert(!is_null($this->entityTypeCollections));
return $this->entityTypeCollections;
}
@@ -168,6 +124,7 @@ class RootCollection implements ICollection {
*/
public function childExists($name) {
$this->initCollections();
+ assert(!is_null($this->entityTypeCollections));
return isset($this->entityTypeCollections[$name]);
}
@@ -204,7 +161,7 @@ class RootCollection implements ICollection {
/**
* Returns the last modification time, as a unix timestamp
*
- * @return int
+ * @return ?int
*/
public function getLastModified() {
return null;
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
new file mode 100644
index 00000000000..03d18853de0
--- /dev/null
+++ b/apps/dav/lib/Connector/LegacyPublicAuth.php
@@ -0,0 +1,102 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Connector;
+
+use OCA\DAV\Connector\Sabre\PublicAuth;
+use OCP\Defaults;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\Security\Bruteforce\IThrottler;
+use OCP\Share\Exceptions\ShareNotFound;
+use OCP\Share\IManager;
+use OCP\Share\IShare;
+use Sabre\DAV\Auth\Backend\AbstractBasic;
+
+/**
+ * Class PublicAuth
+ *
+ * @package OCA\DAV\Connector
+ */
+class LegacyPublicAuth extends AbstractBasic {
+ private const BRUTEFORCE_ACTION = 'legacy_public_webdav_auth';
+
+ private ?IShare $share = null;
+
+ public function __construct(
+ private IRequest $request,
+ private IManager $shareManager,
+ private ISession $session,
+ private IThrottler $throttler,
+ ) {
+ // setup realm
+ $defaults = new Defaults();
+ $this->realm = $defaults->getName() ?: 'Nextcloud';
+ }
+
+ /**
+ * Validates a username and password
+ *
+ * This method should return true or false depending on if login
+ * succeeded.
+ *
+ * @param string $username
+ * @param string $password
+ *
+ * @return bool
+ * @throws \Sabre\DAV\Exception\NotAuthenticated
+ */
+ protected function validateUserPass($username, $password) {
+ $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION);
+
+ try {
+ $share = $this->shareManager->getShareByToken($username);
+ } 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
+ if ($share->getPassword() !== null) {
+ if ($share->getShareType() === IShare::TYPE_LINK
+ || $share->getShareType() === IShare::TYPE_EMAIL
+ || $share->getShareType() === IShare::TYPE_CIRCLE) {
+ if ($this->shareManager->checkPassword($share, $password)) {
+ return true;
+ } elseif ($this->session->exists(PublicAuth::DAV_AUTHENTICATED)
+ && $this->session->get(PublicAuth::DAV_AUTHENTICATED) === $share->getId()) {
+ return true;
+ } else {
+ if (in_array('XMLHttpRequest', explode(',', $this->request->getHeader('X-Requested-With')))) {
+ // do not re-authenticate over ajax, use dummy auth name to prevent browser popup
+ http_response_code(401);
+ header('WWW-Authenticate: DummyBasic realm="' . $this->realm . '"');
+ throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls');
+ }
+
+ $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
+ return false;
+ }
+ } elseif ($share->getShareType() === IShare::TYPE_REMOTE) {
+ return true;
+ } else {
+ $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public function getShare(): IShare {
+ assert($this->share !== null);
+ return $this->share;
+ }
+}
diff --git a/apps/dav/lib/Connector/PublicAuth.php b/apps/dav/lib/Connector/PublicAuth.php
deleted file mode 100644
index 426cbf871d7..00000000000
--- a/apps/dav/lib/Connector/PublicAuth.php
+++ /dev/null
@@ -1,147 +0,0 @@
-<?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/>
- *
- */
-namespace OCA\DAV\Connector;
-
-use OC\Security\Bruteforce\Throttler;
-use OCP\IRequest;
-use OCP\ISession;
-use OCP\Share\Exceptions\ShareNotFound;
-use OCP\Share\IManager;
-use OCP\Share\IShare;
-use Sabre\DAV\Auth\Backend\AbstractBasic;
-
-/**
- * Class PublicAuth
- *
- * @package OCA\DAV\Connector
- */
-class PublicAuth extends AbstractBasic {
- private const BRUTEFORCE_ACTION = 'public_webdav_auth';
-
- /** @var \OCP\Share\IShare */
- private $share;
-
- /** @var IManager */
- private $shareManager;
-
- /** @var ISession */
- private $session;
-
- /** @var IRequest */
- private $request;
-
- /** @var Throttler */
- private $throttler;
-
- /**
- * @param IRequest $request
- * @param IManager $shareManager
- * @param ISession $session
- * @param Throttler $throttler
- */
- public function __construct(IRequest $request,
- IManager $shareManager,
- ISession $session,
- Throttler $throttler) {
- $this->request = $request;
- $this->shareManager = $shareManager;
- $this->session = $session;
- $this->throttler = $throttler;
-
- // setup realm
- $defaults = new \OCP\Defaults();
- $this->realm = $defaults->getName();
- }
-
- /**
- * Validates a username and password
- *
- * This method should return true or false depending on if login
- * succeeded.
- *
- * @param string $username
- * @param string $password
- *
- * @return bool
- * @throws \Sabre\DAV\Exception\NotAuthenticated
- */
- protected function validateUserPass($username, $password) {
- $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION);
-
- try {
- $share = $this->shareManager->getShareByToken($username);
- } 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
- if ($share->getPassword() !== null) {
- if ($share->getShareType() === IShare::TYPE_LINK
- || $share->getShareType() === IShare::TYPE_EMAIL
- || $share->getShareType() === IShare::TYPE_CIRCLE) {
- if ($this->shareManager->checkPassword($share, $password)) {
- return true;
- } elseif ($this->session->exists('public_link_authenticated')
- && $this->session->get('public_link_authenticated') === (string)$share->getId()) {
- return true;
- } else {
- if (in_array('XMLHttpRequest', explode(',', $this->request->getHeader('X-Requested-With')))) {
- // do not re-authenticate over ajax, use dummy auth name to prevent browser popup
- http_response_code(401);
- header('WWW-Authenticate: DummyBasic realm="' . $this->realm . '"');
- throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls');
- }
-
- $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
- return false;
- }
- } elseif ($share->getShareType() === IShare::TYPE_REMOTE) {
- return true;
- } else {
- $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
- return false;
- }
- } else {
- return true;
- }
- }
-
- /**
- * @return \OCP\Share\IShare
- */
- public function getShare() {
- return $this->share;
- }
-}
diff --git a/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php b/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php
index 6c3600fa5eb..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;
@@ -54,7 +34,7 @@ class AnonymousOptionsPlugin extends ServerPlugin {
* @return bool
*/
public function isRequestInRoot($path) {
- return $path === '' || (is_string($path) && strpos($path, '/') === false);
+ return $path === '' || (is_string($path) && !str_contains($path, '/'));
}
/**
diff --git a/apps/dav/lib/Connector/Sabre/AppEnabledPlugin.php b/apps/dav/lib/Connector/Sabre/AppEnabledPlugin.php
deleted file mode 100644
index 244e5de0683..00000000000
--- a/apps/dav/lib/Connector/Sabre/AppEnabledPlugin.php
+++ /dev/null
@@ -1,88 +0,0 @@
-<?php
-/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @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/>
- *
- */
-namespace OCA\DAV\Connector\Sabre;
-
-use OCP\App\IAppManager;
-use Sabre\DAV\Exception\Forbidden;
-use Sabre\DAV\ServerPlugin;
-
-/**
- * Plugin to check if an app is enabled for the current user
- */
-class AppEnabledPlugin extends ServerPlugin {
-
- /**
- * Reference to main server object
- *
- * @var \Sabre\DAV\Server
- */
- private $server;
-
- /**
- * @var string
- */
- private $app;
-
- /**
- * @var \OCP\App\IAppManager
- */
- private $appManager;
-
- /**
- * @param string $app
- * @param \OCP\App\IAppManager $appManager
- */
- public function __construct($app, IAppManager $appManager) {
- $this->app = $app;
- $this->appManager = $appManager;
- }
-
- /**
- * 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.
- *
- * @param \Sabre\DAV\Server $server
- * @return void
- */
- public function initialize(\Sabre\DAV\Server $server) {
- $this->server = $server;
- $this->server->on('beforeMethod:*', [$this, 'checkAppEnabled'], 30);
- }
-
- /**
- * This method is called before any HTTP after auth and checks if the user has access to the app
- *
- * @throws \Sabre\DAV\Exception\Forbidden
- * @return bool
- */
- public function checkAppEnabled() {
- if (!$this->appManager->isEnabledForUser($this->app)) {
- throw new Forbidden();
- }
- }
-}
diff --git a/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php b/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php
new file mode 100644
index 00000000000..9cff113140a
--- /dev/null
+++ b/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php
@@ -0,0 +1,112 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Connector\Sabre;
+
+use Sabre\DAV\Server;
+use Sabre\DAV\ServerPlugin;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+
+/**
+ * A plugin which tries to work-around peculiarities of the MacOS DAV client
+ * apps. The following problems are addressed:
+ *
+ * - OSX calendar client sends REPORT requests to a random principal
+ * collection but expects to find all principals (forgot to set
+ * {DAV:}principal-property-search flag?)
+ */
+class AppleQuirksPlugin extends ServerPlugin {
+
+ /*
+ private const OSX_CALENDAR_AGENT = 'CalendarAgent';
+ private const OSX_DATAACCESSD_AGENT = 'dataaccessd';
+ private const OSX_ACCOUNTSD_AGENT = 'accountsd';
+ private const OSX_CONTACTS_AGENT = 'AddressBookCore';
+ */
+
+ private const OSX_AGENT_PREFIX = 'macOS';
+
+ /** @var bool */
+ private $isMacOSDavAgent = false;
+
+ /**
+ * Sets up the plugin.
+ *
+ * This method is automatically called by the server class.
+ *
+ * @return void
+ */
+ public function initialize(Server $server) {
+ $server->on('beforeMethod:REPORT', [$this, 'beforeReport'], 0);
+ $server->on('report', [$this, 'report'], 0);
+ }
+
+ /**
+ * Triggered before any method is handled.
+ *
+ * @return void
+ */
+ public function beforeReport(RequestInterface $request, ResponseInterface $response) {
+ $userAgent = $request->getRawServerValue('HTTP_USER_AGENT') ?? 'unknown';
+ $this->isMacOSDavAgent = $this->isMacOSUserAgent($userAgent);
+ }
+
+ /**
+ * This method handles HTTP REPORT requests.
+ *
+ * @param string $reportName
+ * @param mixed $report
+ * @param mixed $path
+ *
+ * @return bool
+ */
+ public function report($reportName, $report, $path) {
+ if ($reportName == '{DAV:}principal-property-search' && $this->isMacOSDavAgent) {
+ /** @var \Sabre\DAVACL\Xml\Request\PrincipalPropertySearchReport $report */
+ $report->applyToPrincipalCollectionSet = true;
+ }
+ return true;
+ }
+
+ /**
+ * Check whether the given $userAgent string pretends to originate from OSX.
+ *
+ * @param string $userAgent
+ *
+ * @return bool
+ */
+ protected function isMacOSUserAgent(string $userAgent):bool {
+ return str_starts_with(self::OSX_AGENT_PREFIX, $userAgent);
+ }
+
+ /**
+ * Decode the given OSX DAV agent string.
+ *
+ * @param string $agent
+ *
+ * @return null|array
+ */
+ protected function decodeMacOSAgentString(string $userAgent):?array {
+ // OSX agent string is like: macOS/13.2.1 (22D68) dataaccessd/1.0
+ if (preg_match('|^' . self::OSX_AGENT_PREFIX . '/([0-9]+)\\.([0-9]+)\\.([0-9]+)\s+\((\w+)\)\s+([^/]+)/([0-9]+)(?:\\.([0-9]+))?(?:\\.([0-9]+))?$|i', $userAgent, $matches)) {
+ return [
+ 'macOSVersion' => [
+ 'major' => $matches[1],
+ 'minor' => $matches[2],
+ 'patch' => $matches[3],
+ ],
+ 'macOSAgent' => $matches[5],
+ 'macOSAgentVersion' => [
+ 'major' => $matches[6],
+ 'minor' => $matches[7] ?? null,
+ 'patch' => $matches[8] ?? null,
+ ],
+ ];
+ }
+ return null;
+ }
+}
diff --git a/apps/dav/lib/Connector/Sabre/Auth.php b/apps/dav/lib/Connector/Sabre/Auth.php
index df4e3c65ce0..a174920946a 100644
--- a/apps/dav/lib/Connector/Sabre/Auth.php
+++ b/apps/dav/lib/Connector/Sabre/Auth.php
@@ -1,46 +1,26 @@
<?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;
use Exception;
use OC\Authentication\Exceptions\PasswordLoginForbiddenException;
use OC\Authentication\TwoFactorAuth\Manager;
-use OC\Security\Bruteforce\Throttler;
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;
use Sabre\DAV\Exception\ServiceUnavailable;
@@ -49,44 +29,21 @@ use Sabre\HTTP\ResponseInterface;
class Auth extends AbstractBasic {
public const DAV_AUTHENTICATED = 'AUTHENTICATED_TO_DAV_BACKEND';
-
- /** @var ISession */
- private $session;
- /** @var Session */
- private $userSession;
- /** @var IRequest */
- private $request;
- /** @var string */
- private $currentUser;
- /** @var Manager */
- private $twoFactorManager;
- /** @var Throttler */
- private $throttler;
-
- /**
- * @param ISession $session
- * @param Session $userSession
- * @param IRequest $request
- * @param Manager $twoFactorManager
- * @param Throttler $throttler
- * @param string $principalPrefix
- */
- public function __construct(ISession $session,
- Session $userSession,
- IRequest $request,
- Manager $twoFactorManager,
- Throttler $throttler,
- $principalPrefix = 'principals/users/') {
- $this->session = $session;
- $this->userSession = $userSession;
- $this->twoFactorManager = $twoFactorManager;
- $this->request = $request;
- $this->throttler = $throttler;
+ private ?string $currentUser = null;
+
+ 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();
- $this->realm = $defaults->getName();
+ $defaults = new Defaults();
+ $this->realm = $defaults->getName() ?: 'Nextcloud';
}
/**
@@ -96,13 +53,10 @@ class Auth extends AbstractBasic {
* account was changed.
*
* @see https://github.com/owncloud/core/issues/13245
- *
- * @param string $username
- * @return bool
*/
- public function isDavAuthenticated($username) {
- return !is_null($this->session->get(self::DAV_AUTHENTICATED)) &&
- $this->session->get(self::DAV_AUTHENTICATED) === $username;
+ public function isDavAuthenticated(string $username): bool {
+ return !is_null($this->session->get(self::DAV_AUTHENTICATED))
+ && $this->session->get(self::DAV_AUTHENTICATED) === $username;
}
/**
@@ -117,17 +71,14 @@ 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())
) {
- \OC_Util::setupFS($this->userSession->getUser()->getUID());
$this->session->close();
return true;
} else {
- \OC_Util::setupFS(); //login hooks may need early access to the filesystem
try {
if ($this->userSession->logClientIn($username, $password, $this->request, $this->throttler)) {
- \OC_Util::setupFS($this->userSession->getUser()->getUID());
$this->session->set(self::DAV_AUTHENTICATED, $this->userSession->getUser()->getUID());
$this->session->close();
return true;
@@ -138,14 +89,15 @@ class Auth extends AbstractBasic {
} catch (PasswordLoginForbiddenException $ex) {
$this->session->close();
throw new PasswordLoginForbidden();
+ } catch (MaxDelayReached $ex) {
+ $this->session->close();
+ throw new TooManyRequests();
}
}
}
/**
- * @param RequestInterface $request
- * @param ResponseInterface $response
- * @return array
+ * @return array{bool, string}
* @throws NotAuthenticated
* @throws ServiceUnavailable
*/
@@ -157,19 +109,18 @@ class Auth extends AbstractBasic {
} catch (Exception $e) {
$class = get_class($e);
$msg = $e->getMessage();
- \OC::$server->getLogger()->logException($e);
+ Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
throw new ServiceUnavailable("$class: $msg");
}
}
/**
* Checks whether a CSRF check is required on the request
- *
- * @return bool
*/
- private function requiresCSRFCheck() {
- // GET requires no check at all
- if ($this->request->getMethod() === 'GET') {
+ private function requiresCSRFCheck(): bool {
+
+ $methodsWithoutCsrf = ['GET', 'HEAD', 'OPTIONS'];
+ if (in_array($this->request->getMethod(), $methodsWithoutCsrf)) {
return false;
}
@@ -193,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;
}
@@ -202,21 +153,19 @@ class Auth extends AbstractBasic {
}
/**
- * @param RequestInterface $request
- * @param ResponseInterface $response
- * @return array
+ * @return array{bool, string}
* @throws NotAuthenticated
*/
- private function auth(RequestInterface $request, ResponseInterface $response) {
+ 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.');
}
}
@@ -229,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;
@@ -241,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 d28a9cfdb84..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,33 +17,16 @@ use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class BearerAuth extends AbstractBearer {
- /** @var IUserSession */
- private $userSession;
- /** @var ISession */
- private $session;
- /** @var IRequest */
- private $request;
- /** @var string */
- private $principalPrefix;
-
- /**
- * @param IUserSession $userSession
- * @param ISession $session
- * @param string $principalPrefix
- * @param IRequest $request
- */
- 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();
- $this->realm = $defaults->getName();
+ $defaults = new Defaults();
+ $this->realm = $defaults->getName() ?: 'Nextcloud';
}
private function setupUserFs($userId) {
@@ -90,7 +59,15 @@ class BearerAuth extends AbstractBearer {
* @param RequestInterface $request
* @param ResponseInterface $response
*/
- public function challenge(RequestInterface $request, ResponseInterface $response) {
- $response->setStatus(401);
+ public function challenge(RequestInterface $request, ResponseInterface $response): void {
+ // 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 8e2ea4d4e16..21358406a4a 100644
--- a/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php
@@ -1,31 +1,16 @@
<?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;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
@@ -36,23 +21,18 @@ use Sabre\HTTP\RequestInterface;
* @package OCA\DAV\Connector\Sabre
*/
class BlockLegacyClientPlugin extends ServerPlugin {
- /** @var \Sabre\DAV\Server */
- protected $server;
- /** @var IConfig */
- protected $config;
+ protected ?Server $server = null;
- /**
- * @param IConfig $config
- */
- public function __construct(IConfig $config) {
- $this->config = $config;
+ public function __construct(
+ private IConfig $config,
+ private ThemingDefaults $themingDefaults,
+ ) {
}
/**
- * @param \Sabre\DAV\Server $server
* @return void
*/
- public function initialize(\Sabre\DAV\Server $server) {
+ public function initialize(Server $server) {
$this->server = $server;
$this->server->on('beforeMethod:*', [$this, 'beforeHandler'], 200);
}
@@ -69,14 +49,26 @@ class BlockLegacyClientPlugin extends ServerPlugin {
return;
}
- $minimumSupportedDesktopVersion = $this->config->getSystemValue('minimum.supported.desktop.version', '2.0.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 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);
- // Match on the mirall version which is in scheme "Mozilla/5.0 (%1) mirall/%2" or
- // "mirall/%1" for older releases
- preg_match("/(?:mirall\\/)([\d.]+)/i", $userAgent, $versionMatches);
- if (isset($versionMatches[1]) &&
- version_compare($versionMatches[1], $minimumSupportedDesktopVersion) === -1) {
- throw new \Sabre\DAV\Exception\Forbidden('Unsupported client version.');
+ 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 eb1233d3540..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;
@@ -28,24 +11,25 @@ use Sabre\DAV\Tree;
class CachingTree extends Tree {
/**
* Store a node in the cache
- *
- * @param Node $node
- * @param null|string $path
*/
- public function cacheNode(Node $node, $path = null) {
+ public function cacheNode(Node $node, ?string $path = null): void {
if (is_null($path)) {
$path = $node->getPath();
}
$this->cache[trim($path, '/')] = $node;
}
+ /**
+ * @param string $path
+ * @return void
+ */
public function markDirty($path) {
// We don't care enough about sub-paths
// flushing the entire cache
$path = trim($path, '/');
foreach ($this->cache as $nodePath => $node) {
- $nodePath = (string) $nodePath;
- if ('' === $path || $nodePath == $path || 0 === strpos($nodePath, $path.'/')) {
+ $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 74cdc98ef4f..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;
@@ -35,17 +20,14 @@ class ChecksumList implements XmlSerializable {
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
/** @var string[] of TYPE:CHECKSUM */
- private $checksums;
+ private array $checksums;
- /**
- * @param string $checksum
- */
- public function __construct($checksum) {
- $this->checksums = explode(',', $checksum);
+ public function __construct(string $checksum) {
+ $this->checksums = explode(' ', $checksum);
}
/**
- * The xmlSerialize metod is called during xml writing.
+ * The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
diff --git a/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php b/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php
index 3247259357f..18009080585 100644
--- a/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php
+++ b/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php
@@ -2,38 +2,22 @@
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;
use Sabre\HTTP\ResponseInterface;
class ChecksumUpdatePlugin extends ServerPlugin {
- /**
- * @var \Sabre\DAV\Server
- */
- protected $server;
+ protected ?Server $server = null;
- public function initialize(\Sabre\DAV\Server $server) {
+ public function initialize(Server $server) {
$this->server = $server;
$server->on('method:PATCH', [$this, 'httpPatch']);
}
@@ -42,19 +26,7 @@ class ChecksumUpdatePlugin extends ServerPlugin {
return 'checksumupdate';
}
- 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 82980553fa8..e4b6c2636da 100644
--- a/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php
@@ -1,33 +1,17 @@
<?php
+
+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;
use OCP\Comments\ICommentsManager;
use OCP\IUserSession;
use Sabre\DAV\PropFind;
+use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
class CommentPropertiesPlugin extends ServerPlugin {
@@ -35,20 +19,13 @@ class CommentPropertiesPlugin extends ServerPlugin {
public const PROPERTY_NAME_COUNT = '{http://owncloud.org/ns}comments-count';
public const PROPERTY_NAME_UNREAD = '{http://owncloud.org/ns}comments-unread';
- /** @var \Sabre\DAV\Server */
- protected $server;
-
- /** @var ICommentsManager */
- private $commentsManager;
-
- /** @var IUserSession */
- private $userSession;
-
- private $cachedUnreadCount = [];
+ protected ?Server $server = null;
+ 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,
+ ) {
}
/**
@@ -67,7 +44,7 @@ class CommentPropertiesPlugin extends ServerPlugin {
$this->server->on('propFind', [$this, 'handleGetProperties']);
}
- private function cacheDirectory(Directory $directory) {
+ private function cacheDirectory(Directory $directory): void {
$children = $directory->getChildren();
$ids = [];
@@ -84,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) {
@@ -102,62 +79,52 @@ 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;
}
// need prefetch ?
- if ($node instanceof \OCA\DAV\Connector\Sabre\Directory
+ if ($node instanceof Directory
&& $propFind->getDepth() !== 0
&& !is_null($propFind->getStatus(self::PROPERTY_NAME_UNREAD))
) {
$this->cacheDirectory($node);
}
- $propFind->handle(self::PROPERTY_NAME_COUNT, function () use ($node) {
+ $propFind->handle(self::PROPERTY_NAME_COUNT, function () use ($node): int {
return $this->commentsManager->getNumberOfCommentsForObject('files', (string)$node->getId());
});
- $propFind->handle(self::PROPERTY_NAME_HREF, function () use ($node) {
+ $propFind->handle(self::PROPERTY_NAME_HREF, function () use ($node): ?string {
return $this->getCommentsLink($node);
});
- $propFind->handle(self::PROPERTY_NAME_UNREAD, function () use ($node) {
- if (isset($this->cachedUnreadCount[$node->getId()])) {
- return $this->cachedUnreadCount[$node->getId()];
- }
- return $this->getUnreadCount($node);
+ $propFind->handle(self::PROPERTY_NAME_UNREAD, function () use ($node): ?int {
+ return $this->cachedUnreadCount[$node->getId()] ?? $this->getUnreadCount($node);
});
}
/**
- * returns a reference to the comments node
- *
- * @param Node $node
- * @return mixed|string
+ * Returns a reference to the comments node
*/
- public function getCommentsLink(Node $node) {
+ public function getCommentsLink(Node $node): ?string {
$href = $this->server->getBaseUri();
$entryPoint = strpos($href, '/remote.php/');
if ($entryPoint === false) {
// in case we end up somewhere else, unexpectedly.
return null;
}
- $commentsPart = 'dav/comments/files/' . rawurldecode($node->getId());
- $href = substr_replace($href, $commentsPart, $entryPoint + strlen('/remote.php/'));
- return $href;
+ $commentsPart = 'dav/comments/files/' . rawurldecode((string)$node->getId());
+ return substr_replace($href, $commentsPart, $entryPoint + strlen('/remote.php/'));
}
/**
- * returns the number of unread comments for the currently logged in user
+ * Returns the number of unread comments for the currently logged in user
* on the given file or directory node
- *
- * @param Node $node
- * @return Int|null
*/
- public function getUnreadCount(Node $node) {
+ public function getUnreadCount(Node $node): ?int {
$user = $this->userSession->getUser();
if (is_null($user)) {
return null;
diff --git a/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php b/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php
index 029e631f4d9..609ac295b4c 100644
--- a/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php
@@ -1,30 +1,14 @@
<?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;
use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\Server;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
@@ -34,9 +18,8 @@ use Sabre\HTTP\ResponseInterface;
* or mangle Etag headers.
*/
class CopyEtagHeaderPlugin extends \Sabre\DAV\ServerPlugin {
+ private ?Server $server = null;
- /** @var \Sabre\DAV\Server */
- private $server;
/**
* This initializes the plugin.
*
diff --git a/apps/dav/lib/Connector/Sabre/DavAclPlugin.php b/apps/dav/lib/Connector/Sabre/DavAclPlugin.php
index 6842975835d..100d719ef01 100644
--- a/apps/dav/lib/Connector/Sabre/DavAclPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/DavAclPlugin.php
@@ -1,33 +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>
- *
- * @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;
@@ -58,23 +42,38 @@ class DavAclPlugin extends \Sabre\DAVACL\Plugin {
case AddressBook::class:
$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;
}
public function propFind(PropFind $propFind, INode $node) {
+ if ($node instanceof Node) {
+ // files don't use dav acls
+ return;
+ }
+
// If the node is neither readable nor writable then fail unless its of
// the standard user-principal
if (!($node instanceof User)) {
@@ -94,8 +93,23 @@ class DavAclPlugin extends \Sabre\DAVACL\Plugin {
$path = $request->getPath();
// prevent the plugin from causing an unneeded overhead for file requests
- if (strpos($path, 'files/') !== 0) {
- parent::beforeMethod($request, $response);
+ if (str_starts_with($path, 'files/')) {
+ return;
+ }
+
+ parent::beforeMethod($request, $response);
+
+ if (!str_starts_with($path, 'addressbooks/') && !str_starts_with($path, 'calendars/')) {
+ return;
+ }
+
+ [$parentName] = \Sabre\Uri\split($path);
+ if ($request->getMethod() === 'REPORT') {
+ // is calendars/users/bob or addressbooks/users/bob readable?
+ $this->checkPrivileges($parentName, '{DAV:}read');
+ } elseif ($request->getMethod() === 'MKCALENDAR' || $request->getMethod() === 'MKCOL') {
+ // is calendars/users/bob or addressbooks/users/bob writeable?
+ $this->checkPrivileges($parentName, '{DAV:}write');
}
}
}
diff --git a/apps/dav/lib/Connector/Sabre/Directory.php b/apps/dav/lib/Connector/Sabre/Directory.php
index ed98b5050f8..fe09c3f423f 100644
--- a/apps/dav/lib/Connector/Sabre/Directory.php
+++ b/apps/dav/lib/Connector/Sabre/Directory.php
@@ -1,52 +1,36 @@
<?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;
use OC\Files\Mount\MoveableMount;
use OC\Files\View;
-use OC\Metadata\FileMetadata;
-use OC\Metadata\MetadataGroup;
+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;
+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;
use Sabre\DAV\Exception\Locked;
@@ -55,41 +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[]
- */
- private $dirContent;
-
- /**
- * Cached quota info
- *
- * @var array
+ * @var FileInfo[]
*/
- private $quotaInfo;
+ private ?array $dirContent = null;
- /**
- * @var ObjectTree|null
- */
- private $tree;
-
- /** @var array<string, array<int, FileMetadata>> */
- private array $metadata = [];
+ /** Cached quota info */
+ private ?array $quotaInfo = null;
/**
* Sets up the node, expects a full path name
- *
- * @param \OC\Files\View $view
- * @param \OCP\Files\FileInfo $info
- * @param ObjectTree|null $tree
- * @param \OCP\Share\IManager $shareManager
*/
- public function __construct(View $view, FileInfo $info, $tree = null, $shareManager = null) {
+ public function __construct(
+ View $view,
+ FileInfo $info,
+ private ?CachingTree $tree = null,
+ ?IShareManager $shareManager = null,
+ ) {
parent::__construct($view, $info, $shareManager);
- $this->tree = $tree;
}
/**
@@ -126,22 +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);
@@ -155,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);
@@ -197,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);
}
@@ -212,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) {
- if (!$this->info->isReadable()) {
+ public function getChild($name, $info = null, ?IRequest $request = null, ?IL10N $l10n = null) {
+ $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();
}
@@ -227,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);
}
}
@@ -245,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);
+ // 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);
@@ -258,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)) {
@@ -268,7 +241,11 @@ 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
- throw new Forbidden('No read permissions');
+ 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');
+ }
}
$folderContent = $this->getNode()->getDirectoryListing();
} catch (LockedException $e) {
@@ -276,8 +253,11 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
}
$nodes = [];
+ $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);
+ $node = $this->getChild($info->getName(), $info, $request, $l10n);
$nodes[] = $node;
}
$this->dirContent = $nodes;
@@ -326,21 +306,29 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
}
}
+ private function getLogger(): LoggerInterface {
+ return Server::get(LoggerInterface::class);
+ }
+
/**
* Returns available diskspace information
*
* @return array
*/
public function getQuotaInfo() {
- /** @var LoggerInterface $logger */
- $logger = \OC::$server->get(LoggerInterface::class);
if ($this->quotaInfo) {
return $this->quotaInfo;
}
+ $relativePath = $this->fileView->getRelativePath($this->info->getPath());
+ if ($relativePath === null) {
+ $this->getLogger()->warning('error while getting quota as the relative path cannot be found');
+ return [0, 0];
+ }
+
try {
- $storageInfo = \OC_Helper::getStorageInfo($this->info->getPath(), $this->info, false);
- if ($storageInfo['quota'] === \OCP\Files\FileInfo::SPACE_UNLIMITED) {
- $free = \OCP\Files\FileInfo::SPACE_UNLIMITED;
+ $storageInfo = \OC_Helper::getStorageInfo($relativePath, $this->info, false);
+ if ($storageInfo['quota'] === FileInfo::SPACE_UNLIMITED) {
+ $free = FileInfo::SPACE_UNLIMITED;
} else {
$free = $storageInfo['free'];
}
@@ -349,14 +337,14 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
$free
];
return $this->quotaInfo;
- } catch (\OCP\Files\NotFoundException $e) {
- $logger->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) {
- $logger->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) {
- $logger->warning("error while getting quota into", ['exception' => $e]);
+ $this->getLogger()->warning('error while getting quota into', ['exception' => $e]);
return [0, 0];
}
}
@@ -396,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;
@@ -417,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;
@@ -457,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);
}
@@ -470,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());
+ }
+
+ $copyOkay = $this->fileView->copy($sourcePath, $destinationPath);
- return $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 3a8469e99b4..f6baceb748b 100644
--- a/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php
+++ b/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php
@@ -1,31 +1,14 @@
<?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;
@@ -43,8 +26,7 @@ use Sabre\HTTP\ResponseInterface;
* @package OCA\DAV\Connector\Sabre
*/
class DummyGetResponsePlugin extends \Sabre\DAV\ServerPlugin {
- /** @var \Sabre\DAV\Server */
- protected $server;
+ protected ?Server $server = null;
/**
* @param \Sabre\DAV\Server $server
@@ -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.';
- $stream = fopen('php://memory','r+');
+ $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 36063da8d65..38708e945e9 100644
--- a/apps/dav/lib/Connector/Sabre/Exception/FileLocked.php
+++ b/apps/dav/lib/Connector/Sabre/Exception/FileLocked.php
@@ -1,36 +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 {
- public function __construct($message = "", $code = 0, Exception $previous = null) {
- if ($previous instanceof \OCP\Files\LockNotAcquiredException) {
+ /**
+ * @param string $message
+ * @param int $code
+ */
+ 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 aabd5fda2fb..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;
}
/**
@@ -48,17 +31,17 @@ class Forbidden extends \Sabre\DAV\Exception\Forbidden {
* @param \DOMElement $errorNode
* @return void
*/
- public function serialize(\Sabre\DAV\Server $server,\DOMElement $errorNode) {
+ public function serialize(\Sabre\DAV\Server $server, \DOMElement $errorNode) {
// set ownCloud namespace
$errorNode->setAttribute('xmlns:o', self::NS_OWNCLOUD);
// adding the retry node
- $error = $errorNode->ownerDocument->createElementNS('o:','o:retry', var_export($this->retry, true));
+ $error = $errorNode->ownerDocument->createElementNS('o:', 'o:retry', var_export($this->retry, true));
$errorNode->appendChild($error);
// adding the message node
- $error = $errorNode->ownerDocument->createElementNS('o:','o:reason', $this->getMessage());
+ $error = $errorNode->ownerDocument->createElementNS('o:', 'o:reason', $this->getMessage());
$errorNode->appendChild($error);
}
}
diff --git a/apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php b/apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php
index c504483d45a..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;
}
/**
@@ -60,17 +42,17 @@ class InvalidPath extends Exception {
* @param \DOMElement $errorNode
* @return void
*/
- public function serialize(\Sabre\DAV\Server $server,\DOMElement $errorNode) {
+ public function serialize(\Sabre\DAV\Server $server, \DOMElement $errorNode) {
// set ownCloud namespace
$errorNode->setAttribute('xmlns:o', self::NS_OWNCLOUD);
// adding the retry node
- $error = $errorNode->ownerDocument->createElementNS('o:','o:retry', var_export($this->retry, true));
+ $error = $errorNode->ownerDocument->createElementNS('o:', 'o:retry', var_export($this->retry, true));
$errorNode->appendChild($error);
// adding the message node
- $error = $errorNode->ownerDocument->createElementNS('o:','o:reason', $this->getMessage());
+ $error = $errorNode->ownerDocument->createElementNS('o:', 'o:reason', $this->getMessage());
$errorNode->appendChild($error);
}
}
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
new file mode 100644
index 00000000000..67455fc9474
--- /dev/null
+++ b/apps/dav/lib/Connector/Sabre/Exception/TooManyRequests.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Connector\Sabre\Exception;
+
+use DOMElement;
+use Sabre\DAV\Exception\NotAuthenticated;
+use Sabre\DAV\Server;
+
+class TooManyRequests extends NotAuthenticated {
+ public const NS_OWNCLOUD = 'http://owncloud.org/ns';
+
+ public function getHTTPCode() {
+ return 429;
+ }
+
+ /**
+ * This method allows the exception to include additional information
+ * into the WebDAV error response
+ *
+ * @param Server $server
+ * @param DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(Server $server, DOMElement $errorNode) {
+
+ // set ownCloud namespace
+ $errorNode->setAttribute('xmlns:o', self::NS_OWNCLOUD);
+
+ $error = $errorNode->ownerDocument->createElementNS('o:', 'o:hint', 'too many requests');
+ $errorNode->appendChild($error);
+ }
+}
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 b4df1f582db..686386dbfef 100644
--- a/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php
@@ -1,36 +1,17 @@
<?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;
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
use OCA\DAV\Connector\Sabre\Exception\PasswordLoginForbidden;
+use OCA\DAV\Exception\ServerMaintenanceMode;
use OCP\Files\StorageNotAvailableException;
-use OCP\ILogger;
+use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Conflict;
use Sabre\DAV\Exception\Forbidden;
@@ -41,7 +22,6 @@ use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\Exception\NotImplemented;
use Sabre\DAV\Exception\PreconditionFailed;
use Sabre\DAV\Exception\RequestedRangeNotSatisfiable;
-use Sabre\DAV\Exception\ServiceUnavailable;
class ExceptionLoggerPlugin extends \Sabre\DAV\ServerPlugin {
protected $nonFatalExceptions = [
@@ -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,
@@ -81,21 +64,16 @@ class ExceptionLoggerPlugin extends \Sabre\DAV\ServerPlugin {
FileLocked::class => true,
// An invalid range is requested
RequestedRangeNotSatisfiable::class => true,
+ ServerMaintenanceMode::class => true,
];
- /** @var string */
- private $appName;
-
- /** @var ILogger */
- private $logger;
-
/**
- * @param string $loggerAppName app name to use when logging
- * @param ILogger $logger
+ * @param string $appName app name to use when logging
*/
- public function __construct($loggerAppName, $logger) {
- $this->appName = $loggerAppName;
- $this->logger = $logger;
+ public function __construct(
+ private string $appName,
+ private LoggerInterface $logger,
+ ) {
}
/**
@@ -115,23 +93,20 @@ class ExceptionLoggerPlugin extends \Sabre\DAV\ServerPlugin {
/**
* Log exception
- *
*/
public function logException(\Throwable $ex) {
$exceptionClass = get_class($ex);
- $level = ILogger::FATAL;
- if (isset($this->nonFatalExceptions[$exceptionClass]) ||
- (
- $exceptionClass === ServiceUnavailable::class &&
- $ex->getMessage() === 'System in maintenance mode.'
- )
- ) {
- $level = ILogger::DEBUG;
+ if (isset($this->nonFatalExceptions[$exceptionClass])) {
+ $this->logger->debug($ex->getMessage(), [
+ 'app' => $this->appName,
+ 'exception' => $ex,
+ ]);
+ return;
}
- $this->logger->logException($ex, [
+ $this->logger->critical($ex->getMessage(), [
'app' => $this->appName,
- 'level' => $level,
+ 'exception' => $ex,
]);
}
}
diff --git a/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php b/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php
index d05cd6d2e6d..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);
@@ -90,7 +74,7 @@ class FakeLockerPlugin extends ServerPlugin {
*/
public function propFind(PropFind $propFind, INode $node) {
$propFind->handle('{DAV:}supportedlock', function () {
- return new SupportedLock(true);
+ return new SupportedLock();
});
$propFind->handle('{DAV:}lockdiscovery', function () use ($propFind) {
return new LockDiscovery([]);
@@ -108,7 +92,7 @@ class FakeLockerPlugin extends ServerPlugin {
if (isset($fileCondition['tokens'])) {
foreach ($fileCondition['tokens'] as &$token) {
if (isset($token['token'])) {
- if (substr($token['token'], 0, 16) === 'opaquelocktoken:') {
+ if (str_starts_with($token['token'], 'opaquelocktoken:')) {
$token['validToken'] = true;
}
}
@@ -125,19 +109,19 @@ class FakeLockerPlugin extends ServerPlugin {
* @return bool
*/
public function fakeLockProvider(RequestInterface $request,
- ResponseInterface $response) {
+ ResponseInterface $response) {
$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;
@@ -151,8 +135,8 @@ class FakeLockerPlugin extends ServerPlugin {
* @return bool
*/
public function fakeUnlockProvider(RequestInterface $request,
- ResponseInterface $response) {
- $response->setStatus(204);
+ ResponseInterface $response) {
+ $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 6c379984995..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;
@@ -43,67 +12,71 @@ use OC\AppFramework\Http\Request;
use OC\Files\Filesystem;
use OC\Files\Stream\HashWrapper;
use OC\Files\View;
-use OC\Metadata\FileMetadata;
use OCA\DAV\AppInfo\Application;
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 OCA\DAV\Connector\Sabre\Exception\BadGateway;
+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\ILogger;
+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;
class File extends Node implements IFile {
- protected $request;
-
+ protected IRequest $request;
protected IL10N $l10n;
- /** @var array<string, FileMetadata> */
- private array $metadata = [];
-
/**
* Sets up the node, expects a full path name
*
- * @param \OC\Files\View $view
- * @param \OCP\Files\FileInfo $info
- * @param \OCP\Share\IManager $shareManager
- * @param \OC\AppFramework\Http\Request $request
+ * @param View $view
+ * @param FileInfo $info
+ * @param ?\OCP\Share\IManager $shareManager
+ * @param ?IRequest $request
+ * @param ?IL10N $l10n
*/
- public function __construct(View $view, FileInfo $info, IManager $shareManager = null, Request $request = null) {
+ public function __construct(View $view, FileInfo $info, ?IManager $shareManager = null, ?IRequest $request = null, ?IL10N $l10n = null) {
parent::__construct($view, $info, $shareManager);
- // Querying IL10N directly results in a dependency loop
- /** @var IL10NFactory $l10nFactory */
- $l10nFactory = \OC::$server->get(IL10NFactory::class);
- $this->l10n = $l10nFactory->get(Application::APP_ID);
+ if ($l10n) {
+ $this->l10n = $l10n;
+ } else {
+ // Querying IL10N directly results in a dependency loop
+ /** @var IL10NFactory $l10nFactory */
+ $l10nFactory = Server::get(IL10NFactory::class);
+ $this->l10n = $l10nFactory->get(Application::APP_ID);
+ }
if (isset($request)) {
$this->request = $request;
} else {
- $this->request = \OC::$server->getRequest();
+ $this->request = Server::get(IRequest::class);
}
}
@@ -124,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
@@ -138,7 +111,7 @@ class File extends Node implements IFile {
public function put($data) {
try {
$exists = $this->fileView->file_exists($this->path);
- if ($this->info && $exists && !$this->info->isUpdateable()) {
+ if ($exists && !$this->info->isUpdateable()) {
throw new Forbidden();
}
} catch (StorageNotAvailableException $e) {
@@ -148,24 +121,18 @@ class File extends Node implements IFile {
// verify path of the target
$this->verifyPath();
- // chunked handling
- if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
- 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;
@@ -181,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 {
@@ -215,93 +183,97 @@ class File extends Node implements IFile {
$data = $tmpData;
}
- $data = HashWrapper::wrap($data, 'md5', function ($hash) {
- $this->header('X-Hash-MD5: ' . $hash);
- });
- $data = HashWrapper::wrap($data, 'sha1', function ($hash) {
- $this->header('X-Hash-SHA1: ' . $hash);
- });
- $data = HashWrapper::wrap($data, 'sha256', function ($hash) {
- $this->header('X-Hash-SHA256: ' . $hash);
- });
-
- if ($partStorage->instanceOfStorage(Storage\IWriteStreamStorage::class)) {
- $isEOF = false;
- $wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF) {
- $isEOF = feof($stream);
- });
+ if ($this->request->getHeader('X-HASH') !== '') {
+ $hash = $this->request->getHeader('X-HASH');
+ if ($hash === 'all' || $hash === 'md5') {
+ $data = HashWrapper::wrap($data, 'md5', function ($hash): void {
+ $this->header('X-Hash-MD5: ' . $hash);
+ });
+ }
- $result = true;
- $count = -1;
- try {
- $count = $partStorage->writeStream($internalPartPath, $wrappedData);
- } catch (GenericFileException $e) {
- $result = false;
- } catch (BadGateway $e) {
- throw $e;
+ if ($hash === 'all' || $hash === 'sha1') {
+ $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): void {
+ $this->header('X-Hash-SHA256: ' . $hash);
+ });
+ }
+ }
+
+ $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): void {
+ $isEOF = feof($stream);
+ });
- 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->getLogger()->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;
- if (isset($_SERVER['CONTENT_LENGTH'])) {
- $expected = $_SERVER['CONTENT_LENGTH'];
- }
- 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
- if (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['REQUEST_METHOD'] === 'PUT') {
- $expected = (int)$_SERVER['CONTENT_LENGTH'];
- 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) {
- $context = [];
-
if ($e instanceof LockedException) {
- $context['level'] = ILogger::DEBUG;
+ Server::get(LoggerInterface::class)->debug($e->getMessage(), ['exception' => $e]);
+ } else {
+ Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
}
- \OC::$server->getLogger()->logException($e, $context);
if ($needsPartFile) {
$partStorage->unlink($internalPartPath);
}
@@ -340,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->getLogger()->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) {
@@ -364,8 +336,9 @@ class File extends Node implements IFile {
}
// allow sync clients to send the mtime along in a header
- if (isset($this->request->server['HTTP_X_OC_MTIME'])) {
- $mtime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_MTIME']);
+ $mtimeHeader = $this->request->getHeader('x-oc-mtime');
+ if ($mtimeHeader !== '') {
+ $mtime = $this->sanitizeMtime($mtimeHeader);
if ($this->fileView->touch($this->path, $mtime)) {
$this->header('X-OC-MTime: accepted');
}
@@ -376,8 +349,9 @@ class File extends Node implements IFile {
];
// allow sync clients to send the creation time along in a header
- if (isset($this->request->server['HTTP_X_OC_CTIME'])) {
- $ctime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_CTIME']);
+ $ctimeHeader = $this->request->getHeader('x-oc-ctime');
+ if ($ctimeHeader) {
+ $ctime = $this->sanitizeMtime($ctimeHeader);
$fileInfoUpdate['creation_time'] = $ctime;
$this->header('X-OC-CTime: accepted');
}
@@ -390,8 +364,9 @@ class File extends Node implements IFile {
$this->refreshInfo();
- if (isset($this->request->server['HTTP_OC_CHECKSUM'])) {
- $checksum = trim($this->request->server['HTTP_OC_CHECKSUM']);
+ $checksumHeader = $this->request->getHeader('oc-checksum');
+ if ($checksumHeader) {
+ $checksum = trim($checksumHeader);
$this->setChecksum($checksum);
} elseif ($this->getChecksum() !== null && $this->getChecksum() !== '') {
$this->setChecksum('');
@@ -404,61 +379,68 @@ 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);
}
}
- /**
- * @param string $path
- */
- private function emitPreHooks($exists, $path = null) {
+ private function emitPreHooks(bool $exists, ?string $path = null): bool {
if (is_null($path)) {
$path = $this->path;
}
$hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
+ if ($hookPath === null) {
+ // We only trigger hooks from inside default view
+ return true;
+ }
$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;
}
- /**
- * @param string $path
- */
- private function emitPostHooks($exists, $path = null) {
+ private function emitPostHooks(bool $exists, ?string $path = null): void {
if (is_null($path)) {
$path = $this->path;
}
$hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
+ if ($hookPath === null) {
+ // We only trigger hooks from inside default view
+ 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
]);
}
@@ -476,14 +458,31 @@ 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 = Server::get(LoggerInterface::class);
+ $logger->warning('fixing cached size of file id=' . $this->getId());
+
+ $this->getFileInfo()->getStorage()->getUpdater()->update($this->getFileInfo()->getInternalPath());
+ $this->refreshInfo();
+ }
+
return $res;
} catch (GenericEncryptionException $e) {
// returning 503 will allow retry of the operation at a later point in time
@@ -533,20 +532,19 @@ class File extends Node implements IFile {
$mimeType = $this->info->getMimetype();
// PROPFIND needs to return the correct mime type, for consistency with the web UI
- if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PROPFIND') {
+ 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\App::isEnabled('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 [];
@@ -556,132 +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 (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PUT') {
- if (isset($_SERVER['CONTENT_LENGTH'])) {
- $expected = (int)$_SERVER['CONTENT_LENGTH'];
- 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->getLogger()->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
- if (isset($this->request->server['HTTP_X_OC_MTIME'])) {
- $mtime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_MTIME']);
- 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);
-
- if (isset($this->request->server['HTTP_OC_CHECKSUM'])) {
- $checksum = trim($this->request->server['HTTP_OC_CHECKSUM']);
- $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
@@ -737,9 +609,6 @@ class File extends Node implements IFile {
* @return string|null
*/
public function getChecksum() {
- if (!$this->info) {
- return null;
- }
return $this->info->getChecksum();
}
@@ -761,16 +630,4 @@ class File extends Node implements IFile {
public function getNode(): \OCP\Files\File {
return $this->node;
}
-
- public function getMetadata(string $group): FileMetadata {
- return $this->metadata[$group];
- }
-
- public function setMetadata(string $group, FileMetadata $metadata): void {
- $this->metadata[$group] = $metadata;
- }
-
- public function hasMetadata(string $group) {
- return array_key_exists($group, $this->metadata);
- }
}
diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php
index 9c4f912610b..843383a0452 100644
--- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php
@@ -1,61 +1,45 @@
<?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 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\Metadata\IMetadataManager;
+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;
+use OCP\FilesMetadata\IFilesMetadataManager;
+use OCP\FilesMetadata\Model\IMetadataValueWrapper;
use OCP\IConfig;
use OCP\IPreview;
use OCP\IRequest;
use OCP\IUserSession;
-use Psr\Log\LoggerInterface;
+use OCP\L10N\IFactory;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\IFile;
use Sabre\DAV\PropFind;
use Sabre\DAV\PropPatch;
+use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\DAV\Tree;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
-use Sabre\Uri;
class FilesPlugin extends ServerPlugin {
-
// namespace
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
@@ -64,93 +48,55 @@ class FilesPlugin extends ServerPlugin {
public const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions';
public const SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-collaboration-services.org/ns}share-permissions';
public const OCM_SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-cloud-mesh.org/ns}share-permissions';
+ public const SHARE_ATTRIBUTES_PROPERTYNAME = '{http://nextcloud.org/ns}share-attributes';
public const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL';
public const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size';
public const GETETAG_PROPERTYNAME = '{DAV:}getetag';
public const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified';
public const CREATIONDATE_PROPERTYNAME = '{DAV:}creationdate';
+ public const DISPLAYNAME_PROPERTYNAME = '{DAV:}displayname';
public const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id';
public const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name';
public const CHECKSUMS_PROPERTYNAME = '{http://owncloud.org/ns}checksums';
public const DATA_FINGERPRINT_PROPERTYNAME = '{http://owncloud.org/ns}data-fingerprint';
public const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview';
public const MOUNT_TYPE_PROPERTYNAME = '{http://nextcloud.org/ns}mount-type';
- public const IS_ENCRYPTED_PROPERTYNAME = '{http://nextcloud.org/ns}is-encrypted';
+ public const MOUNT_ROOT_PROPERTYNAME = '{http://nextcloud.org/ns}is-mount-root';
+ 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_SIZE = '{http://nextcloud.org/ns}file-metadata-size';
-
- /**
- * Reference to main server object
- *
- * @var \Sabre\DAV\Server
- */
- private $server;
-
- /**
- * @var Tree
- */
- private $tree;
-
- /**
- * @var IUserSession
- */
- private $userSession;
-
- /**
- * Whether this is public webdav.
- * If true, some returned information will be stripped off.
- *
- * @var bool
- */
- private $isPublic;
+ public const FILE_METADATA_PREFIX = '{http://nextcloud.org/ns}metadata-';
+ public const HIDDEN_PROPERTYNAME = '{http://nextcloud.org/ns}hidden';
- /**
- * @var bool
- */
- private $downloadAttachment;
-
- /**
- * @var IConfig
- */
- private $config;
-
- /**
- * @var IRequest
- */
- private $request;
-
- /**
- * @var IPreview
- */
- private $previewManager;
+ /** Reference to main server object */
+ private ?Server $server = null;
/**
* @param Tree $tree
* @param IConfig $config
* @param IRequest $request
* @param IPreview $previewManager
- * @param bool $isPublic
+ * @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
*/
- public function __construct(Tree $tree,
- IConfig $config,
- IRequest $request,
- IPreview $previewManager,
- IUserSession $userSession,
- $isPublic = false,
- $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,
+ ) {
}
/**
@@ -161,10 +107,9 @@ class FilesPlugin extends ServerPlugin {
*
* This method should set up the required event subscriptions.
*
- * @param \Sabre\DAV\Server $server
* @return void
*/
- public function initialize(\Sabre\DAV\Server $server) {
+ public function initialize(Server $server) {
$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
$server->xml->namespaceMap[self::NS_NEXTCLOUD] = 'nc';
$server->protectedProperties[] = self::FILEID_PROPERTYNAME;
@@ -172,6 +117,7 @@ class FilesPlugin extends ServerPlugin {
$server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME;
$server->protectedProperties[] = self::SHARE_PERMISSIONS_PROPERTYNAME;
$server->protectedProperties[] = self::OCM_SHARE_PERMISSIONS_PROPERTYNAME;
+ $server->protectedProperties[] = self::SHARE_ATTRIBUTES_PROPERTYNAME;
$server->protectedProperties[] = self::SIZE_PROPERTYNAME;
$server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME;
$server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME;
@@ -180,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
@@ -194,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');
}
}
@@ -272,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(
[
@@ -288,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 !== '') {
@@ -308,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,7 +332,7 @@ class FilesPlugin extends ServerPlugin {
);
});
- $propFind->handle(self::OCM_SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest) {
+ $propFind->handle(self::OCM_SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest): ?string {
$user = $this->userSession->getUser();
if ($user === null) {
return null;
@@ -356,14 +341,18 @@ class FilesPlugin extends ServerPlugin {
$user->getUID()
);
$ocmPermissions = $this->ncPermissions2ocmPermissions($ncPermissions);
- return json_encode($ocmPermissions);
+ return json_encode($ocmPermissions, JSON_THROW_ON_ERROR);
+ });
+
+ $propFind->handle(self::SHARE_ATTRIBUTES_PROPERTYNAME, function () use ($node, $httpRequest) {
+ return json_encode($node->getShareAttributes(), JSON_THROW_ON_ERROR);
});
- $propFind->handle(self::GETETAG_PROPERTYNAME, function () use ($node) {
+ $propFind->handle(self::GETETAG_PROPERTYNAME, function () use ($node): string {
return $node->getETag();
});
- $propFind->handle(self::OWNER_ID_PROPERTYNAME, function () use ($node) {
+ $propFind->handle(self::OWNER_ID_PROPERTYNAME, function () use ($node): ?string {
$owner = $node->getOwner();
if (!$owner) {
return null;
@@ -371,36 +360,79 @@ class FilesPlugin extends ServerPlugin {
return $owner->getUID();
}
});
- $propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function () use ($node) {
+ $propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function () use ($node): ?string {
$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) {
- return json_encode($this->previewManager->isAvailable($node->getFileInfo()));
+ return json_encode($this->previewManager->isAvailable($node->getFileInfo()), JSON_THROW_ON_ERROR);
});
- $propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node) {
+ $propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node): int|float {
return $node->getSize();
});
$propFind->handle(self::MOUNT_TYPE_PROPERTYNAME, function () use ($node) {
return $node->getFileInfo()->getMountPoint()->getMountType();
});
- $propFind->handle(self::SHARE_NOTE, function () use ($node, $httpRequest) {
+ /**
+ * This is a special property which is used to determine if a node
+ * is a mount root or not, e.g. a shared folder.
+ * If so, then the node can only be unshared and not deleted.
+ * @see https://github.com/nextcloud/server/blob/cc75294eb6b16b916a342e69998935f89222619d/lib/private/Files/View.php#L696-L698
+ */
+ $propFind->handle(self::MOUNT_ROOT_PROPERTYNAME, function () use ($node) {
+ return $node->getNode()->getInternalPath() === '' ? 'true' : 'false';
+ });
+
+ $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) {
@@ -411,9 +443,34 @@ class FilesPlugin extends ServerPlugin {
$propFind->handle(self::CREATION_TIME_PROPERTYNAME, function () use ($node) {
return $node->getFileInfo()->getCreationTime();
});
+
+ foreach ($node->getFileInfo()->getMetadata() as $metadataKey => $metadataValue) {
+ $propFind->handle(self::FILE_METADATA_PREFIX . $metadataKey, $metadataValue);
+ }
+
+ $propFind->handle(self::HIDDEN_PROPERTYNAME, function () use ($node) {
+ $isLivePhoto = isset($node->getFileInfo()->getMetadata()['files-live-photo']);
+ $isMovFile = $node->getFileInfo()->getMimetype() === 'video/quicktime';
+ return ($isLivePhoto && $isMovFile) ? 'true' : 'false';
+ });
+
+ /**
+ * Return file/folder name as displayname. The primary reason to
+ * implement it this way is to avoid costly fallback to
+ * CustomPropertiesBackend (esp. visible when querying all files
+ * in a folder).
+ */
+ $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();
@@ -440,29 +497,6 @@ class FilesPlugin extends ServerPlugin {
$propFind->handle(self::UPLOAD_TIME_PROPERTYNAME, function () use ($node) {
return $node->getFileInfo()->getUploadTime();
});
-
- if ($this->config->getSystemValueBool('enable_file_metadata', true)) {
- $propFind->handle(self::FILE_METADATA_SIZE, function () use ($node) {
- if (!str_starts_with($node->getFileInfo()->getMimetype(), 'image')) {
- return json_encode((object)[]);
- }
-
- if ($node->hasMetadata('size')) {
- $sizeMetadata = $node->getMetadata('size');
- } else {
- // This code path should not be called since we try to preload
- // the metadata when loading the folder or the search results
- // in one go
- $metadataManager = \OC::$server->get(IMetadataManager::class);
- $sizeMetadata = $metadataManager->fetchMetadataFor('size', [$node->getId()])[$node->getId()];
-
- // TODO would be nice to display this in the profiler...
- \OC::$server->get(LoggerInterface::class)->debug('Inefficient fetching of metadata');
- }
-
- return json_encode((object)$sizeMetadata->getMetadata());
- });
- }
}
if ($node instanceof Directory) {
@@ -470,37 +504,8 @@ 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();
- // TODO detect dynamically which metadata groups are requested and
- // preload all of them and not just size
- if ($this->config->getSystemValueBool('enable_file_metadata', true)
- && in_array(self::FILE_METADATA_SIZE, $requestProperties, true)) {
- // Preloading of the metadata
- $fileIds = [];
- foreach ($node->getChildren() as $child) {
- /** @var \OCP\Files\Node|Node $child */
- if (str_starts_with($child->getFileInfo()->getMimeType(), 'image/')) {
- /** @var File $child */
- $fileIds[] = $child->getFileInfo()->getId();
- }
- }
- /** @var IMetaDataManager $metadataManager */
- $metadataManager = \OC::$server->get(IMetadataManager::class);
- $preloadedMetadata = $metadataManager->fetchMetadataFor('size', $fileIds);
- foreach ($node->getChildren() as $child) {
- /** @var \OCP\Files\Node|Node $child */
- if (str_starts_with($child->getFileInfo()->getMimeType(), 'image')) {
- /** @var File $child */
- $child->setMetadata('size', $preloadedMetadata[$child->getFileInfo()->getId()]);
- }
- }
- }
-
if (in_array(self::SUBFILE_COUNT_PROPERTYNAME, $requestProperties, true)
|| in_array(self::SUBFOLDER_COUNT_PROPERTYNAME, $requestProperties, true)) {
$nbFiles = 0;
@@ -536,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';
}
@@ -554,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;
}
@@ -569,10 +574,7 @@ class FilesPlugin extends ServerPlugin {
if (empty($etag)) {
return false;
}
- if ($node->setEtag($etag) !== -1) {
- return true;
- }
- return false;
+ return $node->setEtag($etag) !== -1;
});
$propPatch->handle(self::CREATIONDATE_PROPERTYNAME, function ($time) use ($node) {
if (empty($time)) {
@@ -586,36 +588,154 @@ class FilesPlugin extends ServerPlugin {
if (empty($time)) {
return false;
}
- $node->setCreationTime((int) $time);
+ $node->setCreationTime((int)$time);
return true;
});
+
+ $this->handleUpdatePropertiesMetadata($propPatch, $node);
+
+ /**
+ * Disable modification of the displayname property for files and
+ * folders via PROPPATCH. See PROPFIND for more information.
+ */
+ $propPatch->handle(self::DISPLAYNAME_PROPERTYNAME, function ($displayName) {
+ return 403;
+ });
}
+
/**
- * @param string $filePath
- * @param \Sabre\DAV\INode $node
- * @throws \Sabre\DAV\Exception\BadRequest
+ * handle the update of metadata from PROPPATCH requests
+ *
+ * @param PropPatch $propPatch
+ * @param Node $node
+ *
+ * @throws FilesMetadataException
*/
- 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'];
+ private function handleUpdatePropertiesMetadata(PropPatch $propPatch, Node $node): void {
+ $userId = $this->userSession->getUser()?->getUID();
+ if ($userId === null) {
+ return;
+ }
+
+ $accessRight = $this->getMetadataFileAccessRight($node, $userId);
+ $filesMetadataManager = $this->initFilesMetadataManager();
+ $knownMetadata = $filesMetadataManager->getKnownMetadata();
+
+ foreach ($propPatch->getRemainingMutations() as $mutation) {
+ if (!str_starts_with($mutation, self::FILE_METADATA_PREFIX)) {
+ continue;
}
+
+ $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
+ if ($knownMetadata->getEditPermission($metadataKey) < $accessRight) {
+ 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);
+ } catch (FilesMetadataNotFoundException) {
+ $type = IMetadataValueWrapper::TYPE_STRING;
+ }
+
+ switch ($type) {
+ case IMetadataValueWrapper::TYPE_STRING:
+ $metadata->setString($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
+ break;
+ case IMetadataValueWrapper::TYPE_INT:
+ $metadata->setInt($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
+ break;
+ case IMetadataValueWrapper::TYPE_FLOAT:
+ $metadata->setFloat($metadataKey, $value);
+ break;
+ case IMetadataValueWrapper::TYPE_BOOL:
+ $metadata->setBool($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
+ break;
+ case IMetadataValueWrapper::TYPE_ARRAY:
+ $metadata->setArray($metadataKey, $value);
+ break;
+ case IMetadataValueWrapper::TYPE_STRING_LIST:
+ $metadata->setStringList($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
+ break;
+ case IMetadataValueWrapper::TYPE_INT_LIST:
+ $metadata->setIntList($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
+ break;
+ }
+
+ $filesMetadataManager->saveMetadata($metadata);
+
+ return true;
+ }
+ );
}
+ }
- // 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;
+ /**
+ * init default internal metadata
+ *
+ * @return IFilesMetadataManager
+ */
+ private function initFilesMetadataManager(): IFilesMetadataManager {
+ /** @var IFilesMetadataManager $manager */
+ $manager = \OCP\Server::get(IFilesMetadataManager::class);
+ $manager->initMetadata('files-live-photo', IMetadataValueWrapper::TYPE_STRING, false, IMetadataValueWrapper::EDIT_REQ_OWNERSHIP);
+
+ return $manager;
+ }
+
+ /**
+ * based on owner and shares, returns the bottom limit to update related metadata
+ *
+ * @param Node $node
+ * @param string $userId
+ *
+ * @return int
+ */
+ private function getMetadataFileAccessRight(Node $node, string $userId): int {
+ if ($node->getOwner()?->getUID() === $userId) {
+ return IMetadataValueWrapper::EDIT_REQ_OWNERSHIP;
+ } else {
+ $filePermissions = $node->getSharePermissions($userId);
+ if ($filePermissions & Constants::PERMISSION_UPDATE) {
+ return IMetadataValueWrapper::EDIT_REQ_WRITE_PERMISSION;
+ }
}
- $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);
+
+ return IMetadataValueWrapper::EDIT_REQ_READ_PERMISSION;
+ }
+
+ /**
+ * @param string $filePath
+ * @param ?\Sabre\DAV\INode $node
+ * @return void
+ * @throws \Sabre\DAV\Exception\BadRequest
+ */
+ public function sendFileIdHeader($filePath, ?\Sabre\DAV\INode $node = null) {
+ // we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder
+ 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 4876e9ad8f3..b59d1373af5 100644
--- a/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php
@@ -1,35 +1,18 @@
<?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;
use OCP\ITagManager;
use OCP\IUserSession;
@@ -45,9 +28,9 @@ use Sabre\DAV\Xml\Element\Response;
use Sabre\DAV\Xml\Response\MultiStatus;
class FilesReportPlugin extends ServerPlugin {
-
// namespace
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
+ public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
public const REPORT_NAME = '{http://owncloud.org/ns}filter-files';
public const SYSTEMTAG_PROPERTYNAME = '{http://owncloud.org/ns}systemtag';
public const CIRCLE_PROPERTYNAME = '{http://owncloud.org/ns}circle';
@@ -60,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
@@ -117,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;
}
/**
@@ -186,6 +117,7 @@ class FilesReportPlugin extends ServerPlugin {
}
$ns = '{' . $this::NS_OWNCLOUD . '}';
+ $ncns = '{' . $this::NS_NEXTCLOUD . '}';
$requestedProps = [];
$filterRules = [];
@@ -199,6 +131,14 @@ class FilesReportPlugin extends ServerPlugin {
foreach ($reportProps['value'] as $propVal) {
$requestedProps[] = $propVal['name'];
}
+ } elseif ($name === '{DAV:}limit') {
+ foreach ($reportProps['value'] as $propVal) {
+ if ($propVal['name'] === '{DAV:}nresults') {
+ $limit = (int)$propVal['value'];
+ } elseif ($propVal['name'] === $ncns . 'firstresult') {
+ $offset = (int)$propVal['value'];
+ }
+ }
}
}
@@ -209,13 +149,32 @@ class FilesReportPlugin extends ServerPlugin {
// gather all file ids matching filter
try {
- $resultFileIds = $this->processFilterRules($filterRules);
+ $resultFileIds = $this->processFilterRulesForFileIDs($filterRules);
+ // no logic in circles and favorites for paging, we always have all results, and slice later on
+ $resultFileIds = array_slice($resultFileIds, $offset ?? 0, $limit ?? null);
+ // fetching nodes has paging on DB level – therefore we cannot mix and slice the results, similar
+ // 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 = [];
+ foreach ($resultNodes as $entry) {
+ if ($entry) {
+ $results[] = $this->wrapNode($entry);
+ }
}
// find sabre nodes by file id, restricted to the root node path
- $results = $this->findNodesByFileIds($reportTargetNode, $resultFileIds);
+ $additionalNodes = $this->findNodesByFileIds($reportTargetNode, $resultFileIds);
+ if ($additionalNodes && $results) {
+ $results = array_uintersect($results, $additionalNodes, function (Node $a, Node $b): int {
+ return $a->getId() - $b->getId();
+ });
+ } elseif (!$results && $additionalNodes) {
+ $results = $additionalNodes;
+ }
$filesUri = $this->getFilesBaseUri($uri, $reportTargetNode->getPath());
$responses = $this->prepareResponses($filesUri, $requestedProps, $results);
@@ -225,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);
@@ -261,19 +220,13 @@ class FilesReportPlugin extends ServerPlugin {
*
* @param array $filterRules
* @return array array of unique file id results
- *
- * @throws TagNotFoundException whenever a tag was not found
*/
- protected function processFilterRules($filterRules) {
+ protected function processFilterRulesForFileIDs(array $filterRules): array {
$ns = '{' . $this::NS_OWNCLOUD . '}';
- $resultFileIds = null;
- $systemTagIds = [];
+ $resultFileIds = [];
$circlesIds = [];
$favoriteFilter = null;
foreach ($filterRules as $filterRule) {
- if ($filterRule['name'] === $ns . 'systemtag') {
- $systemTagIds[] = $filterRule['value'];
- }
if ($filterRule['name'] === self::CIRCLE_PROPERTYNAME) {
$circlesIds[] = $filterRule['value'];
}
@@ -289,15 +242,6 @@ class FilesReportPlugin extends ServerPlugin {
}
}
- if (!empty($systemTagIds)) {
- $fileIds = $this->getSystemTagFileIds($systemTagIds);
- if (empty($resultFileIds)) {
- $resultFileIds = $fileIds;
- } else {
- $resultFileIds = array_intersect($fileIds, $resultFileIds);
- }
- }
-
if (!empty($circlesIds)) {
$fileIds = $this->getCirclesFileIds($circlesIds);
if (empty($resultFileIds)) {
@@ -310,47 +254,46 @@ class FilesReportPlugin extends ServerPlugin {
return $resultFileIds;
}
- private function getSystemTagFileIds($systemTagIds) {
- $resultFileIds = null;
-
- // check user permissions, if applicable
- if (!$this->isAdmin()) {
- // check visibility/permission
- $tags = $this->tagManager->getTagsByIds($systemTagIds);
- $unknownTagIds = [];
- foreach ($tags as $tag) {
- if (!$tag->isUserVisible()) {
- $unknownTagIds[] = $tag->getId();
- }
- }
-
- if (!empty($unknownTagIds)) {
- throw new TagNotFoundException('Tag with ids ' . implode(', ', $unknownTagIds) . ' not found');
+ protected function processFilterRulesForFileNodes(array $filterRules, ?int $limit, ?int $offset): array {
+ $systemTagIds = [];
+ foreach ($filterRules as $filterRule) {
+ if ($filterRule['name'] === self::SYSTEMTAG_PROPERTYNAME) {
+ $systemTagIds[] = $filterRule['value'];
}
}
- // fetch all file ids and intersect them
- foreach ($systemTagIds as $systemTagId) {
- $fileIds = $this->tagMapper->getObjectIdsForTags($systemTagId, 'files');
+ $nodes = [];
- if (empty($fileIds)) {
- // This tag has no files, nothing can ever show up
- return [];
- }
+ if (!empty($systemTagIds)) {
+ $tags = $this->tagManager->getTagsByIds($systemTagIds, $this->userSession->getUser());
- // first run ?
- if ($resultFileIds === null) {
- $resultFileIds = $fileIds;
- } else {
- $resultFileIds = array_intersect($resultFileIds, $fileIds);
+ // For we run DB queries per tag and require intersection, we cannot apply limit and offset for DB queries on multi tag search.
+ $oneTagSearch = count($tags) === 1;
+ $dbLimit = $oneTagSearch ? $limit ?? 0 : 0;
+ $dbOffset = $oneTagSearch ? $offset ?? 0 : 0;
+
+ foreach ($tags as $tag) {
+ $tagName = $tag->getName();
+ $tmpNodes = $this->userFolder->searchBySystemTag($tagName, $this->userSession->getUser()->getUID(), $dbLimit, $dbOffset);
+ if (count($nodes) === 0) {
+ $nodes = $tmpNodes;
+ } else {
+ $nodes = array_uintersect($nodes, $tmpNodes, function (INode $a, INode $b): int {
+ return $a->getId() - $b->getId();
+ });
+ }
+ if ($nodes === []) {
+ // there cannot be a common match when nodes are empty early.
+ return $nodes;
+ }
}
- if (empty($resultFileIds)) {
- // Empty intersection, nothing can show up anymore
- return [];
+ if (!$oneTagSearch && ($limit !== null || $offset !== null)) {
+ $nodes = array_slice($nodes, $offset, $limit);
}
}
- return $resultFileIds;
+
+ return $nodes;
}
/**
@@ -362,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);
}
@@ -370,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[]
@@ -393,7 +336,6 @@ class FilesReportPlugin extends ServerPlugin {
$responses[] = new Response(
rtrim($this->server->getBaseUri(), '/') . $filesUri . $node->getPath(),
$result,
- 200
);
}
return $responses;
@@ -406,28 +348,35 @@ class FilesReportPlugin extends ServerPlugin {
* @param array $fileIds file ids
* @return Node[] array of Sabre nodes
*/
- public function findNodesByFileIds($rootNode, $fileIds) {
+ public function findNodesByFileIds(Node $rootNode, array $fileIds): array {
+ if (empty($fileIds)) {
+ return [];
+ }
$folder = $this->userFolder;
if (trim($rootNode->getPath(), '/') !== '') {
+ /** @var Folder $folder */
$folder = $folder->get($rootNode->getPath());
}
$results = [];
foreach ($fileIds as $fileId) {
- $entry = $folder->getById($fileId);
+ $entry = $folder->getFirstNodeById((int)$fileId);
if ($entry) {
- $entry = current($entry);
- if ($entry instanceof \OCP\Files\File) {
- $results[] = new File($this->fileView, $entry);
- } elseif ($entry instanceof \OCP\Files\Folder) {
- $results[] = new Directory($this->fileView, $entry);
- }
+ $results[] = $this->wrapNode($entry);
}
}
return $results;
}
+ protected function wrapNode(INode $node): File|Directory {
+ if ($node instanceof \OCP\Files\File) {
+ return new File($this->fileView, $node);
+ } else {
+ return new Directory($this->fileView, $node);
+ }
+ }
+
/**
* Returns whether the currently logged in user is an administrator
*/
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 e7e3b273b98..d5ab7f09dfa 100644
--- a/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php
+++ b/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php
@@ -1,32 +1,13 @@
<?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;
+use OCA\DAV\Exception\ServerMaintenanceMode;
use OCP\IConfig;
use OCP\IL10N;
use OCP\Util;
@@ -35,10 +16,7 @@ use Sabre\DAV\ServerPlugin;
class MaintenancePlugin extends ServerPlugin {
- /** @var IConfig */
- private $config;
-
- /** @var \OCP\IL10N */
+ /** @var IL10N */
private $l10n;
/**
@@ -51,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');
}
@@ -82,10 +62,10 @@ class MaintenancePlugin extends ServerPlugin {
*/
public function checkMaintenanceMode() {
if ($this->config->getSystemValueBool('maintenance')) {
- throw new ServiceUnavailable($this->l10n->t('System is in maintenance mode.'));
+ throw new ServerMaintenanceMode($this->l10n->t('System is in maintenance mode.'));
}
if (Util::needUpgrade()) {
- throw new ServiceUnavailable($this->l10n->t('Upgrade needed'));
+ throw new ServerMaintenanceMode($this->l10n->t('Upgrade needed'));
}
return true;
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 e4517068f42..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,19 @@ 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\Share\IShare;
+use OCP\Server;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
abstract class Node implements \Sabre\DAV\INode {
-
- /**
- * @var \OC\Files\View
- */
- protected $fileView;
-
/**
* The path to the current node
*
@@ -68,10 +39,7 @@ abstract class Node implements \Sabre\DAV\INode {
*/
protected $property_cache = null;
- /**
- * @var \OCP\Files\FileInfo
- */
- protected $info;
+ protected FileInfo $info;
/**
* @var IManager
@@ -82,39 +50,45 @@ abstract class Node implements \Sabre\DAV\INode {
/**
* Sets up the node, expects a full path name
- *
- * @param \OC\Files\View $view
- * @param \OCP\Files\FileInfo $info
- * @param IManager $shareManager
*/
- 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 {
- $root = \OC::$server->get(IRootFolder::class);
+ // The Node API assumes that the view passed doesn't have a fake root
+ $rootView = Server::get(View::class);
+ $root = Server::get(IRootFolder::class);
if ($info->getType() === FileInfo::TYPE_FOLDER) {
- $this->node = new Folder($root, $view, $this->path, $info);
+ $this->node = new Folder($root, $rootView, $this->fileView->getAbsolutePath($this->path), $info);
} else {
- $this->node = new File($root, $view, $this->path, $info);
+ $this->node = new File($root, $rootView, $this->fileView->getAbsolutePath($this->path), $info);
}
}
}
- protected function refreshInfo() {
- $this->info = $this->fileView->getFileInfo($this->path);
- $root = \OC::$server->get(IRootFolder::class);
+ 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);
+ }
+ $this->info = $info;
+ $root = Server::get(IRootFolder::class);
+ $rootView = Server::get(View::class);
if ($this->info->getType() === FileInfo::TYPE_FOLDER) {
- $this->node = new Folder($root, $this->fileView, $this->path, $this->info);
+ $this->node = new Folder($root, $rootView, $this->path, $this->info);
} else {
- $this->node = new File($root, $this->fileView, $this->path, $this->info);
+ $this->node = new File($root, $rootView, $this->path, $this->info);
}
}
@@ -144,22 +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;
@@ -232,9 +205,10 @@ abstract class Node implements \Sabre\DAV\INode {
/**
* Returns the size of the node, in bytes
*
- * @return integer
+ * @psalm-suppress ImplementedReturnTypeMismatch \Sabre\DAV\IFile::getSize signature does not support 32bit
+ * @return int|float
*/
- public function getSize() {
+ public function getSize(): int|float {
return $this->info->getSize();
}
@@ -251,10 +225,8 @@ abstract class Node implements \Sabre\DAV\INode {
* @return string|null
*/
public function getFileId() {
- if ($this->info->getId()) {
- $instanceId = \OC_Util::getInstanceId();
- $id = sprintf('%08d', $this->info->getId());
- return $id . $instanceId;
+ if ($id = $this->info->getId()) {
+ return DavUtil::getDavFileId($id);
}
return null;
@@ -267,12 +239,15 @@ abstract class Node implements \Sabre\DAV\INode {
return $this->info->getId();
}
+ public function getInternalPath(): string {
+ return $this->info->getInternalPath();
+ }
+
/**
* @param string $user
* @return int
*/
public function getSharePermissions($user) {
-
// check of we access a federated share
if ($user !== null) {
try {
@@ -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();
@@ -303,98 +278,88 @@ abstract class Node implements \Sabre\DAV\INode {
$mountpoint = $this->info->getMountPoint();
if (!($mountpoint instanceof MoveableMount)) {
$mountpointpath = $mountpoint->getMountPoint();
- if (substr($mountpointpath, -1) === '/') {
+ if (str_ends_with($mountpointpath, '/')) {
$mountpointpath = substr($mountpointpath, 0, -1);
}
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;
}
/**
- * @param string $user
- * @return string
+ * @return array
*/
- public function getNoteFromShare($user) {
- if ($user === null) {
- return '';
+ public function getShareAttributes(): array {
+ try {
+ $storage = $this->node->getStorage();
+ } catch (NotFoundException $e) {
+ return [];
}
- $types = [
- IShare::TYPE_USER,
- IShare::TYPE_GROUP,
- IShare::TYPE_CIRCLE,
- IShare::TYPE_ROOM
- ];
-
- foreach ($types as $shareType) {
- $shares = $this->shareManager->getSharedWith($user, $shareType, $this, -1);
- foreach ($shares as $share) {
- $note = $share->getNote();
- if ($share->getShareOwner() !== $user && !empty($note)) {
- return $note;
- }
+ $attributes = [];
+ if ($storage->instanceOfStorage(ISharedStorage::class)) {
+ /** @var ISharedStorage $storage */
+ $attributes = $storage->getShare()->getAttributes();
+ if ($attributes === null) {
+ return [];
+ } else {
+ return $attributes->toArray();
+ }
+ }
+
+ return $attributes;
+ }
+
+ public function getNoteFromShare(?string $user): ?string {
+ try {
+ $storage = $this->node->getStorage();
+ } catch (NotFoundException) {
+ return null;
+ }
+
+ 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();
}
- return '';
+ return null;
}
/**
* @return string
*/
public function getDavPermissions() {
- $p = '';
- if ($this->info->isShared()) {
- $p .= 'S';
- }
- if ($this->info->isShareable()) {
- $p .= 'R';
- }
- if ($this->info->isMounted()) {
- $p .= 'M';
- }
- if ($this->info->isReadable()) {
- $p .= 'G';
- }
- if ($this->info->isDeletable()) {
- $p .= 'D';
- }
- if ($this->info->isUpdateable()) {
- $p .= 'NV'; // Renameable, Moveable
- }
- if ($this->info->getType() === \OCP\Files\FileInfo::TYPE_FILE) {
- if ($this->info->isUpdateable()) {
- $p .= 'W';
- }
- } else {
- if ($this->info->isCreatable()) {
- $p .= 'CK';
- }
- }
- return $p;
+ return DavUtil::getDavPermissions($this->info);
}
public function getOwner() {
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());
}
}
@@ -428,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 c3f06f95783..d6ea9fd887d 100644
--- a/apps/dav/lib/Connector/Sabre/Principal.php
+++ b/apps/dav/lib/Connector/Sabre/Principal.php
@@ -1,46 +1,21 @@
<?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;
+use OCP\Accounts\IAccountProperty;
+use OCP\Accounts\PropertyDoesNotExistException;
use OCP\App\IAppManager;
use OCP\AppFramework\QueryException;
use OCP\Constants;
@@ -58,21 +33,6 @@ use Sabre\DAVACL\PrincipalBackend\BackendInterface;
class Principal implements BackendInterface {
- /** @var IUserManager */
- private $userManager;
-
- /** @var IGroupManager */
- private $groupManager;
-
- /** @var IShareManager */
- private $shareManager;
-
- /** @var IUserSession */
- private $userSession;
-
- /** @var IAppManager */
- private $appManager;
-
/** @var string */
private $principalPrefix;
@@ -82,38 +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,
- IShareManager $shareManager,
- IUserSession $userSession,
- IAppManager $appManager,
- ProxyMapper $proxyMapper,
- KnownUserService $knownUserService,
- IConfig $config,
- IFactory $languageFactory,
- string $principalPrefix = 'principals/users/') {
- $this->userManager = $userManager;
- $this->groupManager = $groupManager;
- $this->shareManager = $shareManager;
- $this->userSession = $userSession;
- $this->appManager = $appManager;
+ 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,
+ 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 {
@@ -200,6 +147,16 @@ class Principal implements BackendInterface {
'{DAV:}displayname' => $group->getDisplayName(),
];
}
+ } elseif ($prefix === 'principals/system') {
+ return [
+ 'uri' => 'principals/system/' . $name,
+ '{DAV:}displayname' => $this->languageFactory->get('dav')->t('Accounts'),
+ ];
+ } elseif ($prefix === 'principals/shares') {
+ return [
+ 'uri' => 'principals/shares/' . $name,
+ '{DAV:}displayname' => $name,
+ ];
}
return null;
}
@@ -229,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());
}
}
@@ -247,6 +207,7 @@ class Principal implements BackendInterface {
* @return int
*/
public function updatePrincipal($path, PropPatch $propPatch) {
+ // Updating schedule-default-calendar-URL is handled in CustomPropertiesBackend
return 0;
}
@@ -270,6 +231,8 @@ class Principal implements BackendInterface {
$limitEnumerationGroup = $this->shareManager->limitEnumerationToGroups();
$limitEnumerationPhone = $this->shareManager->limitEnumerationToPhone();
$allowEnumerationFullMatch = $this->shareManager->allowEnumerationFullMatch();
+ $ignoreSecondDisplayName = $this->shareManager->ignoreSecondDisplayName();
+ $matchEmail = $this->shareManager->matchEmail();
// If sharing is restricted to group members only,
// return only members that have groups in common
@@ -298,7 +261,7 @@ class Principal implements BackendInterface {
switch ($prop) {
case '{http://sabredav.org/ns}email-address':
if (!$allowEnumeration) {
- if ($allowEnumerationFullMatch) {
+ if ($allowEnumerationFullMatch && $matchEmail) {
$users = $this->userManager->getByEmail($value);
} else {
$users = [];
@@ -349,8 +312,9 @@ class Principal implements BackendInterface {
if ($allowEnumerationFullMatch) {
$lowerSearch = strtolower($value);
$users = $this->userManager->searchDisplayName($value, $searchLimit);
- $users = \array_filter($users, static function (IUser $user) use ($lowerSearch) {
- return strtolower($user->getDisplayName()) === $lowerSearch;
+ $users = \array_filter($users, static function (IUser $user) use ($lowerSearch, $ignoreSecondDisplayName) {
+ $lowerDisplayName = strtolower($user->getDisplayName());
+ return $lowerDisplayName === $lowerSearch || ($ignoreSecondDisplayName && trim(preg_replace('/ \(.*\)$/', '', $lowerDisplayName)) === $lowerSearch);
});
} else {
$users = [];
@@ -471,7 +435,7 @@ class Principal implements BackendInterface {
$restrictGroups = $this->groupManager->getUserGroupIds($user);
}
- if (strpos($uri, 'mailto:') === 0) {
+ if (str_starts_with($uri, 'mailto:')) {
if ($principalPrefix === 'principals/users') {
$users = $this->userManager->getByEmail(substr($uri, 7));
if (count($users) !== 1) {
@@ -489,7 +453,7 @@ class Principal implements BackendInterface {
return $this->principalPrefix . '/' . $user->getUID();
}
}
- if (substr($uri, 0, 10) === 'principal:') {
+ if (str_starts_with($uri, 'principal:')) {
$principal = substr($uri, 10);
$principal = $this->getPrincipalByPath($principal);
if ($principal !== null) {
@@ -503,6 +467,7 @@ class Principal implements BackendInterface {
/**
* @param IUser $user
* @return array
+ * @throws PropertyDoesNotExistException
*/
protected function userToPrincipal($user) {
$userId = $user->getUID();
@@ -514,11 +479,18 @@ class Principal implements BackendInterface {
'{http://nextcloud.com/ns}language' => $this->languageFactory->getUserLanguage($user),
];
+ $account = $this->accountManager->getAccount($user);
+ $alternativeEmails = array_map(fn (IAccountProperty $property) => 'mailto:' . $property->getValue(), $account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL)->getProperties());
+
$email = $user->getSystemEMailAddress();
if (!empty($email)) {
$principal['{http://sabredav.org/ns}email-address'] = $email;
}
+ if (!empty($alternativeEmails)) {
+ $principal['{DAV:}alternate-URI-set'] = $alternativeEmails;
+ }
+
return $principal;
}
@@ -536,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) {
@@ -561,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 {
@@ -576,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);
@@ -588,4 +560,44 @@ class Principal implements BackendInterface {
return [];
}
+
+ /**
+ * Get all email addresses associated to a principal.
+ *
+ * @param array $principal Data from getPrincipal*()
+ * @return string[] All email addresses without the mailto: prefix
+ */
+ public function getEmailAddressesOfPrincipal(array $principal): array {
+ $emailAddresses = [];
+
+ if (isset($principal['{http://sabredav.org/ns}email-address'])) {
+ $emailAddresses[] = $principal['{http://sabredav.org/ns}email-address'];
+ }
+
+ if (isset($principal['{DAV:}alternate-URI-set'])) {
+ foreach ($principal['{DAV:}alternate-URI-set'] as $address) {
+ if (str_starts_with($address, 'mailto:')) {
+ $emailAddresses[] = substr($address, 7);
+ }
+ }
+ }
+
+ if (isset($principal['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'])) {
+ foreach ($principal['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'] as $address) {
+ if (str_starts_with($address, 'mailto:')) {
+ $emailAddresses[] = substr($address, 7);
+ }
+ }
+ }
+
+ if (isset($principal['{http://calendarserver.org/ns/}email-address-set'])) {
+ foreach ($principal['{http://calendarserver.org/ns/}email-address-set'] as $address) {
+ if (str_starts_with($address, 'mailto:')) {
+ $emailAddresses[] = substr($address, 7);
+ }
+ }
+ }
+
+ return array_values(array_unique($emailAddresses));
+ }
}
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 d0251b2755f..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;
@@ -61,7 +44,7 @@ class PropfindCompressionPlugin extends ServerPlugin {
return $response;
}
- if (strpos($header, 'gzip') !== false) {
+ if (str_contains($header, 'gzip')) {
$body = $response->getBody();
if (is_string($body)) {
$response->setHeader('Content-Encoding', 'gzip');
diff --git a/apps/dav/lib/Connector/Sabre/PublicAuth.php b/apps/dav/lib/Connector/Sabre/PublicAuth.php
new file mode 100644
index 00000000000..2ca1c25e2f6
--- /dev/null
+++ b/apps/dav/lib/Connector/Sabre/PublicAuth.php
@@ -0,0 +1,227 @@
+<?php
+
+declare(strict_types=1);
+
+
+/**
+ * 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;
+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;
+use Sabre\HTTP\ResponseInterface;
+
+/**
+ * Class PublicAuth
+ *
+ * @package OCA\DAV\Connector
+ */
+class PublicAuth extends AbstractBasic {
+ private const BRUTEFORCE_ACTION = 'public_dav_auth';
+ public const DAV_AUTHENTICATED = 'public_link_authenticated';
+
+ private ?IShare $share = null;
+
+ 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 Defaults();
+ $this->realm = $defaults->getName();
+ }
+
+ /**
+ * @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,
+ $response
+ );
+
+ $userpass = $auth->getCredentials();
+ // If authentication provided, checking its validity
+ if ($userpass && !$this->validateUserPass($userpass[0], $userpass[1])) {
+ return [false, 'Username or password was incorrect'];
+ }
+
+ return $this->checkToken();
+ } catch (NotAuthenticated|MaxDelayReached $e) {
+ $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
+ throw $e;
+ } catch (PreconditionFailed $e) {
+ $response->setHeader(
+ 'Location',
+ $this->urlGenerator->linkToRoute(
+ 'files_sharing.share.showShare',
+ [ 'token' => $this->getToken() ],
+ ),
+ );
+ throw $e;
+ } catch (\Exception $e) {
+ $class = get_class($e);
+ $msg = $e->getMessage();
+ $this->logger->error($e->getMessage(), ['exception' => $e]);
+ throw new ServiceUnavailable("$class: $msg");
+ }
+ }
+
+ /**
+ * Extract token from request url
+ * @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();
+ }
+
+ return $splittedPath[3];
+ }
+
+ /**
+ * Check token validity
+ *
+ * @throws NotFound
+ * @throws NotAuthenticated
+ */
+ private function checkToken(): array {
+ $token = $this->getToken();
+
+ try {
+ /** @var IShare $share */
+ $share = $this->shareManager->getShareByToken($token);
+ } catch (ShareNotFound $e) {
+ $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
+ throw new NotFound();
+ }
+
+ $this->share = $share;
+ \OC_User::setIncognitoMode(true);
+
+ // If already authenticated
+ if ($this->session->exists(self::DAV_AUTHENTICATED)
+ && $this->session->get(self::DAV_AUTHENTICATED) === $share->getId()) {
+ return [true, $this->principalPrefix . $token];
+ }
+
+ // If the share is protected but user is not authenticated
+ if ($share->getPassword() !== null) {
+ $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
+ throw new NotAuthenticated();
+ }
+
+ return [true, $this->principalPrefix . $token];
+ }
+
+ /**
+ * Validates a username and password
+ *
+ * This method should return true or false depending on if login
+ * succeeded.
+ *
+ * @param string $username
+ * @param string $password
+ *
+ * @return bool
+ * @throws NotAuthenticated
+ */
+ protected function validateUserPass($username, $password) {
+ $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION);
+
+ try {
+ $share = $this->getShare();
+ } catch (ShareNotFound $e) {
+ $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
+ return false;
+ }
+
+ \OC_User::setIncognitoMode(true);
+
+ // check if the share is password protected
+ if ($share->getPassword() !== null) {
+ if ($share->getShareType() === IShare::TYPE_LINK
+ || $share->getShareType() === IShare::TYPE_EMAIL
+ || $share->getShareType() === IShare::TYPE_CIRCLE) {
+ if ($this->shareManager->checkPassword($share, $password)) {
+ // If not set, set authenticated session cookie
+ if (!$this->session->exists(self::DAV_AUTHENTICATED)
+ || $this->session->get(self::DAV_AUTHENTICATED) !== $share->getId()) {
+ $this->session->set(self::DAV_AUTHENTICATED, $share->getId());
+ }
+ return true;
+ }
+
+ if ($this->session->exists(PublicAuth::DAV_AUTHENTICATED)
+ && $this->session->get(PublicAuth::DAV_AUTHENTICATED) === $share->getId()) {
+ return true;
+ }
+
+ if (in_array('XMLHttpRequest', explode(',', $this->request->getHeader('X-Requested-With')))) {
+ // do not re-authenticate over ajax, use dummy auth name to prevent browser popup
+ http_response_code(401);
+ header('WWW-Authenticate: DummyBasic realm="' . $this->realm . '"');
+ throw new NotAuthenticated('Cannot authenticate over ajax calls');
+ }
+
+ $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
+ return false;
+ } elseif ($share->getShareType() === IShare::TYPE_REMOTE) {
+ return true;
+ }
+
+ $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
+ return false;
+ }
+
+ return true;
+ }
+
+ public function getShare(): IShare {
+ $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 f2b652e3320..bbb378edc9b 100644
--- a/apps/dav/lib/Connector/Sabre/QuotaPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/QuotaPlugin.php
@@ -1,40 +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.
@@ -44,10 +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,7 +57,9 @@ 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);
}
/**
@@ -90,6 +71,19 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
* @param bool $modified modified
*/
public function beforeCreateFile($uri, $data, INode $parent, $modified) {
+ $request = $this->server->httpRequest;
+ if ($parent instanceof UploadFolder && $request->getHeader('Destination')) {
+ // If chunked upload and Total-Length header is set, use that
+ // value for quota check. This allows us to also check quota while
+ // uploading chunks and not only when the file is assembled.
+ $length = $request->getHeader('OC-Total-Length');
+ $destinationPath = $this->server->calculateUri($request->getHeader('Destination'));
+ $quotaPath = $this->getPathForDestination($destinationPath);
+ if ($quotaPath && is_numeric($length)) {
+ return $this->checkQuota($quotaPath, (int)$length);
+ }
+ }
+
if (!$parent instanceof Node) {
return;
}
@@ -98,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
@@ -114,40 +133,76 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
}
/**
- * Check if we're moving a Futurefile in which case we need to check
+ * Check if we're moving a FutureFile in which case we need to check
* the quota on the target destination.
- *
- * @param string $source source path
- * @param string $destination destination path
*/
- public function beforeMove($source, $destination) {
- $sourceNode = $this->server->tree->getNodeForPath($source);
+ public function beforeMove(string $sourcePath, string $destinationPath): bool {
+ $sourceNode = $this->server->tree->getNodeForPath($sourcePath);
if (!$sourceNode instanceof FutureFile) {
- return;
+ return true;
}
- // get target node for proper path conversion
- if ($this->server->tree->nodeExists($destination)) {
- $destinationNode = $this->server->tree->getNodeForPath($destination);
- $path = $destinationNode->getPath();
- } else {
- $parentNode = $this->server->tree->getNodeForPath(dirname($destination));
- $path = $parentNode->getPath();
+ try {
+ // The final path is not known yet, we check the quota on the parent
+ $path = $this->getPathForDestination($destinationPath);
+ } catch (\Exception $e) {
+ return true;
}
return $this->checkQuota($path, $sourceNode->getSize());
}
+ /**
+ * Check quota on the target destination before a copy.
+ */
+ public function beforeCopy(string $sourcePath, string $destinationPath): bool {
+ $sourceNode = $this->server->tree->getNodeForPath($sourcePath);
+ if (!$sourceNode instanceof Node) {
+ return true;
+ }
+
+ try {
+ $path = $this->getPathForDestination($destinationPath);
+ } catch (\Exception $e) {
+ return true;
+ }
+
+ return $this->checkQuota($path, $sourceNode->getSize());
+ }
+
+ private function getPathForDestination(string $destinationPath): string {
+ // get target node for proper path conversion
+ if ($this->server->tree->nodeExists($destinationPath)) {
+ $destinationNode = $this->server->tree->getNodeForPath($destinationPath);
+ if (!$destinationNode instanceof Node) {
+ throw new \Exception('Invalid destination node');
+ }
+ return $destinationNode->getPath();
+ }
+
+ $parent = dirname($destinationPath);
+ if ($parent === '.') {
+ $parent = '';
+ }
+
+ $parentNode = $this->server->tree->getNodeForPath($parent);
+ if (!$parentNode instanceof Node) {
+ throw new \Exception('Invalid destination node');
+ }
+
+ return $parentNode->getPath();
+ }
+
/**
* This method is called before any HTTP method and validates there is enough free space to store the file
*
* @param string $path relative to the users home
- * @param int $length
+ * @param int|float|null $length
* @throws InsufficientStorage
* @return bool
*/
- public function checkQuota($path, $length = null) {
+ public function checkQuota(string $path, $length = null, $isDir = false) {
if ($length === null) {
$length = $this->getLength();
}
@@ -158,29 +213,21 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
$parentPath = '';
}
$req = $this->server->httpRequest;
- 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 (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");
}
}
- return true;
- }
- public function getFileChunking($info) {
- // FIXME: need a factory for better mocking support
- return new \OC_FileChunking($info);
+ return true;
}
public function getLength() {
@@ -192,11 +239,13 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
}
$ocLength = $req->getHeader('OC-Total-Length');
- if (is_numeric($length) && is_numeric($ocLength)) {
- return max($length, $ocLength);
+ if (!is_numeric($ocLength)) {
+ return $length;
}
-
- return $length;
+ if (!is_numeric($length)) {
+ return $ocLength;
+ }
+ return max($length, $ocLength);
}
/**
diff --git a/apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php b/apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php
index b281a1053b8..5484bab9237 100644
--- a/apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php
@@ -1,22 +1,9 @@
-<?php declare(strict_types=1);
+<?php
+
+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;
@@ -26,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 b13dbd20ca9..a6a27057177 100644
--- a/apps/dav/lib/Connector/Sabre/ServerFactory.php
+++ b/apps/dav/lib/Connector/Sabre/ServerFactory.php
@@ -1,137 +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 OCP\Files\Folder;
+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\ILogger;
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 Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Sabre\DAV\SimpleCollection;
class ServerFactory {
- /** @var IConfig */
- private $config;
- /** @var ILogger */
- private $logger;
- /** @var IDBConnection */
- private $databaseConnection;
- /** @var IUserSession */
- private $userSession;
- /** @var IMountManager */
- private $mountManager;
- /** @var ITagManager */
- private $tagManager;
- /** @var IRequest */
- private $request;
- /** @var IPreview */
- private $previewManager;
- /** @var EventDispatcherInterface */
- private $eventDispatcher;
- /** @var IL10N */
- private $l10n;
- /**
- * @param IConfig $config
- * @param ILogger $logger
- * @param IDBConnection $databaseConnection
- * @param IUserSession $userSession
- * @param IMountManager $mountManager
- * @param ITagManager $tagManager
- * @param IRequest $request
- * @param IPreview $previewManager
- */
public function __construct(
- IConfig $config,
- ILogger $logger,
- IDBConnection $databaseConnection,
- IUserSession $userSession,
- IMountManager $mountManager,
- ITagManager $tagManager,
- IRequest $request,
- IPreview $previewManager,
- EventDispatcherInterface $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 string $baseUri
- * @param string $requestUri
- * @param Plugin $authPlugin
* @param callable $viewCallBack callback that should return the view for the dav endpoint
- * @return Server
*/
- public function createServer($baseUri,
- $requestUri,
- Plugin $authPlugin,
- callable $viewCallBack) {
+ public function createServer(
+ bool $isPublicShare,
+ string $baseUri,
+ string $requestUri,
+ Plugin $authPlugin,
+ 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.
@@ -140,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)) {
@@ -148,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;
@@ -162,65 +137,108 @@ 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 \OCA\DAV\Connector\Sabre\File($view, $rootInfo);
+ $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 {
+ /** @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(
+ $userFolder,
+ ));
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(
- $objectTree,
+ new CustomPropertiesBackend(
+ $server,
+ $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->dispatch($event);
+ $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 6fbae0dee4a..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,
+ ) {
}
/**
@@ -79,7 +61,7 @@ class ShareTypeList implements Element {
}
/**
- * The xmlSerialize metod is called during xml writing.
+ * The xmlSerialize method is called during xml writing.
*
* @param Writer $writer
* @return void
diff --git a/apps/dav/lib/Connector/Sabre/ShareeList.php b/apps/dav/lib/Connector/Sabre/ShareeList.php
index db8c011cc45..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,15 +18,14 @@ 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,
+ ) {
}
/**
- * The xmlSerialize metod is called during xml writing.
+ * The xmlSerialize method is called during xml writing.
*
* @param Writer $writer
* @return void
diff --git a/apps/dav/lib/Connector/Sabre/SharesPlugin.php b/apps/dav/lib/Connector/Sabre/SharesPlugin.php
index 57c91e05a8c..f49e85333f3 100644
--- a/apps/dav/lib/Connector/Sabre/SharesPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/SharesPlugin.php
@@ -1,39 +1,23 @@
<?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;
use OCP\Files\NotFoundException;
use OCP\IUserSession;
+use OCP\Share\IManager;
use OCP\Share\IShare;
use Sabre\DAV\PropFind;
+use Sabre\DAV\Server;
+use Sabre\DAV\Tree;
/**
* Sabre Plugin to provide share-related properties
@@ -50,40 +34,19 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
* @var \Sabre\DAV\Server
*/
private $server;
-
- /** @var \OCP\Share\IManager */
- private $shareManager;
-
- /** @var \Sabre\DAV\Tree */
- private $tree;
-
- /** @var string */
- private $userId;
-
- /** @var \OCP\Files\Folder */
- private $userFolder;
+ private string $userId;
/** @var IShare[][] */
- private $cachedShares = [];
-
+ private array $cachedShares = [];
/** @var string[] */
- private $cachedFolders = [];
+ private array $cachedFolders = [];
- /**
- * @param \Sabre\DAV\Tree $tree tree
- * @param IUserSession $userSession user session
- * @param \OCP\Files\Folder $userFolder user home folder
- * @param \OCP\Share\IManager $shareManager share manager
- */
public function __construct(
- \Sabre\DAV\Tree $tree,
- IUserSession $userSession,
- \OCP\Files\Folder $userFolder,
- \OCP\Share\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();
}
@@ -95,9 +58,9 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
*
* This method should set up the required event subscriptions.
*
- * @param \Sabre\DAV\Server $server
+ * @return void
*/
- public function initialize(\Sabre\DAV\Server $server) {
+ public function initialize(Server $server) {
$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
$server->xml->elementMap[self::SHARETYPES_PROPERTYNAME] = ShareTypeList::class;
$server->protectedProperties[] = self::SHARETYPES_PROPERTYNAME;
@@ -108,10 +71,10 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
}
/**
- * @param \OCP\Files\Node $node
+ * @param Node $node
* @return IShare[]
*/
- private function getShare(\OCP\Files\Node $node): array {
+ private function getShare(Node $node): array {
$result = [];
$requestedShareTypes = [
IShare::TYPE_USER,
@@ -122,19 +85,31 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
IShare::TYPE_ROOM,
IShare::TYPE_CIRCLE,
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;
}
@@ -156,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 [];
}
/**
@@ -187,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();
@@ -209,7 +188,7 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
}
}
- $propFind->handle(self::SHARETYPES_PROPERTYNAME, function () use ($sabreNode) {
+ $propFind->handle(self::SHARETYPES_PROPERTYNAME, function () use ($sabreNode): ShareTypeList {
$shares = $this->getShares($sabreNode);
$shareTypes = array_unique(array_map(function (IShare $share) {
@@ -219,7 +198,7 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
return new ShareTypeList($shareTypes);
});
- $propFind->handle(self::SHAREES_PROPERTYNAME, function () use ($sabreNode) {
+ $propFind->handle(self::SHAREES_PROPERTYNAME, function () use ($sabreNode): ShareeList {
$shares = $this->getShares($sabreNode);
return new ShareeList($shares);
diff --git a/apps/dav/lib/Connector/Sabre/TagList.php b/apps/dav/lib/Connector/Sabre/TagList.php
index bbb938fb27d..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,
+ ) {
}
/**
@@ -95,7 +76,7 @@ class TagList implements Element {
}
/**
- * The xmlSerialize metod is called during xml writing.
+ * The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
diff --git a/apps/dav/lib/Connector/Sabre/TagsPlugin.php b/apps/dav/lib/Connector/Sabre/TagsPlugin.php
index da5dd874905..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 4305d6daaef..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 955400998cf..ea209168123 100644
--- a/apps/dav/lib/Controller/DirectController.php
+++ b/apps/dav/lib/Controller/DirectController.php
@@ -3,36 +3,23 @@
declare(strict_types=1);
/**
- * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Iscle <albertiscle9@gmail.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\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;
use OCP\AppFramework\OCS\OCSNotFoundException;
use OCP\AppFramework\OCSController;
use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\Events\BeforeDirectFileDownloadEvent;
use OCP\Files\File;
use OCP\Files\IRootFolder;
use OCP\IRequest;
@@ -41,52 +28,39 @@ 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;
-
-
- public function __construct(string $appName,
- IRequest $request,
- IRootFolder $rootFolder,
- string $userId,
- DirectMapper $mapper,
- ISecureRandom $random,
- ITimeFactory $timeFactory,
- IURLGenerator $urlGenerator) {
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ 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;
}
/**
- * @NoAdminRequired
+ * Get a direct link to a file
+ *
+ * @param int $fileId ID of the file
+ * @param int $expirationTime Duration until the link expires
+ * @return DataResponse<Http::STATUS_OK, array{url: string}, array{}>
+ * @throws OCSNotFoundException File not found
+ * @throws OCSBadRequestException Getting direct link is not possible
+ * @throws OCSForbiddenException Missing permissions to get direct link
+ *
+ * 200: Direct link returned
*/
+ #[NoAdminRequired]
public function getUrl(int $fileId, int $expirationTime = 60 * 60 * 8): DataResponse {
$userFolder = $this->rootFolder->getUserFolder($this->userId);
- $files = $userFolder->getById($fileId);
+ $file = $userFolder->getFirstNodeById($fileId);
- if ($files === []) {
+ if (!$file) {
throw new OCSNotFoundException();
}
@@ -94,11 +68,17 @@ class DirectController extends OCSController {
throw new OCSBadRequestException('Expiration time should be greater than 0 and less than or equal to ' . (60 * 60 * 24));
}
- $file = array_shift($files);
if (!($file instanceof File)) {
throw new OCSBadRequestException('Direct download only works for files');
}
+ $event = new BeforeDirectFileDownloadEvent($userFolder->getRelativePath($file->getPath()));
+ $this->eventDispatcher->dispatchTyped($event);
+
+ if ($event->isSuccessful() === false) {
+ throw new OCSForbiddenException('Permission denied to download file');
+ }
+
//TODO: at some point we should use the directdownlaod function of storages
$direct = new Direct();
$direct->setUserId($this->userId);
@@ -110,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 de22e3ba6a9..19eb4097b45 100644
--- a/apps/dav/lib/Controller/InvitationResponseController.php
+++ b/apps/dav/lib/Controller/InvitationResponseController.php
@@ -3,32 +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>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have 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;
@@ -36,17 +20,9 @@ use OCP\IRequest;
use Sabre\VObject\ITip\Message;
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.
*
@@ -56,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) {
@@ -93,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) {
@@ -118,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
@@ -131,24 +105,21 @@ class InvitationResponseController extends Controller {
}
/**
- * @PublicPage
- * @NoCSRFRequired
- *
* @param string $token
*
* @return TemplateResponse
*/
+ #[PublicPage]
+ #[NoCSRFRequired]
public function processMoreOptionsResult(string $token):TemplateResponse {
$partstat = $this->request->getParam('partStat');
- $guests = (int) $this->request->getParam('guests');
- $comment = $this->request->getParam('comment');
$row = $this->getTokenInformation($token);
if (!$row || !\in_array($partstat, ['ACCEPTED', 'DECLINED', 'TENTATIVE'])) {
return new TemplateResponse($this->appName, 'schedule-response-error', [], 'guest');
}
- $iTipMessage = $this->buildITipResponse($row, $partstat, $guests, $comment);
+ $iTipMessage = $this->buildITipResponse($row, $partstat);
$this->responseServer->handleITipMessage($iTipMessage);
if ($iTipMessage->getScheduleStatus() === '1.2') {
return new TemplateResponse($this->appName, 'schedule-response-success', [], 'guest');
@@ -168,15 +139,16 @@ class InvitationResponseController extends Controller {
$query->select('*')
->from('calendar_invitations')
->where($query->expr()->eq('token', $query->createNamedParameter($token)));
- $stmt = $query->execute();
+ $stmt = $query->executeQuery();
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
+ $stmt->closeCursor();
if (!$row) {
return null;
}
$currentTime = $this->timeFactory->getTime();
- if (((int) $row['expiration']) < $currentTime) {
+ if (((int)$row['expiration']) < $currentTime) {
return null;
}
@@ -190,8 +162,7 @@ class InvitationResponseController extends Controller {
* @param string|null $comment
* @return Message
*/
- private function buildITipResponse(array $row, string $partStat, int $guests = null,
- string $comment = null):Message {
+ private function buildITipResponse(array $row, string $partStat):Message {
$iTipMessage = new Message();
$iTipMessage->uid = $row['uid'];
$iTipMessage->component = 'VEVENT';
@@ -225,19 +196,7 @@ EOF;
$row['uid'], $row['sequence'] ?? 0, $row['recurrenceid'] ?? ''
]));
$vEvent = $vObject->{'VEVENT'};
- /** @var \Sabre\VObject\Property\ICalendar\CalAddress $attendee */
- $attendee = $vEvent->{'ATTENDEE'};
-
$vEvent->DTSTAMP = date('Ymd\\THis\\Z', $this->timeFactory->getTime());
-
- if ($comment) {
- $attendee->add('X-RESPONSE-COMMENT', $comment);
- $vEvent->add('COMMENT', $comment);
- }
- if ($guests) {
- $attendee->add('X-NUM-GUESTS', $guests);
- }
-
$iTipMessage->message = $vObject;
return $iTipMessage;
diff --git a/apps/dav/lib/Controller/OutOfOfficeController.php b/apps/dav/lib/Controller/OutOfOfficeController.php
new file mode 100644
index 00000000000..d3516d092e8
--- /dev/null
+++ b/apps/dav/lib/Controller/OutOfOfficeController.php
@@ -0,0 +1,189 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Controller;
+
+use DateTimeImmutable;
+use OCA\DAV\ResponseDefinitions;
+use OCA\DAV\Service\AbsenceService;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\OCSController;
+use OCP\IRequest;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use OCP\User\IAvailabilityCoordinator;
+use function mb_strlen;
+
+/**
+ * @psalm-import-type DAVOutOfOfficeData from ResponseDefinitions
+ * @psalm-import-type DAVCurrentOutOfOfficeData from ResponseDefinitions
+ */
+class OutOfOfficeController extends OCSController {
+
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ private IUserManager $userManager,
+ private ?IUserSession $userSession,
+ private AbsenceService $absenceService,
+ private IAvailabilityCoordinator $coordinator,
+ ) {
+ parent::__construct($appName, $request);
+ }
+
+ /**
+ * Get the currently configured out-of-office data of a user
+ *
+ * @param string $userId The user id to get out-of-office data for.
+ * @return DataResponse<Http::STATUS_OK, DAVCurrentOutOfOfficeData, array{}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}>
+ *
+ * 200: Out-of-office data
+ * 404: No out-of-office data was found
+ */
+ #[NoAdminRequired]
+ public function getCurrentOutOfOfficeData(string $userId): DataResponse {
+ $user = $this->userManager->get($userId);
+ if ($user === null) {
+ return new DataResponse(null, Http::STATUS_NOT_FOUND);
+ }
+ try {
+ $data = $this->absenceService->getCurrentAbsence($user);
+ if ($data === null) {
+ return new DataResponse(null, Http::STATUS_NOT_FOUND);
+ }
+ } catch (DoesNotExistException) {
+ return new DataResponse(null, Http::STATUS_NOT_FOUND);
+ }
+
+ return new DataResponse($data->jsonSerialize());
+ }
+
+ /**
+ * Get the configured out-of-office data of a user.
+ *
+ * @param string $userId The user id to get out-of-office data for.
+ * @return DataResponse<Http::STATUS_OK, DAVOutOfOfficeData, array{}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}>
+ *
+ * 200: Out-of-office data
+ * 404: No out-of-office data was found
+ */
+ #[NoAdminRequired]
+ public function getOutOfOffice(string $userId): DataResponse {
+ try {
+ $data = $this->absenceService->getAbsence($userId);
+ if ($data === null) {
+ return new DataResponse(null, Http::STATUS_NOT_FOUND);
+ }
+ } catch (DoesNotExistException) {
+ return new DataResponse(null, Http::STATUS_NOT_FOUND);
+ }
+
+ return new DataResponse([
+ 'id' => $data->getId(),
+ 'userId' => $data->getUserId(),
+ 'firstDay' => $data->getFirstDay(),
+ 'lastDay' => $data->getLastDay(),
+ 'status' => $data->getStatus(),
+ 'message' => $data->getMessage(),
+ 'replacementUserId' => $data->getReplacementUserId(),
+ 'replacementUserDisplayName' => $data->getReplacementUserDisplayName(),
+ ]);
+ }
+
+ /**
+ * Set out-of-office absence
+ *
+ * @param string $firstDay First day of the absence in format `YYYY-MM-DD`
+ * @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
+ * @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 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(
+ string $firstDay,
+ 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);
+ if ($parsedFirstDay->getTimestamp() > $parsedLastDay->getTimestamp()) {
+ return new DataResponse(['error' => 'firstDay'], Http::STATUS_BAD_REQUEST);
+ }
+
+ $data = $this->absenceService->createOrUpdateAbsence(
+ $user,
+ $firstDay,
+ $lastDay,
+ $status,
+ $message,
+ $replacementUserId,
+ $replacementUser?->getDisplayName()
+ );
+ $this->coordinator->clearCache($user->getUID());
+
+ return new DataResponse([
+ 'id' => $data->getId(),
+ 'userId' => $data->getUserId(),
+ 'firstDay' => $data->getFirstDay(),
+ 'lastDay' => $data->getLastDay(),
+ 'status' => $data->getStatus(),
+ 'message' => $data->getMessage(),
+ 'replacementUserId' => $data->getReplacementUserId(),
+ 'replacementUserDisplayName' => $data->getReplacementUserDisplayName(),
+ ]);
+ }
+
+ /**
+ * Clear the out-of-office
+ *
+ * @return DataResponse<Http::STATUS_OK|Http::STATUS_UNAUTHORIZED, null, array{}>
+ *
+ * 200: When the absence was cleared successfully
+ * 401: When the user is not logged in
+ */
+ #[NoAdminRequired]
+ public function clearOutOfOffice(): DataResponse {
+ $user = $this->userSession?->getUser();
+ if ($user === null) {
+ return new DataResponse(null, Http::STATUS_UNAUTHORIZED);
+ }
+
+ $this->absenceService->clearAbsence($user);
+ $this->coordinator->clearCache($user->getUID());
+ return new DataResponse(null);
+ }
+}
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 acee65cd00d..f9a4f8ee986 100644
--- a/apps/dav/lib/DAV/CustomPropertiesBackend.php
+++ b/apps/dav/lib/DAV/CustomPropertiesBackend.php
@@ -1,36 +1,33 @@
<?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>
- *
- * @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 OCA\DAV\Connector\Sabre\Node;
+use Exception;
+use OCA\DAV\CalDAV\Calendar;
+use OCA\DAV\CalDAV\CalendarObject;
+use OCA\DAV\CalDAV\DefaultCalendarValidator;
+use OCA\DAV\Connector\Sabre\Directory;
+use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IUser;
+use Sabre\DAV\Exception as DavException;
use Sabre\DAV\PropertyStorage\Backend\BackendInterface;
use Sabre\DAV\PropFind;
use Sabre\DAV\PropPatch;
+use Sabre\DAV\Server;
use Sabre\DAV\Tree;
+use Sabre\DAV\Xml\Property\Complex;
+use Sabre\DAV\Xml\Property\Href;
+use Sabre\DAV\Xml\Property\LocalHref;
+use Sabre\Xml\ParseException;
+use Sabre\Xml\Service as XmlService;
+
use function array_intersect;
class CustomPropertiesBackend implements BackendInterface {
@@ -39,6 +36,26 @@ class CustomPropertiesBackend implements BackendInterface {
private const TABLE_NAME = 'properties';
/**
+ * Value is stored as string.
+ */
+ public const PROPERTY_TYPE_STRING = 1;
+
+ /**
+ * Value is stored as XML fragment.
+ */
+ public const PROPERTY_TYPE_XML = 2;
+
+ /**
+ * Value is stored as a property object.
+ */
+ public const PROPERTY_TYPE_OBJECT = 3;
+
+ /**
+ * Value is stored as a {DAV:}href string.
+ */
+ public const PROPERTY_TYPE_HREF = 4;
+
+ /**
* Ignored properties
*
* @var string[]
@@ -49,58 +66,35 @@ 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',
];
/**
- * Properties set by one user, readable by all others
+ * Allowed properties for the oc/nc namespace, all other properties in the namespace are ignored
*
- * @var array[]
+ * @var string[]
*/
- private const PUBLISHED_READ_ONLY_PROPERTIES = [
- '{urn:ietf:params:xml:ns:caldav}calendar-availability',
+ private const ALLOWED_NC_PROPERTIES = [
+ '{http://owncloud.org/ns}calendar-enabled',
+ '{http://owncloud.org/ns}enabled',
];
/**
- * @var Tree
- */
- private $tree;
-
- /**
- * @var IDBConnection
+ * Properties set by one user, readable by all others
+ *
+ * @var string[]
*/
- private $connection;
+ private const PUBLISHED_READ_ONLY_PROPERTIES = [
+ '{urn:ietf:params:xml:ns:caldav}calendar-availability',
+ '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL',
+ ];
/**
- * @var IUser
+ * Map of custom XML elements to parse when trying to deserialize an instance of
+ * \Sabre\DAV\Xml\Property\Complex to find a more specialized PROPERTY_TYPE_*
*/
- private $user;
+ private const COMPLEX_XML_ELEMENT_MAP = [
+ '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => Href::class,
+ ];
/**
* Properties cache
@@ -108,6 +102,7 @@ class CustomPropertiesBackend implements BackendInterface {
* @var array
*/
private $userCache = [];
+ private XmlService $xmlService;
/**
* @param Tree $tree node tree
@@ -115,12 +110,17 @@ class CustomPropertiesBackend implements BackendInterface {
* @param IUser $user owner of the tree and properties
*/
public function __construct(
- Tree $tree,
- IDBConnection $connection,
- IUser $user) {
- $this->tree = $tree;
- $this->connection = $connection;
- $this->user = $user;
+ private Server $server,
+ private Tree $tree,
+ private IDBConnection $connection,
+ private IUser $user,
+ private DefaultCalendarValidator $defaultCalendarValidator,
+ ) {
+ $this->xmlService = new XmlService();
+ $this->xmlService->elementMap = array_merge(
+ $this->xmlService->elementMap,
+ self::COMPLEX_XML_ELEMENT_MAP,
+ );
}
/**
@@ -133,15 +133,14 @@ class CustomPropertiesBackend implements BackendInterface {
public function propFind($path, PropFind $propFind) {
$requestedProps = $propFind->get404Properties();
- // these might appear
- $requestedProps = array_diff(
+ $requestedProps = array_filter(
$requestedProps,
- self::IGNORED_PROPERTIES
+ $this->isPropertyAllowed(...),
);
// substr of calendars/ => path is inside the CalDAV component
// two '/' => this a calendar (no calendar-home nor calendar object)
- if (substr($path, 0, 10) === 'calendars/' && substr_count($path, '/') === 2) {
+ if (str_starts_with($path, 'calendars/') && substr_count($path, '/') === 2) {
$allRequestedProps = $propFind->getRequestedProperties();
$customPropertiesForShares = [
'{DAV:}displayname',
@@ -159,20 +158,80 @@ class CustomPropertiesBackend implements BackendInterface {
}
}
+ // substr of addressbooks/ => path is inside the CardDAV component
+ // three '/' => this a addressbook (no addressbook-home nor contact object)
+ if (str_starts_with($path, 'addressbooks/') && substr_count($path, '/') === 3) {
+ $allRequestedProps = $propFind->getRequestedProperties();
+ $customPropertiesForShares = [
+ '{DAV:}displayname',
+ ];
+
+ foreach ($customPropertiesForShares as $customPropertyForShares) {
+ if (in_array($customPropertyForShares, $allRequestedProps, true)) {
+ $requestedProps[] = $customPropertyForShares;
+ }
+ }
+ }
+
+ // substr of principals/users/ => path is a user principal
+ // two '/' => this a principal collection (and not some child object)
+ if (str_starts_with($path, 'principals/users/') && substr_count($path, '/') === 2) {
+ $allRequestedProps = $propFind->getRequestedProperties();
+ $customProperties = [
+ '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL',
+ ];
+
+ foreach ($customProperties as $customProperty) {
+ if (in_array($customProperty, $allRequestedProps, true)) {
+ $requestedProps[] = $customProperty;
+ }
+ }
+ }
+
if (empty($requestedProps)) {
return;
}
+ $node = $this->tree->getNodeForPath($path);
+ if ($node instanceof Directory && $propFind->getDepth() !== 0) {
+ $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) {
+ try {
+ $this->validateProperty($path, $propName, $propValue);
+ } catch (DavException $e) {
+ continue;
+ }
$propFind->set($propName, $propValue);
}
foreach ($this->getUserProperties($path, $requestedProps) as $propName => $propValue) {
+ try {
+ $this->validateProperty($path, $propName, $propValue);
+ } catch (DavException $e) {
+ continue;
+ }
$propFind->set($propName, $propValue);
}
}
+ 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
*
@@ -212,14 +271,39 @@ 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();
}
/**
+ * Validate the value of a property. Will throw if a value is invalid.
+ *
+ * @throws DavException The value of the property is invalid
+ */
+ private function validateProperty(string $path, string $propName, mixed $propValue): void {
+ switch ($propName) {
+ case '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL':
+ /** @var Href $propValue */
+ $href = $propValue->getHref();
+ if ($href === null) {
+ throw new DavException('Href is empty');
+ }
+
+ // $path is the principal here as this prop is only set on principals
+ $node = $this->tree->getNodeForPath($href);
+ if (!($node instanceof Calendar) || $node->getOwner() !== $path) {
+ throw new DavException('No such calendar');
+ }
+
+ $this->defaultCalendarValidator->validateScheduleDefaultCalendar($node);
+ break;
+ }
+ }
+
+ /**
* @param string $path
* @param string[] $requestedProperties
*
@@ -239,13 +323,48 @@ class CustomPropertiesBackend implements BackendInterface {
$result = $qb->executeQuery();
$props = [];
while ($row = $result->fetch()) {
- $props[$row['propertyname']] = $row['propertyvalue'];
+ $props[$row['propertyname']] = $this->decodeValueFromDatabase($row['propertyvalue'], $row['valuetype']);
}
$result->closeCursor();
return $props;
}
/**
+ * prefetch all user properties in a directory
+ */
+ private function cacheDirectory(string $path, Directory $node): void {
+ $prefix = ltrim($path . '/', '/');
+ $query = $this->connection->getQueryBuilder();
+ $query->select('name', 'p.propertypath', 'p.propertyname', 'p.propertyvalue', 'p.valuetype')
+ ->from('filecache', 'f')
+ ->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 = [];
+
+ while ($row = $result->fetch()) {
+ $childPath = $prefix . $row['name'];
+ if (!isset($propsByPath[$childPath])) {
+ $propsByPath[$childPath] = [];
+ }
+ if (isset($row['propertyname'])) {
+ $propsByPath[$childPath][$row['propertyname']] = $this->decodeValueFromDatabase($row['propertyvalue'], $row['valuetype']);
+ }
+ }
+ $this->userCache = array_merge($this->userCache, $propsByPath);
+ }
+
+ /**
* Returns a list of properties for the given path and current user
*
* @param string $path
@@ -271,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(
@@ -282,7 +401,7 @@ class CustomPropertiesBackend implements BackendInterface {
$props = [];
while ($row = $result->fetch()) {
- $props[$row['propertyname']] = $row['propertyvalue'];
+ $props[$row['propertyname']] = $this->decodeValueFromDatabase($row['propertyvalue'], $row['valuetype']);
}
$result->closeCursor();
@@ -292,68 +411,58 @@ class CustomPropertiesBackend implements BackendInterface {
}
/**
- * Update properties
- *
- * @param string $path path for which to update properties
- * @param array $properties array of properties to update
- *
- * @return bool
+ * @throws Exception
*/
- private function updateProperties(string $path, array $properties) {
- $deleteStatement = 'DELETE FROM `*PREFIX*properties`' .
- ' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?';
-
- $insertStatement = 'INSERT INTO `*PREFIX*properties`' .
- ' (`userid`,`propertypath`,`propertyname`,`propertyvalue`) VALUES(?,?,?,?)';
-
- $updateStatement = 'UPDATE `*PREFIX*properties` SET `propertyvalue` = ?' .
- ' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?';
-
+ private function updateProperties(string $path, array $properties): bool {
// TODO: use "insert or update" strategy ?
$existing = $this->getUserProperties($path, []);
- $this->connection->beginTransaction();
- foreach ($properties as $propertyName => $propertyValue) {
- // If it was null, we need to delete the property
- if (is_null($propertyValue)) {
- if (array_key_exists($propertyName, $existing)) {
- $this->connection->executeUpdate($deleteStatement,
- [
- $this->user->getUID(),
- $this->formatPath($path),
- $propertyName,
- ]
- );
- }
- } else {
- if ($propertyValue instanceOf \Sabre\DAV\Xml\Property\Complex) {
- $propertyValue = $propertyValue->getXml();
- } elseif (!is_string($propertyValue)) {
- $propertyValue = (string)$propertyValue;
- }
- if (!array_key_exists($propertyName, $existing)) {
- $this->connection->executeUpdate($insertStatement,
- [
- $this->user->getUID(),
- $this->formatPath($path),
- $propertyName,
- $propertyValue,
- ]
- );
+ try {
+ $this->connection->beginTransaction();
+ foreach ($properties as $propertyName => $propertyValue) {
+ // common parameters for all queries
+ $dbParameters = [
+ 'userid' => $this->user->getUID(),
+ 'propertyPath' => $this->formatPath($path),
+ 'propertyName' => $propertyName,
+ ];
+
+ // If it was null, we need to delete the property
+ if (is_null($propertyValue)) {
+ if (array_key_exists($propertyName, $existing)) {
+ $deleteQuery = $deleteQuery ?? $this->createDeleteQuery();
+ $deleteQuery
+ ->setParameters($dbParameters)
+ ->executeStatement();
+ }
} else {
- $this->connection->executeUpdate($updateStatement,
- [
- $propertyValue,
- $this->user->getUID(),
- $this->formatPath($path),
- $propertyName,
- ]
+ [$value, $valueType] = $this->encodeValueForDatabase(
+ $path,
+ $propertyName,
+ $propertyValue,
);
+ $dbParameters['propertyValue'] = $value;
+ $dbParameters['valueType'] = $valueType;
+
+ if (!array_key_exists($propertyName, $existing)) {
+ $insertQuery = $insertQuery ?? $this->createInsertQuery();
+ $insertQuery
+ ->setParameters($dbParameters)
+ ->executeStatement();
+ } else {
+ $updateQuery = $updateQuery ?? $this->createUpdateQuery();
+ $updateQuery
+ ->setParameters($dbParameters)
+ ->executeStatement();
+ }
}
}
- }
- $this->connection->commit();
- unset($this->userCache[$path]);
+ $this->connection->commit();
+ unset($this->userCache[$path]);
+ } catch (Exception $e) {
+ $this->connection->rollBack();
+ throw $e;
+ }
return true;
}
@@ -367,8 +476,122 @@ class CustomPropertiesBackend implements BackendInterface {
private function formatPath(string $path): string {
if (strlen($path) > 250) {
return sha1($path);
+ }
+
+ return $path;
+ }
+
+ /**
+ * @throws ParseException If parsing a \Sabre\DAV\Xml\Property\Complex value fails
+ * @throws DavException If the property value is invalid
+ */
+ private function encodeValueForDatabase(string $path, string $name, mixed $value): array {
+ // Try to parse a more specialized property type first
+ if ($value instanceof Complex) {
+ $xml = $this->xmlService->write($name, [$value], $this->server->getBaseUri());
+ $value = $this->xmlService->parse($xml, $this->server->getBaseUri()) ?? $value;
+ }
+
+ if ($name === '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL') {
+ $value = $this->encodeDefaultCalendarUrl($value);
+ }
+
+ try {
+ $this->validateProperty($path, $name, $value);
+ } catch (DavException $e) {
+ throw new DavException(
+ "Property \"$name\" has an invalid value: " . $e->getMessage(),
+ 0,
+ $e,
+ );
+ }
+
+ if (is_scalar($value)) {
+ $valueType = self::PROPERTY_TYPE_STRING;
+ } elseif ($value instanceof Complex) {
+ $valueType = self::PROPERTY_TYPE_XML;
+ $value = $value->getXml();
+ } elseif ($value instanceof Href) {
+ $valueType = self::PROPERTY_TYPE_HREF;
+ $value = $value->getHref();
} else {
- return $path;
+ $valueType = self::PROPERTY_TYPE_OBJECT;
+ // 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];
+ }
+
+ /**
+ * @return mixed|Complex|string
+ */
+ private function decodeValueFromDatabase(string $value, int $valueType) {
+ switch ($valueType) {
+ case self::PROPERTY_TYPE_XML:
+ return new Complex($value);
+ case self::PROPERTY_TYPE_HREF:
+ return new Href($value);
+ case self::PROPERTY_TYPE_OBJECT:
+ // 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;
+ }
+ }
+
+ private function encodeDefaultCalendarUrl(Href $value): Href {
+ $href = $value->getHref();
+ if ($href === null) {
+ return $value;
+ }
+
+ if (!str_starts_with($href, '/')) {
+ return $value;
+ }
+
+ try {
+ // Build path relative to the dav base URI to be used later to find the node
+ $value = new LocalHref($this->server->calculateUri($href) . '/');
+ } catch (DavException\Forbidden) {
+ // Not existing calendars will be handled later when the value is validated
}
+
+ return $value;
+ }
+
+ private function createDeleteQuery(): IQueryBuilder {
+ $deleteQuery = $this->connection->getQueryBuilder();
+ $deleteQuery->delete('properties')
+ ->where($deleteQuery->expr()->eq('userid', $deleteQuery->createParameter('userid')))
+ ->andWhere($deleteQuery->expr()->eq('propertypath', $deleteQuery->createParameter('propertyPath')))
+ ->andWhere($deleteQuery->expr()->eq('propertyname', $deleteQuery->createParameter('propertyName')));
+ return $deleteQuery;
+ }
+
+ private function createInsertQuery(): IQueryBuilder {
+ $insertQuery = $this->connection->getQueryBuilder();
+ $insertQuery->insert('properties')
+ ->values([
+ 'userid' => $insertQuery->createParameter('userid'),
+ 'propertypath' => $insertQuery->createParameter('propertyPath'),
+ 'propertyname' => $insertQuery->createParameter('propertyName'),
+ 'propertyvalue' => $insertQuery->createParameter('propertyValue'),
+ 'valuetype' => $insertQuery->createParameter('valueType'),
+ ]);
+ return $insertQuery;
+ }
+
+ private function createUpdateQuery(): IQueryBuilder {
+ $updateQuery = $this->connection->getQueryBuilder();
+ $updateQuery->update('properties')
+ ->set('propertyvalue', $updateQuery->createParameter('propertyValue'))
+ ->set('valuetype', $updateQuery->createParameter('valueType'))
+ ->where($updateQuery->expr()->eq('userid', $updateQuery->createParameter('userid')))
+ ->andWhere($updateQuery->expr()->eq('propertypath', $updateQuery->createParameter('propertyPath')))
+ ->andWhere($updateQuery->expr()->eq('propertyname', $updateQuery->createParameter('propertyName')));
+ return $updateQuery;
}
}
diff --git a/apps/dav/lib/DAV/GroupPrincipalBackend.php b/apps/dav/lib/DAV/GroupPrincipalBackend.php
index f1f15fd61a6..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);
+ }
}
}
@@ -104,7 +70,7 @@ class GroupPrincipalBackend implements BackendInterface {
* @return array
*/
public function getPrincipalByPath($path) {
- $elements = explode('/', $path, 3);
+ $elements = explode('/', $path, 3);
if ($elements[0] !== 'principals') {
return null;
}
@@ -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) {
@@ -288,7 +258,7 @@ class GroupPrincipalBackend implements BackendInterface {
$restrictGroups = $this->groupManager->getUserGroupIds($user);
}
- if (strpos($uri, 'principal:principals/groups/') === 0) {
+ if (str_starts_with($uri, 'principal:principals/groups/')) {
$name = urlencode(substr($uri, 28));
if ($restrictGroups !== false && !\in_array($name, $restrictGroups, true)) {
return null;
diff --git a/apps/dav/lib/DAV/PublicAuth.php b/apps/dav/lib/DAV/PublicAuth.php
index 83874ab0d4d..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.'];
}
/**
@@ -86,7 +71,7 @@ class PublicAuth implements BackendInterface {
private function isRequestPublic(RequestInterface $request) {
$url = $request->getPath();
$matchingUrls = array_filter($this->publicURLs, function ($publicUrl) use ($url) {
- return strpos($url, $publicUrl, 0) === 0;
+ return str_starts_with($url, $publicUrl);
});
return !empty($matchingUrls);
}
diff --git a/apps/dav/lib/DAV/Sharing/Backend.php b/apps/dav/lib/DAV/Sharing/Backend.php
index 0f675ea4c15..d60f5cca7c6 100644
--- a/apps/dav/lib/DAV/Sharing/Backend.php
+++ b/apps/dav/lib/DAV/Sharing/Backend.php
@@ -1,178 +1,106 @@
<?php
+
+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>
- *
- * @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;
use OCA\DAV\Connector\Sabre\Principal;
-use OCP\IDBConnection;
+use OCP\AppFramework\Db\TTransactional;
+use OCP\ICache;
+use OCP\ICacheFactory;
use OCP\IGroupManager;
use OCP\IUserManager;
+use Psr\Log\LoggerInterface;
-class Backend {
-
- /** @var IDBConnection */
- private $db;
- /** @var IUserManager */
- private $userManager;
- /** @var IGroupManager */
- private $groupManager;
- /** @var Principal */
- private $principalBackend;
- /** @var string */
- private $resourceType;
-
+abstract class Backend {
+ use TTransactional;
public const ACCESS_OWNER = 1;
+
public const ACCESS_READ_WRITE = 2;
public const ACCESS_READ = 3;
-
- /**
- * @param IDBConnection $db
- * @param IUserManager $userManager
- * @param IGroupManager $groupManager
- * @param Principal $principalBackend
- * @param string $resourceType
- */
- public function __construct(IDBConnection $db, IUserManager $userManager, IGroupManager $groupManager, Principal $principalBackend, $resourceType) {
- $this->db = $db;
- $this->userManager = $userManager;
- $this->groupManager = $groupManager;
- $this->principalBackend = $principalBackend;
- $this->resourceType = $resourceType;
+ // 4 is already in use for public calendars
+ public const ACCESS_UNSHARED = 5;
+
+ private ICache $shareCache;
+
+ public function __construct(
+ private IUserManager $userManager,
+ private IGroupManager $groupManager,
+ private Principal $principalBackend,
+ private ICacheFactory $cacheFactory,
+ private SharingService $service,
+ private LoggerInterface $logger,
+ ) {
+ $this->shareCache = $this->cacheFactory->createInMemory();
}
/**
- * @param IShareable $shareable
- * @param string[] $add
- * @param string[] $remove
+ * @param list<array{href: string, commonName: string, readOnly: bool}> $add
+ * @param list<string> $remove
*/
- public function updateShares(IShareable $shareable, array $add, array $remove) {
+ public function updateShares(IShareable $shareable, array $add, array $remove, array $oldShares = []): void {
+ $this->shareCache->clear();
foreach ($add as $element) {
$principal = $this->principalBackend->findByUri($element['href'], '');
- if ($principal !== '') {
- $this->shareWith($shareable, $element);
+ if (empty($principal)) {
+ continue;
}
- }
- foreach ($remove as $element) {
- $principal = $this->principalBackend->findByUri($element, '');
- if ($principal !== '') {
- $this->unshare($shareable, $element);
+
+ // We need to validate manually because some principals are only virtual
+ // i.e. Group principals
+ $principalparts = explode('/', $principal, 3);
+ if (count($principalparts) !== 3 || $principalparts[0] !== 'principals' || !in_array($principalparts[1], ['users', 'groups', 'circles'], true)) {
+ // Invalid principal
+ continue;
}
- }
- }
- /**
- * @param IShareable $shareable
- * @param string $element
- */
- private function shareWith($shareable, $element) {
- $user = $element['href'];
- $parts = explode(':', $user, 2);
- if ($parts[0] !== 'principal') {
- return;
- }
+ // Don't add share for owner
+ if ($shareable->getOwner() !== null && strcasecmp($shareable->getOwner(), $principal) === 0) {
+ continue;
+ }
- // don't share with owner
- if ($shareable->getOwner() === $parts[1]) {
- return;
- }
+ $principalparts[2] = urldecode($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;
+ }
- $principal = explode('/', $parts[1], 3);
- if (count($principal) !== 3 || $principal[0] !== 'principals' || !in_array($principal[1], ['users', 'groups', 'circles'], true)) {
- // Invalid principal
- return;
- }
+ $access = Backend::ACCESS_READ;
+ if (isset($element['readOnly'])) {
+ $access = $element['readOnly'] ? Backend::ACCESS_READ : Backend::ACCESS_READ_WRITE;
+ }
- $principal[2] = urldecode($principal[2]);
- if (($principal[1] === 'users' && !$this->userManager->userExists($principal[2])) ||
- ($principal[1] === 'groups' && !$this->groupManager->groupExists($principal[2]))) {
- // User or group does not exist
- return;
+ $this->service->shareWith($shareable->getResourceId(), $principal, $access);
}
+ foreach ($remove as $element) {
+ $principal = $this->principalBackend->findByUri($element, '');
+ if (empty($principal)) {
+ continue;
+ }
- // remove the share if it already exists
- $this->unshare($shareable, $element['href']);
- $access = self::ACCESS_READ;
- if (isset($element['readOnly'])) {
- $access = $element['readOnly'] ? self::ACCESS_READ : self::ACCESS_READ_WRITE;
- }
+ // Don't add unshare for owner
+ if ($shareable->getOwner() !== null && strcasecmp($shareable->getOwner(), $principal) === 0) {
+ continue;
+ }
- $query = $this->db->getQueryBuilder();
- $query->insert('dav_shares')
- ->values([
- 'principaluri' => $query->createNamedParameter($parts[1]),
- 'type' => $query->createNamedParameter($this->resourceType),
- 'access' => $query->createNamedParameter($access),
- 'resourceid' => $query->createNamedParameter($shareable->getResourceId())
- ]);
- $query->execute();
+ // Delete any possible direct shares (since the frontend does not separate between them)
+ $this->service->deleteShare($shareable->getResourceId(), $principal);
+ }
}
- /**
- * @param $resourceId
- */
- public function deleteAllShares($resourceId) {
- $query = $this->db->getQueryBuilder();
- $query->delete('dav_shares')
- ->where($query->expr()->eq('resourceid', $query->createNamedParameter($resourceId)))
- ->andWhere($query->expr()->eq('type', $query->createNamedParameter($this->resourceType)))
- ->execute();
+ public function deleteAllShares(int $resourceId): void {
+ $this->shareCache->clear();
+ $this->service->deleteAllShares($resourceId);
}
- public function deleteAllSharesByUser($principaluri) {
- $query = $this->db->getQueryBuilder();
- $query->delete('dav_shares')
- ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principaluri)))
- ->andWhere($query->expr()->eq('type', $query->createNamedParameter($this->resourceType)))
- ->execute();
- }
-
- /**
- * @param IShareable $shareable
- * @param string $element
- */
- private function unshare($shareable, $element) {
- $parts = explode(':', $element, 2);
- if ($parts[0] !== 'principal') {
- return;
- }
-
- // don't share with owner
- if ($shareable->getOwner() === $parts[1]) {
- return;
- }
-
- $query = $this->db->getQueryBuilder();
- $query->delete('dav_shares')
- ->where($query->expr()->eq('resourceid', $query->createNamedParameter($shareable->getResourceId())))
- ->andWhere($query->expr()->eq('type', $query->createNamedParameter($this->resourceType)))
- ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($parts[1])))
- ;
- $query->execute();
+ public function deleteAllSharesByUser(string $principaluri): void {
+ $this->shareCache->clear();
+ $this->service->deleteAllSharesByUser($principaluri);
}
/**
@@ -183,45 +111,67 @@ class Backend {
* * commonName - Optional, for example a first + last name
* * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
* * readOnly - boolean
- * * summary - Optional, a description for the share
*
* @param int $resourceId
- * @return array
+ * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}>
*/
- public function getShares($resourceId) {
- $query = $this->db->getQueryBuilder();
- $result = $query->select(['principaluri', 'access'])
- ->from('dav_shares')
- ->where($query->expr()->eq('resourceid', $query->createNamedParameter($resourceId)))
- ->andWhere($query->expr()->eq('type', $query->createNamedParameter($this->resourceType)))
- ->groupBy(['principaluri', 'access'])
- ->execute();
+ public function getShares(int $resourceId): array {
+ $cached = $this->shareCache->get((string)$resourceId);
+ if ($cached) {
+ return $cached;
+ }
+ $rows = $this->service->getShares($resourceId);
$shares = [];
- while ($row = $result->fetch()) {
+ foreach ($rows as $row) {
$p = $this->principalBackend->getPrincipalByPath($row['principaluri']);
$shares[] = [
- 'href' => "principal:${row['principaluri']}",
- 'commonName' => isset($p['{DAV:}displayname']) ? $p['{DAV:}displayname'] : '',
+ 'href' => "principal:{$row['principaluri']}",
+ 'commonName' => isset($p['{DAV:}displayname']) ? (string)$p['{DAV:}displayname'] : '',
'status' => 1,
- 'readOnly' => (int) $row['access'] === self::ACCESS_READ,
- '{http://owncloud.org/ns}principal' => $row['principaluri'],
- '{http://owncloud.org/ns}group-share' => is_null($p)
+ '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') || str_starts_with($p['uri'], 'principals/circles'))
];
}
-
+ $this->shareCache->set((string)$resourceId, $shares);
return $shares;
}
+ public function preloadShares(array $resourceIds): void {
+ $resourceIds = array_filter($resourceIds, function (int $resourceId) {
+ return empty($this->shareCache->get((string)$resourceId));
+ });
+ if (empty($resourceIds)) {
+ return;
+ }
+
+ $rows = $this->service->getSharesForIds($resourceIds);
+ $sharesByResource = array_fill_keys($resourceIds, []);
+ 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,
+ '{http://owncloud.org/ns}principal' => (string)$row['principaluri'],
+ '{http://owncloud.org/ns}group-share' => isset($p['uri']) && str_starts_with($p['uri'], 'principals/groups')
+ ];
+ $this->shareCache->set((string)$resourceId, $sharesByResource[$resourceId]);
+ }
+ }
+
/**
* For shared resources the sharee is set in the ACL of the resource
*
* @param int $resourceId
- * @param array $acl
- * @return array
+ * @param list<array{privilege: string, principal: string, protected: bool}> $acl
+ * @param list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}> $shares
+ * @return list<array{principal: string, privilege: string, protected: bool}>
*/
- public function applyShareAcl($resourceId, $acl) {
- $shares = $this->getShares($resourceId);
+ public function applyShareAcl(array $shares, array $acl): array {
foreach ($shares as $share) {
$acl[] = [
'privilege' => '{DAV:}read',
@@ -234,7 +184,7 @@ class Backend {
'principal' => $share['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}principal'],
'protected' => true,
];
- } elseif ($this->resourceType === 'calendar') {
+ } elseif (in_array($this->service->getResourceType(), ['calendar','addressbook'])) {
// Allow changing the properties of read only calendars,
// so users can change the visibility.
$acl[] = [
@@ -246,4 +196,45 @@ 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 3833e026696..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;
@@ -40,16 +24,14 @@ interface IShareable extends INode {
* Every element in the add array has the following properties:
* * href - A url. Usually a mailto: address
* * commonName - Usually a first and last name, or false
- * * summary - A description of the share, can also be false
* * readOnly - A boolean value
*
* Every element in the remove array is just the address string.
*
- * @param array $add
- * @param array $remove
- * @return void
+ * @param list<array{href: string, commonName: string, readOnly: bool}> $add
+ * @param list<string> $remove
*/
- public function updateShares(array $add, array $remove);
+ public function updateShares(array $add, array $remove): void;
/**
* Returns the list of people whom this resource is shared with.
@@ -59,19 +41,15 @@ interface IShareable extends INode {
* * commonName - Optional, for example a first + last name
* * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
* * readOnly - boolean
- * * summary - Optional, a description for the share
*
- * @return array
+ * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}>
*/
- public function getShares();
+ public function getShares(): array;
- /**
- * @return int
- */
- public function getResourceId();
+ public function getResourceId(): int;
/**
- * @return string
+ * @return ?string
*/
public function getOwner();
}
diff --git a/apps/dav/lib/DAV/Sharing/Plugin.php b/apps/dav/lib/DAV/Sharing/Plugin.php
index a4b2cd3681c..03e63813bab 100644
--- a/apps/dav/lib/DAV/Sharing/Plugin.php
+++ b/apps/dav/lib/DAV/Sharing/Plugin.php
@@ -1,32 +1,18 @@
<?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;
+use OCA\DAV\CalDAV\CalDavBackend;
+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;
@@ -41,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,
+ ) {
}
/**
@@ -111,7 +89,7 @@ class Plugin extends ServerPlugin {
$this->server->xml->elementMap['{' . Plugin::NS_OWNCLOUD . '}invite'] = Invite::class;
$this->server->on('method:POST', [$this, 'httpPost']);
- $this->server->on('propFind', [$this, 'propFind']);
+ $this->server->on('propFind', [$this, 'propFind']);
}
/**
@@ -125,8 +103,8 @@ class Plugin extends ServerPlugin {
$path = $request->getPath();
// Only handling xml
- $contentType = $request->getHeader('Content-Type');
- if (strpos($contentType, 'application/xml') === false && strpos($contentType, 'text/xml') === false) {
+ $contentType = (string)$request->getHeader('Content-Type');
+ if (!str_contains($contentType, 'application/xml') && !str_contains($contentType, 'text/xml')) {
return;
}
@@ -180,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');
@@ -201,6 +179,20 @@ class Plugin extends ServerPlugin {
* @return void
*/
public function propFind(PropFind $propFind, INode $node) {
+ if ($node instanceof CalendarHome && $propFind->getDepth() === 1) {
+ $backend = $node->getCalDAVBackend();
+ if ($backend instanceof CalDavBackend) {
+ $calendars = $node->getChildren();
+ $calendars = array_filter($calendars, function (INode $node) {
+ return $node instanceof IShareable;
+ });
+ /** @var int[] $resourceIds */
+ $resourceIds = array_map(function (IShareable $node) {
+ return $node->getResourceId();
+ }, $calendars);
+ $backend->preloadShares($resourceIds);
+ }
+ }
if ($node instanceof IShareable) {
$propFind->handle('{' . Plugin::NS_OWNCLOUD . '}invite', function () use ($node) {
return new Invite(
diff --git a/apps/dav/lib/DAV/Sharing/SharingMapper.php b/apps/dav/lib/DAV/Sharing/SharingMapper.php
new file mode 100644
index 00000000000..e4722208189
--- /dev/null
+++ b/apps/dav/lib/DAV/Sharing/SharingMapper.php
@@ -0,0 +1,137 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * 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,
+ ) {
+ }
+
+ protected function getSharesForIdByAccess(int $resourceId, string $resourceType, bool $sharesWithAccess): array {
+ $query = $this->db->getQueryBuilder();
+ $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)))
+ ->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'])
+ ->from('dav_shares')
+ ->where($query->expr()->in('resourceid', $query->createNamedParameter($resourceIds, IQueryBuilder::PARAM_INT_ARRAY)))
+ ->andWhere($query->expr()->eq('type', $query->createNamedParameter($resourceType)))
+ ->andWhere($query->expr()->neq('access', $query->createNamedParameter(Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT)))
+ ->groupBy(['principaluri', 'access', 'resourceid'])
+ ->executeQuery();
+
+ $rows = $result->fetchAll();
+ $result->closeCursor();
+ return $rows;
+ }
+
+ public function unshare(int $resourceId, string $resourceType, string $principal): void {
+ $query = $this->db->getQueryBuilder();
+ $query->insert('dav_shares')
+ ->values([
+ 'principaluri' => $query->createNamedParameter($principal),
+ 'type' => $query->createNamedParameter($resourceType),
+ 'access' => $query->createNamedParameter(Backend::ACCESS_UNSHARED),
+ 'resourceid' => $query->createNamedParameter($resourceId)
+ ]);
+ $query->executeStatement();
+ }
+
+ public function share(int $resourceId, string $resourceType, int $access, string $principal): void {
+ $query = $this->db->getQueryBuilder();
+ $query->insert('dav_shares')
+ ->values([
+ 'principaluri' => $query->createNamedParameter($principal),
+ 'type' => $query->createNamedParameter($resourceType),
+ 'access' => $query->createNamedParameter($access),
+ 'resourceid' => $query->createNamedParameter($resourceId)
+ ]);
+ $query->executeStatement();
+ }
+
+ public function deleteShare(int $resourceId, string $resourceType, string $principal): void {
+ $query = $this->db->getQueryBuilder();
+ $query->delete('dav_shares');
+ $query->where(
+ $query->expr()->eq('resourceid', $query->createNamedParameter($resourceId, IQueryBuilder::PARAM_INT)),
+ $query->expr()->eq('type', $query->createNamedParameter($resourceType)),
+ $query->expr()->eq('principaluri', $query->createNamedParameter($principal))
+ );
+ $query->executeStatement();
+
+ }
+
+ public function deleteAllShares(int $resourceId, string $resourceType): void {
+ $query = $this->db->getQueryBuilder();
+ $query->delete('dav_shares')
+ ->where($query->expr()->eq('resourceid', $query->createNamedParameter($resourceId)))
+ ->andWhere($query->expr()->eq('type', $query->createNamedParameter($resourceType)))
+ ->executeStatement();
+ }
+
+ public function deleteAllSharesByUser(string $principaluri, string $resourceType): void {
+ $query = $this->db->getQueryBuilder();
+ $query->delete('dav_shares')
+ ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principaluri)))
+ ->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
new file mode 100644
index 00000000000..11459e12d74
--- /dev/null
+++ b/apps/dav/lib/DAV/Sharing/SharingService.php
@@ -0,0 +1,53 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * 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 getResourceType(): string {
+ return $this->resourceType;
+ }
+ public function shareWith(int $resourceId, string $principal, int $access): void {
+ // remove the share if it already exists
+ $this->mapper->deleteShare($resourceId, $this->getResourceType(), $principal);
+ $this->mapper->share($resourceId, $this->getResourceType(), $access, $principal);
+ }
+
+ public function unshare(int $resourceId, string $principal): void {
+ $this->mapper->unshare($resourceId, $this->getResourceType(), $principal);
+ }
+
+ public function deleteShare(int $resourceId, string $principal): void {
+ $this->mapper->deleteShare($resourceId, $this->getResourceType(), $principal);
+ }
+
+ public function deleteAllShares(int $resourceId): void {
+ $this->mapper->deleteAllShares($resourceId, $this->getResourceType());
+ }
+
+ public function deleteAllSharesByUser(string $principaluri): void {
+ $this->mapper->deleteAllSharesByUser($principaluri, $this->getResourceType());
+ }
+
+ public function getShares(int $resourceId): array {
+ return $this->mapper->getSharesForId($resourceId, $this->getResourceType());
+ }
+
+ public function getUnshares(int $resourceId): array {
+ return $this->mapper->getUnsharesForId($resourceId, $this->getResourceType());
+ }
+
+ 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 161a8dd0ebf..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,
+ ) {
}
/**
@@ -100,7 +75,7 @@ class Invite implements XmlSerializable {
}
/**
- * The xmlSerialize metod is called during xml writing.
+ * The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
diff --git a/apps/dav/lib/DAV/Sharing/Xml/ShareRequest.php b/apps/dav/lib/DAV/Sharing/Xml/ShareRequest.php
index eb5d7d4661d..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',
]);
@@ -62,8 +44,8 @@ class ShareRequest implements XmlDeserializable {
$set[] = [
'href' => $sharee['{DAV:}href'],
- 'commonName' => isset($sharee[$commonName]) ? $sharee[$commonName] : null,
- 'summary' => isset($sharee[$sumElem]) ? $sharee[$sumElem] : null,
+ 'commonName' => $sharee[$commonName] ?? null,
+ 'summary' => $sharee[$sumElem] ?? null,
'readOnly' => !array_key_exists('{' . Plugin::NS_OWNCLOUD . '}read-write', $sharee),
];
break;
diff --git a/apps/dav/lib/DAV/SystemPrincipalBackend.php b/apps/dav/lib/DAV/SystemPrincipalBackend.php
index e5b9a20037f..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;
@@ -60,7 +45,7 @@ class SystemPrincipalBackend extends AbstractBackend {
}
/**
- * Returns a specific principal, specified by it's path.
+ * Returns a specific principal, specified by its path.
* The returned structure should be the exact same as from
* getPrincipalsByPrefix.
*
@@ -87,7 +72,7 @@ class SystemPrincipalBackend extends AbstractBackend {
}
/**
- * Updates one ore more webdav properties on a principal.
+ * Updates one or more webdav properties on a principal.
*
* The list of mutations is stored in a Sabre\DAV\PropPatch object.
* To do the actual updates, you must tell this object which properties
diff --git a/apps/dav/lib/DAV/ViewOnlyPlugin.php b/apps/dav/lib/DAV/ViewOnlyPlugin.php
new file mode 100644
index 00000000000..9b9615b8063
--- /dev/null
+++ b/apps/dav/lib/DAV/ViewOnlyPlugin.php
@@ -0,0 +1,114 @@
+<?php
+
+/**
+ * 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;
+
+use OCA\DAV\Connector\Sabre\Exception\Forbidden;
+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;
+use Sabre\HTTP\RequestInterface;
+
+/**
+ * Sabre plugin for restricting file share receiver download:
+ */
+class ViewOnlyPlugin extends ServerPlugin {
+ private ?Server $server = null;
+
+ public function __construct(
+ private ?Folder $userFolder,
+ ) {
+ }
+
+ /**
+ * 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;
+ //priority 90 to make sure the plugin is called before
+ //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);
+ }
+
+ /**
+ * Disallow download via DAV Api in case file being received share
+ * and having special permission
+ *
+ * @throws Forbidden
+ * @throws NotFoundException
+ */
+ public function checkViewOnly(RequestInterface $request): bool {
+ $path = $request->getPath();
+
+ try {
+ assert($this->server !== null);
+ $davNode = $this->server->tree->getNodeForPath($path);
+ if ($davNode instanceof DavFile) {
+ // Restrict view-only to nodes which are shared
+ $node = $davNode->getNode();
+ } elseif ($davNode instanceof VersionFile) {
+ $node = $davNode->getVersion()->getSourceFile();
+ $currentUserId = $this->userFolder?->getOwner()?->getUID();
+ // The version source file is relative to the owner storage.
+ // But we need the node from the current user perspective.
+ if ($node->getOwner()->getUID() !== $currentUserId) {
+ $nodes = $this->userFolder->getById($node->getId());
+ $node = array_pop($nodes);
+ if (!$node) {
+ throw new NotFoundException('Version file not accessible by current user');
+ }
+ }
+ } else {
+ return true;
+ }
+
+ $storage = $node->getStorage();
+
+ if (!$storage->instanceOfStorage(ISharedStorage::class)) {
+ return true;
+ }
+
+ // Extract extra permissions
+ /** @var ISharedStorage $storage */
+ $share = $storage->getShare();
+ $attributes = $share->getAttributes();
+ if ($attributes === null) {
+ return true;
+ }
+
+ // 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 (!$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) {
+ // File not found
+ }
+
+ return true;
+ }
+}
diff --git a/apps/dav/lib/Db/Absence.php b/apps/dav/lib/Db/Absence.php
new file mode 100644
index 00000000000..d7cd46087c3
--- /dev/null
+++ b/apps/dav/lib/Db/Absence.php
@@ -0,0 +1,99 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Db;
+
+use DateTime;
+use Exception;
+use InvalidArgumentException;
+use JsonSerializable;
+use OC\User\OutOfOfficeData;
+use OCP\AppFramework\Db\Entity;
+use OCP\IUser;
+use OCP\User\IOutOfOfficeData;
+
+/**
+ * @method string getUserId()
+ * @method void setUserId(string $userId)
+ * @method string getFirstDay()
+ * @method void setFirstDay(string $firstDay)
+ * @method string getLastDay()
+ * @method void setLastDay(string $lastDay)
+ * @method string getStatus()
+ * @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 = '';
+
+ /** Inclusive, formatted as YYYY-MM-DD */
+ protected string $firstDay = '';
+
+ /** Inclusive, formatted as YYYY-MM-DD */
+ protected string $lastDay = '';
+
+ protected string $status = '';
+
+ 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());
+ }
+ if ($this->getId() === null) {
+ throw new Exception('Creating out-of-office data without ID');
+ }
+
+ $tz = new \DateTimeZone($timezone);
+ $startDate = new DateTime($this->getFirstDay(), $tz);
+ $endDate = new DateTime($this->getLastDay(), $tz);
+ $endDate->setTime(23, 59);
+ return new OutOfOfficeData(
+ (string)$this->getId(),
+ $user,
+ $startDate->getTimestamp(),
+ $endDate->getTimestamp(),
+ $this->getStatus(),
+ $this->getMessage(),
+ $this->getReplacementUserId(),
+ $this->getReplacementUserDisplayName(),
+ );
+ }
+
+ public function jsonSerialize(): array {
+ return [
+ 'userId' => $this->userId,
+ 'firstDay' => $this->firstDay,
+ '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
new file mode 100644
index 00000000000..1214a123236
--- /dev/null
+++ b/apps/dav/lib/Db/AbsenceMapper.php
@@ -0,0 +1,89 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Db;
+
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Db\MultipleObjectsReturnedException;
+use OCP\AppFramework\Db\QBMapper;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+
+/**
+ * @template-extends QBMapper<Absence>
+ */
+class AbsenceMapper extends QBMapper {
+ public function __construct(IDBConnection $db) {
+ parent::__construct($db, 'dav_absence', Absence::class);
+ }
+
+ /**
+ * @throws DoesNotExistException
+ * @throws \OCP\DB\Exception
+ */
+ public function findById(int $id): Absence {
+ $qb = $this->db->getQueryBuilder();
+ $qb->select('*')
+ ->from($this->getTableName())
+ ->where($qb->expr()->eq(
+ 'id',
+ $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT),
+ IQueryBuilder::PARAM_INT),
+ );
+ try {
+ return $this->findEntity($qb);
+ } catch (MultipleObjectsReturnedException $e) {
+ // Won't happen as id is the primary key
+ throw new \RuntimeException(
+ 'The impossible has happened! The query returned multiple absence settings for one user.',
+ 0,
+ $e,
+ );
+ }
+ }
+
+ /**
+ * @throws DoesNotExistException
+ * @throws \OCP\DB\Exception
+ */
+ public function findByUserId(string $userId): Absence {
+ $qb = $this->db->getQueryBuilder();
+ $qb->select('*')
+ ->from($this->getTableName())
+ ->where($qb->expr()->eq(
+ 'user_id',
+ $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR),
+ IQueryBuilder::PARAM_STR),
+ );
+ try {
+ return $this->findEntity($qb);
+ } catch (MultipleObjectsReturnedException $e) {
+ // Won't happen as there is a unique index on user_id
+ throw new \RuntimeException(
+ 'The impossible has happened! The query returned multiple absence settings for one user.',
+ 0,
+ $e,
+ );
+ }
+ }
+
+ /**
+ * @throws \OCP\DB\Exception
+ */
+ public function deleteByUserId(string $userId): void {
+ $qb = $this->db->getQueryBuilder();
+ $qb->delete($this->getTableName())
+ ->where($qb->expr()->eq(
+ 'user_id',
+ $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR),
+ IQueryBuilder::PARAM_STR),
+ );
+ $qb->executeStatement();
+ }
+}
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
new file mode 100644
index 00000000000..96c5f75ef4f
--- /dev/null
+++ b/apps/dav/lib/Db/Property.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Db;
+
+use OCP\AppFramework\Db\Entity;
+
+/**
+ * @method string getUserid()
+ * @method string getPropertypath()
+ * @method string getPropertyname()
+ * @method string getPropertyvalue()
+ */
+class Property extends Entity {
+
+ /** @var string|null */
+ protected $userid;
+
+ /** @var string|null */
+ protected $propertypath;
+
+ /** @var string|null */
+ protected $propertyname;
+
+ /** @var string|null */
+ protected $propertyvalue;
+
+ /** @var int|null */
+ protected $valuetype;
+
+}
diff --git a/apps/dav/lib/Db/PropertyMapper.php b/apps/dav/lib/Db/PropertyMapper.php
new file mode 100644
index 00000000000..1789194ee7a
--- /dev/null
+++ b/apps/dav/lib/Db/PropertyMapper.php
@@ -0,0 +1,55 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Db;
+
+use OCP\AppFramework\Db\QBMapper;
+use OCP\IDBConnection;
+
+/**
+ * @template-extends QBMapper<Property>
+ */
+class PropertyMapper extends QBMapper {
+
+ private const TABLE_NAME = 'properties';
+
+ public function __construct(IDBConnection $db) {
+ parent::__construct($db, self::TABLE_NAME, Property::class);
+ }
+
+ /**
+ * @return Property[]
+ */
+ public function findPropertyByPathAndName(string $userId, string $path, string $name): 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)),
+ $selectQb->expr()->eq('propertyname', $selectQb->createNamedParameter($name)),
+ );
+ 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 a4a1999aca7..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) {
@@ -77,6 +52,10 @@ class DirectFile implements IFile {
return $this->file->getEtag();
}
+ /**
+ * @psalm-suppress ImplementedReturnTypeMismatch \Sabre\DAV\IFile::getSize signature does not support 32bit
+ * @return int|float
+ */
public function getSize() {
$this->getFile();
@@ -104,13 +83,16 @@ class DirectFile implements IFile {
private function getFile() {
if ($this->file === null) {
$userFolder = $this->rootFolder->getUserFolder($this->direct->getUserId());
- $files = $userFolder->getById($this->direct->getFileId());
+ $file = $userFolder->getFirstNodeById($this->direct->getFileId());
- if ($files === []) {
+ if (!$file) {
throw new NotFound();
}
+ if (!$file instanceof File) {
+ throw new Forbidden('direct download not allowed on directories');
+ }
- $this->file = array_shift($files);
+ $this->file = $file;
}
return $this->file;
diff --git a/apps/dav/lib/Direct/DirectHome.php b/apps/dav/lib/Direct/DirectHome.php
index a385cd8f39d..ac411c9b52f 100644
--- a/apps/dav/lib/Direct/DirectHome.php
+++ b/apps/dav/lib/Direct/DirectHome.php
@@ -3,36 +3,18 @@
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;
-use OC\Security\Bruteforce\Throttler;
use OCA\DAV\Db\DirectMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\IRootFolder;
use OCP\IRequest;
+use OCP\Security\Bruteforce\IThrottler;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\MethodNotAllowed;
use Sabre\DAV\Exception\NotFound;
@@ -40,36 +22,14 @@ use Sabre\DAV\ICollection;
class DirectHome implements ICollection {
- /** @var IRootFolder */
- private $rootFolder;
-
- /** @var DirectMapper */
- private $mapper;
-
- /** @var ITimeFactory */
- private $timeFactory;
-
- /** @var Throttler */
- private $throttler;
-
- /** @var IRequest */
- private $request;
- private $eventDispatcher;
-
public function __construct(
- IRootFolder $rootFolder,
- DirectMapper $mapper,
- ITimeFactory $timeFactory,
- Throttler $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) {
@@ -91,9 +51,9 @@ class DirectHome implements ICollection {
return new DirectFile($direct, $this->rootFolder, $this->eventDispatcher);
} catch (DoesNotExistException $e) {
- // Since the token space is so huge only throttle on non exsisting token
+ // 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 05587ab4c2c..473439361c2 100644
--- a/apps/dav/lib/Direct/ServerFactory.php
+++ b/apps/dav/lib/Direct/ServerFactory.php
@@ -3,31 +3,11 @@
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;
-use OC\Security\Bruteforce\Throttler;
use OCA\DAV\Connector\Sabre\MaintenancePlugin;
use OCA\DAV\Db\DirectMapper;
use OCP\AppFramework\Utility\ITimeFactory;
@@ -37,27 +17,27 @@ use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\L10N\IFactory;
+use OCP\Security\Bruteforce\IThrottler;
class ServerFactory {
- /** @var IConfig */
- private $config;
/** @var IL10N */
private $l10n;
- 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,
- string $requestURI,
- IRootFolder $rootFolder,
- DirectMapper $mapper,
- ITimeFactory $timeFactory,
- Throttler $throttler,
- IRequest $request): Server {
+ string $requestURI,
+ IRootFolder $rootFolder,
+ DirectMapper $mapper,
+ ITimeFactory $timeFactory,
+ IThrottler $throttler,
+ IRequest $request): Server {
$home = new DirectHome($rootFolder, $mapper, $timeFactory, $throttler, $request, $this->eventDispatcher);
$server = new Server($home);
diff --git a/apps/dav/lib/Events/AddressBookCreatedEvent.php b/apps/dav/lib/Events/AddressBookCreatedEvent.php
index 86c4cd23640..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 3c8da6b7bf0..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 f9f8ff99d40..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 c632f1817a8..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 29e11ddc146..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 eaf98df60bf..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 d47d75ec1d8..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 ba51002f829..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 d6207ac6ee2..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 c04b383d5bf..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 294c778335e..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 7a621994b80..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/CalendarObjectMovedToTrashEvent.php b/apps/dav/lib/Events/CalendarObjectMovedToTrashEvent.php
deleted file mode 100644
index d7a3b99de82..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 42f296e64cc..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 84157afd20a..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 7b3b95f2f77..32fb1c36963 100644
--- a/apps/dav/lib/Events/CalendarPublishedEvent.php
+++ b/apps/dav/lib/Events/CalendarPublishedEvent.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;
@@ -34,16 +17,6 @@ use OCP\EventDispatcher\Event;
* @since 20.0.0
*/
class CalendarPublishedEvent extends Event {
-
- /** @var int */
- private $calendarId;
-
- /** @var array */
- private $calendarData;
-
- /** @var string */
- private $publicUri;
-
/**
* CalendarPublishedEvent constructor.
*
@@ -52,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 ef477ac1d48..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 a9011bc0273..0f8b23ad3ac 100644
--- a/apps/dav/lib/Events/CalendarShareUpdatedEvent.php
+++ b/apps/dav/lib/Events/CalendarShareUpdatedEvent.php
@@ -3,28 +3,12 @@
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;
/**
@@ -32,49 +16,32 @@ use OCP\EventDispatcher\Event;
*
* @package OCA\DAV\Events
* @since 20.0.0
+ *
+ * @psalm-import-type CalendarInfo from CalDavBackend
*/
class CalendarShareUpdatedEvent extends Event {
-
- /** @var int */
- private $calendarId;
-
- /** @var array */
- private $calendarData;
-
- /** @var array */
- private $oldShares;
-
- /** @var array */
- private $added;
-
- /** @var array */
- private $removed;
-
/**
* CalendarShareUpdatedEvent constructor.
*
* @param int $calendarId
+ * @psalm-param CalendarInfo $calendarData
* @param array $calendarData
- * @param array $oldShares
- * @param array $added
- * @param array $removed
+ * @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;
}
/**
- * @return int
* @since 20.0.0
*/
public function getCalendarId(): int {
@@ -82,6 +49,7 @@ class CalendarShareUpdatedEvent extends Event {
}
/**
+ * @psalm-return CalendarInfo
* @return array
* @since 20.0.0
*/
@@ -90,7 +58,7 @@ class CalendarShareUpdatedEvent extends Event {
}
/**
- * @return array
+ * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}>
* @since 20.0.0
*/
public function getOldShares(): array {
@@ -98,7 +66,7 @@ class CalendarShareUpdatedEvent extends Event {
}
/**
- * @return array
+ * @return list<array{href: string, commonName: string, readOnly: bool}>
* @since 20.0.0
*/
public function getAdded(): array {
@@ -106,7 +74,7 @@ class CalendarShareUpdatedEvent extends Event {
}
/**
- * @return array
+ * @return list<string>
* @since 20.0.0
*/
public function getRemoved(): array {
diff --git a/apps/dav/lib/Events/CalendarUnpublishedEvent.php b/apps/dav/lib/Events/CalendarUnpublishedEvent.php
index 0cea53c6f0d..10d1712686d 100644
--- a/apps/dav/lib/Events/CalendarUnpublishedEvent.php
+++ b/apps/dav/lib/Events/CalendarUnpublishedEvent.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;
@@ -34,13 +17,6 @@ use OCP\EventDispatcher\Event;
* @since 20.0.0
*/
class CalendarUnpublishedEvent extends Event {
-
- /** @var int */
- private $calendarId;
-
- /** @var array */
- private $calendarData;
-
/**
* CalendarUnpublishedEvent constructor.
*
@@ -48,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 ec33412b478..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 4c6b1714721..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 f4d7e21fab1..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
new file mode 100644
index 00000000000..be69a046537
--- /dev/null
+++ b/apps/dav/lib/Events/CardMovedEvent.php
@@ -0,0 +1,90 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Events;
+
+use OCP\EventDispatcher\Event;
+
+/**
+ * Class CardMovedEvent
+ *
+ * @package OCA\DAV\Events
+ * @since 27.0.0
+ */
+class CardMovedEvent extends Event {
+ /**
+ * @since 27.0.0
+ */
+ 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();
+ }
+
+ /**
+ * @return int
+ * @since 27.0.0
+ */
+ public function getSourceAddressBookId(): int {
+ return $this->sourceAddressBookId;
+ }
+
+ /**
+ * @return array
+ * @since 27.0.0
+ */
+ public function getSourceAddressBookData(): array {
+ return $this->sourceAddressBookData;
+ }
+
+ /**
+ * @return int
+ * @since 27.0.0
+ */
+ public function getTargetAddressBookId(): int {
+ return $this->targetAddressBookId;
+ }
+
+ /**
+ * @return array
+ * @since 27.0.0
+ */
+ public function getTargetAddressBookData(): array {
+ return $this->targetAddressBookData;
+ }
+
+ /**
+ * @return array
+ * @since 27.0.0
+ */
+ public function getSourceShares(): array {
+ return $this->sourceShares;
+ }
+
+ /**
+ * @return array
+ * @since 27.0.0
+ */
+ public function getTargetShares(): array {
+ return $this->targetShares;
+ }
+
+ /**
+ * @return array
+ * @since 27.0.0
+ */
+ public function getObjectData(): array {
+ return $this->objectData;
+ }
+}
diff --git a/apps/dav/lib/Events/CardUpdatedEvent.php b/apps/dav/lib/Events/CardUpdatedEvent.php
index 213419d51a3..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
new file mode 100644
index 00000000000..866fae2e224
--- /dev/null
+++ b/apps/dav/lib/Events/SabrePluginAddEvent.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Events;
+
+use OCP\EventDispatcher\Event;
+use Sabre\DAV\Server;
+
+/**
+ * This event is triggered during the setup of the SabreDAV server to allow the
+ * registration of additional plugins.
+ *
+ * @since 28.0.0
+ */
+class SabrePluginAddEvent extends Event {
+
+ /**
+ * @since 28.0.0
+ */
+ public function __construct(
+ private Server $server,
+ ) {
+ parent::__construct();
+ }
+
+ /**
+ * @since 28.0.0
+ */
+ public function getServer(): Server {
+ return $this->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 f7fc2101221..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 fc31005ac0f..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 29231d13a7f..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:iVBORw0KGgoAAAANSUhEUgAAAXsAAAF7CAYAAAAzPisLAAAAA
+ XNSR0IArs4c6QAAIABJREFUeF7svQl7G0e2JJrYAe4iRVG77Lavu2fm/f9/NDP3fX3dbVviiu1
+ 9sWUlQFKUtbh9X5NuNRcAhUJVZuTJOHHi9F7/9Ld18Vev19NP63Wpf+Sv7W8f/70eIwf9d/meS
+ +RLWD/2uld6+NfvldLDdV2VYa9XdsfDcry7U54dHZWTvb0ym43LYNAveHm/1yt9PL+UMijr0sP
+ dWK9Kb73C3eD94aO9dRngG56Rx/Bbr8/7t8Jb8ij4oV9wb3R/8kBOtofDl9VyVXoDHFH3uN/Hu
+ 6/Lcrkuw9GQrxsOhqU/HPK4i+WyrPAPLxgOymAwKOvVuszni7LC+Xo8LRaLgn84636vz+Mvl8v
+ 6PvgZnwFPX60WZblYluVyUdarJf6gz8bPh2uw5u/6vHh4xWPns+EYg8GQv/M9fB7tuMxr8B1/X
+ 63WPA7+4bgr30tcg/zLnFit1/xseI5myToX2a/Xi9s5w2fVqbU5l3QverqtPR60rNe84/wdU6+
+ H89NTdO9Kv76nPrfuGd839xjH7PX4vjpLnW2vP+B9HeI+419f54rHBv0Bx47+9Up/MCiDIf6ma
+ 8l7yIPooLhes91ZOTo6LE+ePCmz2awMRsPS7/f5vFVZlwHPoZsU25+eJ/AFX9vYtHGoeo/ufoO
+ PvvYTz4nj6h68xJj2BLzzaN2c1cM93tfuWnXz1Y9n7mqAad5hVnlO5U2EDXh3X9seZ17pPYL9J
+ 97Vh552L9hnguo+DnqlTIeDcrS3W04PD8rTg/2yOx6V8QiTkMODCwMAHzeyD9D1VO0DuDzX+nw
+ /AY0Wg+4EBBQG9ToCBPSYiFkMNNj9vHWvAMiyGOCR6XRahqNxub66KksjIAAdIIAvgP1iBSDmC
+ sVjAziXAGkfC2+xNJDiMRyf54fXeWDfXF+X1XJdxuMRAR1Av1ouygKLxmJRVqtl6QEIcR2wIHE
+ 0r7jQ8Ce/Hx7AdQNA4Wu5xLnpuvDa4r3Xa4KWgL0Dczw3f8N1qODOz9cFP3gNlx0vNhVI6yJ0G
+ +gFwr4RW8CgWX4b7DWfBfK3wb4ZjAT6zQhjzXssoCcQYBFtxgHAHvex4HmDjC7cwj7vIQAY17H
+ X73ORHwyHPJebm5syX3hh7iNAWJfBqF92dnbL09On5fDwsIx4D7VwB6z+rGC/DdIPTfG7Hv8Y2
+ D98PAcLfuIj2D98xf78z2CYrShyPByW/dmknB4/KWdHh+Vwd1pGjHgRSWth4CbAQQEwro+Itt9
+ TlM8V2yv6KvEagnzEUcENTXDsKGp0h+NgchsIBCaKIvWFaMyLByOFXpnOZmUymZSrq+syn8/13
+ ozqe2W5XjECBzAibqg7BkeROqYiUw7pNXYISwIq/sJIGgtCv18WN/Myv5mX4VALHiIenNUSO4L
+ 5Dd8bcMV9T0AW0TXBHufsN8Hi6AUNx8/uAZ8b4MYIdbEo8/kNzyfRfoAezyfANotAjezrKFsXL
+ TFddIXFTJ/J0N8uDts7vTt2ygJrLyg1stcidRvsNUbqKTB624ycE93z8yXCx/H5NAE6rju/A+w
+ 58HAdMfYG3mVp8R6ORgR83PP5YlGub+bdOCk9LLkE+JOTE/7b3dt1QOEx4cW9O9/t6fqvi+wfA
+ o5Pify/COw9lrvbx/1tDYI019vdQfP7Y2T/0O37Fz6OKKjXK3uTSTk5OijPnx6Xk4O9soOJtAL
+ ICEK4+SKFg4g+cfea2+6Bd+98lFQG9sqiNbRty/bNgOCJrkg49I1pAD61Lhvc3oe7S4RPOkQrk
+ Lbl3m2QRCIgYjeAyBdbfkWIHQ62oImoOdEyIv9VWWKw4nPjPJbrspjPtSPweQ0HfVFEiPAd3Q8
+ QeTqSZxSOf4xZdUwseDgPRrVLUEGIQgt3IkMsUr1eWSzmjFBJU5nyWGDR8uJwF9jnAuTTaVnVF
+ 67VgvcPn5eQb0rm9lgj3eY/8/5tLwTeJHGjFhqHb4Jrr4UzwG4ir7uvOa7HQHZNifB5D/mYF/w
+ K9ojwBTDaNWrnpx1gj4skIntSdGtQdEsuvljsdWws2L2yt7dXTk9Py8nTkzIej3U2dXx11+LPR
+ OM8hAbfGuxbGqcLljQo6lzeoHW6hRGPP9I4D93Bf9HjiEong2F5srdbzo6PyunRYdmfjEF1E6R
+ CVVSqRcysArM1KI6x+E8vCohwAaCMPFcrPsYonq/r1QmsLXWiBbzeF6DyfNquA9ixLcfDBMlmo
+ g7GY27xxVfj/RQFkg7CaxyB8NUGDVI3iJTB3AIQ5/Oy7vXLcDKsET6j/fmyLNfLspovyg12D8x
+ HiAZghG9OFMfCIoi4EzQPInStObgGAGvkBPyxvRhxIVqtCFzIh4C+wGcDWIWfx9XIjoOLB9fPR
+ OiimypAOWLPEoNzxXPnuCd+3Wa2a3Ow5ZpqkTCV5WumN9EuicdqOPuNHRIPCaI9OznRLnk1v/v
+ e4v0qfWMEqZx8AB47HvDyjiAF9NhhiT8OFYPFEoCPMXdzfUMKj7cGp9IrZTQclaMnR+Xs+RmBP
+ zupjffPgrNxWf59I3vlojy6GMA/xNlvXiu98pGz/+MhPfR3BVOdQv4MMN6fzMqzJ4fl7MlROdy
+ ZlQmAHhQNb5iAHZOMvDO31Lj5ojRGoxGBDsAmUDLQOwxU3O7onhNZkZnAHscBkGSwJKLnUNF/m
+ OBeLES1CJAAZonsen1wt90ig/cIP6ufu/wSomVE0bkG19eXjIBBCSQZKn5YXwFhcPIE4uWK5zM
+ aAmBWjPp1TRSdL+dI4Ap2yefjupgCcypSydfloia0E50m2aiEoxZLRub+ng+i3ZMSkfniAkV6S
+ u+iyN67rFsUTbMFN4edC6JFE+eve8/b2CRoed3u4OyDDfqOxRX3WVE375lvQiJDLlze2YnFwfj
+ qi6/HEbBd9G5IwK6FH4tjt4vAU0Tp4LXg7Umt4ZwHWd7WZWd3t5w8fVqOj4+ZrOUuC2O22cH82
+ SL7dgHeBo3bFN5tWPkojXNXfmbjEE2u7ZPA3rtzHyNX/jFB+0fDfZeB697ZSghMm9FgUI7398u
+ Lpyfl2f5h2UFCtgBsQDWsy2DQ44QiN49IFNwpk4pQ3Ig/FYWB1whwN1iAOqcB3N6OQ6tjwBef7
+ ySl6QRx9pp+fU5wqGkEtgQJKG+orFiX/mBEIJBKBrQR+F7x4DxOk8DFeQNIEX1j14BBeX1zXZZ
+ rJUazc8h7jwZjhuTL5Zyv42tvFvy8/UGPwI6PMR6NrZ5Zlvn1dZnf3HD30B/gWiCvgahUF530w
+ 2BYFjgmdhXereA4uHbg7bEgBeyzAG2sWE4kK9LPLmItCso0Gh6Z+7Pfue3PTUqUHEoMr/euAfe
+ fQG/8Th7lbrDHQsORoF1AON6qPOmoIt6bAEMTpSNyz1hY4DnEfi28UuooKes1yMos5T0wTph0X
+ +I+zq0S0i4DAcn+/n45ffaMCh38LqpnM6ewOTX/tZH9twT7h2mgVlghik1rtamcqp7LFeuoWg0
+ V3d1HsP+DwZ7SJkjmSLMoGab8aK+M+v2yv7NTnj85ZlR/NJ2VEaO4eVmtkfhc8znDnrbPiu4V4
+ ScC402tkWMiyW47EcqjRvOM6AH23hqaB8YkBoctXl7RHfhXZoeJHfgcUtpQtTGEukIZhJbmwUD
+ GsSwHcqJW8j0cB4AOsCdo9Eu5NjBjgFJls0LkD7BdCVx6A0bvoEYQKUOFw4TpakVgxhkgYYyIE
+ Z/x6uqqXF5eliWon6IFM2omnABph17R4oEoFJw+cyEr/o5jKoGr/APB05+bnw1qFSwMjvhFmQn
+ U8Dec67YMEdRVJjgT4Y6ysyqHKqmLaZNa1eu6JC2j9CqxhTy0lCV5+wg+lTPRF/WT/CmRfV05T
+ Im1dB7pgkT2SIgPQteIz2duhDmYbqsmqi8qHl4YXoMFZKE+TwzX0XhM7v7Zs9My293ReMgON0o
+ sHNbnfitv8TvnbWSyd77sAenl73krXPe6bm9vT3JPIo+9J6Lfflm3FN8N5lmQu/MM2Hd3Vz9tL
+ gKVknuUXv6eW/zpz+0jiubd1NacGmmrQ2bjcXn25KS8gGJhb6/sIkIqoBcWgJ7S66/LeA0+Oqo
+ Ra52dJAPwa5I7yWlgiIJCZ7nwyYpI5WpvmWXV22IRwXtDImk1CqkO7BQsTez3AeDh7fsV7IGL1
+ E0PhtTj4wsSR04BJ+qwQHB30utJpjefM9eACA9gD3AFPXAD9Q2SfFTGzLlzGfYH1uIDtAFeAGp
+ x6zc313zNeDIuu7t7ZXd/h7TU1aUAf3F1VekacNlUkWBRw6IzXzC65y6BevFFubm+EhW0FIBbD
+ CplkGWZI8gNudsQhcQoO/RNKB9z7ACuyvvjPjVRNKG4Jrk13bUoeCeCXIjpHKFiB+j5XQsqNFn
+ FFBJ2XjiG6TizTBwjbTTP8eegIdG1qTemdfDJQdf43nNnVJP63gXmMaucKGv180hhLaJC0kngs
+ 0Jz/+w5ovujKoNdgR7yLpN7EhcA4HQ+F/CtT/j0Sbr1TM7RraTCds5cO1dH0Nk9b4N58zvrIWp
+ ibPMNb1NYrY6qS8pW6N+K7Ld3Id3xWnHGprZea8Gjzv6zB8ldL+wBYDFq+6A49IzVekk54eHOb
+ nl1dlbOjp6U/fG4jJmBWzAiXffATa8LNs0APESnkL8lQYbJ2ilizGczkaodBHcUnEQCJL2v+H7
+ w8IjumXBj0ZO25lTDWM3CsQBURAEVgM0JVvL0iOrXPXLtTG5SVz3isfgREIGHHjLI8/hLReMAg
+ xF02oMhKRcAIhabVg2DCB2RPhOvpHiwCEpdJCoIC4MkmHjT8XBU9vZ2qMkP+C68sEQeimsIegf
+ gAvoMx2RC0Ynwa0hJb65rIVq42eyKsOuILBFyQ9EtUp7gs0lFIYmo5Jbm15NL8QDJZMTCU3MsT
+ UFW8jM4t3zxHgY8oig1vQLARwkbi9mcqKciCotWJWyydGnhiKqmAoXBHosCBZfY/uTeNQVZzN9
+ Ya6/1PDUaqmPQ9cG1BeBjPKmwDdH/bDotx6fH5dnpszKZThRcUEnlwjlcvRXJK0lttxH2d8zMJ
+ p1y56vuAth6rR/B3tFFc+m2V6qP/f4xDux33MP/dk+lisSUCZOGgoCyM52Up4dH5fXZWTna3S0
+ zgDkrQ8FJ4zXSiY/6xWAvcAZItxOsAxeBTcBDQaMAQvJG1pmaDhJlQ7AHv+4IEzRGAIaTmiIa7
+ R6ifR+MxqJgADCupgXEAehxLEyiBaNenQuVMub9cUBG8qyaVXKPi4qjRgB/PhvA/vr6moJyqjt
+ 6lmEusRiuy/JaPD74YQA/PvtkPLYmX5TXeDzhOVxcXJIegmyTuwhGoCqAml/fqCIX6pmbm3J9c
+ 1XllwRzRO9OkHPn0itMSALsqZTi2dxOxrbFWRm0VbXkRSGJ4SwKhOYGiHkv2ryAb6meb1DENfc
+ uLIu1c/NV1qqLrPufCsvUHdTrz0Wr02sL0DvFVqUOnAxXIGBljhU7lLK6uA7VD1gAsVOKwACfd
+ /9wvzw7U3SPXAu1/ImQOeY07vjnLwH7j734oQrafznYY4fTqBoayaVvZVVEZWy1uvvHyP5fuEx
+ wojSJk9GgX57s75XnJyflxfFJ2RkPGdX3EJVFLtnXDR9SFGFgroVP2Vab//WxO7C30pbHE6iFR
+ wbwIpkGcOYi5EFFQHcSVjpzAWPVVntis4Sp0j4Daux7OEkrd1R5KVqENguO/IRTqlIFx59CJfw
+ dES6+EoEDjCG3ROTOfMBaST4koS8vLgjAiNARkQOs53NE4+syZCJbVbEA+wlyIKNxuTg/L/PFn
+ CCPzwglD49hKmi1kH0C3p9JW0rfpIgCoCN3Qa56KcoGFBQWAtI4lrlSEpmI3lv2JJyVENWi20l
+ POxonQBrLBafhar2BFnQdX7GCjhdNF0Cc+iMv7KL1Ngc8338zdd8ttESQTiFDGseRdRZi3sdIw
+ 2Lb4SKsvBMXENOBvcGIiweuJ66zFDiFEf3xyXF5/uI5qTcmy1NZbYTnAqpS8M/+wi723q//BmC
+ f+7dZPHVbZ1/zP/6wbfK95ey3LRJ08x9pnM8eYPe9MLSo5HT9sjuZlLOT4/Ly5Jg+OAD6ISplu
+ SigyKqwglFVsgF7SyUjiQyH2rypIkMVE0VNAjCKnA+UDGmYqCEAEqZGVIxkz5JUsYLLdnQPQB4
+ ych6SDqI0Exp1eOMQRMH4KCcAcFTiMgClzw0QllpDQEu6gNIy8fvX11d8fDwaEcxQPbtwsQ4Tq
+ /1+ubq+lJqHuxRU087LeuFjAdCYQNaB4dUDawfQQ3zeakn1DyJ8RKDklm9UBbzm4rRkxJ6FaNA
+ z2POY3uGYMsIpYwfDAjBaV9hywQla3B5pzrXw4DlMMCNB7V1PJip3OKwQ1iJoFC6qJQOMu4o5U
+ T6foAVpuTLQB/7N7SvYdqrPMssNsG+i6bwfq6VD49iDiZr65mfZdWBtsIXChgVDp7/vD8cMKlj
+ 7AArOlNZgPCi7O7vlzds35cmT4zKZTutnlnpExxAR+Plf/38Be15r/l+3eG0naG+zJpH2PnL2n
+ z+CPuuV5k0NzlDXHO7ultdnz2iLsAvOfLWgCdpwoPWXiUT6zNgiwSocJYV0+7Nb4CRyGBDlxoZ
+ 6g8AqwCcNRK5cEXlNMDU/D3tORgLgAOwEaUktsRsI2ENTjskMMMXiAWCLIgURPRYJGopRJqkdB
+ H93OTcAW+ekilgpdVakbiLN405juSyXl1c8Nq4JOHoMYapiqILRIoUIn9w/TdmQR0DOo5TZzpT
+ 2DbJCWNGCgXkN2lTonOTBI24ZwE+VDytp8RbYRaAaVOAZhQkWTQC96BrfhSxujhwX4Ktb7x1X7
+ 9JGwfw7r48jZCbEK9ksIzZF6Y7kayWuDMWiwMFVqBp/g30i8laNQ2qmltN7TalAkmrZ+HHE6sJ
+ AY8DPuCP1Z+6+gk/7nAHsFMaMHpnctuUErsdoMiovX7ykFPPg8FDX0DUklqp9MdjHNO6+Kfsx+
+ eO/PkG7uTN7EOy3FoMo4R4j+88C7M9/Eas4TUJiizsZDMrJwUF59+J5OdnfLVOA5nJOCmI4VLK
+ qU42wXEqAQUlcU+lqrrtL8Cma131XSRSRdi5tPCViTp4xgeZtP59mfxxaB6w0OQlCw2FZQi0xH
+ FtFJJClhQIkd8MBaRJsxcHfIzJjJSWiaJwrwXol107TQuSMIyc02NNQDXLLXq9cXl64riCuigt
+ G5uDfqd65vqIEENGwNPHSpcvh0oVO/jxIW45HQ3v1rEiHzW+ulCRmBC2aaX6Nv4HmQbS/VKIYC
+ wfBXrkH0RiigRbLhWglc/hZyLhzM4fPugEoqpzc1Otk1ZD7kfsVekR/jytonwuWGBlN/iquRBL
+ TiycXUtAlWMi8Y2ojvQpqrprNAtE+RwDugrsAB4OTTtst4r3TeWe3wp0AgBrgz7HhIKI/LKPxp
+ AyG/aqu4q4FOajRsJwcn5TnL16Up6enNWfDczCt+KWRfVcoeMfcrWPw7nn9ZwP7nGXu2XZkfzv
+ 6vyuy984gRVp80SON8/nIfue46iJ7qEAOdnbKi6fH5dXJ03I0nZTxGtEogEjqF0Tcw8GI6pJBH
+ 4lQWyNwHkhNUkHdW2iADiNCb/Wk3FEUCmkivkaTCcEez81OAJG+rGkkxYPKhtErj6XkLPTS/Dt
+ 5etgajLhDEBJC6yOPGSVnoWNXZSspDPzNVIdoF0kRkXxFZI9SekTI4HUJsKs1wR4LxmQy5qIHE
+ EeCFeod7CLwRZoFi4jL9yHBpGTTOQYAJiN/AK6N2lTW3y9XV5eUXBaeFxa1VbmB8mcxVxUuKB3
+ kTZzMTR6DWnIWgpVydXnJxZnJWpz/YsHrh0UA1xfXidW4ayiIxGNjql3dwE5AFBQWFTFooknIw
+ FvX74z0Rl5DMszuiwVM5HmolbQCSrJL5Yf01e4A+B5VauktfocksELTa4j7opaE/Vv8d6PAYXI
+ +fH5V74CGHMkK2QV2OH/aUUBzPxrRJO3Z2Vl5+epVmU4nKtyj0krUl+SYn/elbMb9nD9LWRoZ5
+ OaVlda/JZFwJrfPptsZ17P8iPRS9+L+c5LsVEfS+fiXLMRZhO+hdborFQsU/aUyAKHfGsmvGIL
+ yaHH8ecPs9qsUUWtigV6AdfFLVMseHpb90aiMe/CvEV9PiXEfgDkm2MtpUDc+E65G8p6U1IjTs
+ EuUiPxeFBXPV0smOnEIcKN4DkHNSbR+T4nReOmArqnvwwwlwjBQOAIrZIsxURnVmbd1RlQVkcg
+ vDLBArMoNwVyUEWWKMR8zgAMwcVXgnglgBNirotU8uCeHFB2qsN3ZUcKVRVM4vsv3cU0owwyQr
+ lZcdEDBABC5kFFXXsr1/JocMiYWErsruGdCbmlqBdw+EJK8MWgaRIFLHS96c9A+UVXFqx/Pv8G
+ OwdE4zoX3k5dRz675gABxs0BL8dQUX9krvrqTe2efimQmXElj0YlOCeBm+JlZqosIQZ+qpga2Y
+ pLn8ht459RHmThKNHjHwhCu3u+ZBG5N6A6GfD3GGsZBxiOpPLwvRArHx+XFy5flyZMj3lf8HTs
+ UBgZfOAE/BvbZJXEM+0JtgDv/9vAZ3ALvB8C+Sme3P9vW6xQeBuxJ5m5w9tsL8OZi3EXwGzTOB
+ thr3j9G9l84yO58OSwP1r2yMx6V5yfH5cXJcTmazWiNMLaXvXJT0rUjWgSQAexJAzFFp1U4EzA
+ qlaho5BtvWSbNvcTrZmuPCBqLApQPmVjU4jPadkKXEVU3sLiT6PdZoUqNOipIvZhgy15N0Ajyi
+ O51vLmll7gWirY17GhSZoCTKkd8O6cWFoWba1VWLlfl/YcP4vr9FR07ontF8SqSglFaisqkNOq
+ Tg1eyExG34lwZfaERi73wwXPf3JSri/Oq1cfEH2BhThTqhZbJZF5bLNhIUDvRahmn5K1S5+ArT
+ VOYUWADlBRfqfqVAQDrGpyMpddQJ1cNiPB7wr3ILaOa6jUUzwNgH5CRidodETOTspvxK3T27XO
+ 3eeMUhFXgMV1VFTvc+ajWAwEC5Kpc8OxWCrDf3d0lb//y1cuys7NjBsfJ/49EwV8+RS1ayEK1n
+ cf4YrC3zcWW6mc7X5LPocY73adq4nrT749g/+X3/A84QkwkR71+Od7bLS9OT8vZk8MyG43KtN8
+ rY3regAs3sBlMWekJrTI8cnD3Sc1oStKIDBDn0nQ15lDErRVbXiYA2GtEy1D4oDjKfCgeR5Q5G
+ k3KdDrbaCLCmIKJPBfMjEY0sqJ6BcBFpY82mUzIIlIDvROrYFA4TMw69qVs0eX0VMtIe41EKQH
+ SUkwuEjc3ZTgcEQwvTd3gtXjO9cVluaSSZkjvf3Q+wvPgtBjwTFXuxcUFjwVci9xSxWgyGWMFr
+ ncaFxcfypoST3nvoNgKx7u+vKL/Dr5o3WB5qHT6SJ5LOaOq3jXVPgrG1MyD4IbdlYu/ZF2sBZO
+ 7CNseCIhVlFV9dZzTUBSc4jgdHuOBEtAHwF5nYqqhunJqeITO2YR9cfZZXrHDTJi/AfqJEO/YI
+ TC6T1Uwv2t3gGuHewOuHmZpUERhlwQq5+DgsLx++5omacgRgb5REvzzaZxPmdYMkhw9b0f3+v1
+ LIvtPB3sAPcfCrYWn3YFteuPUBbZ+0KhuPPnrt0aN8xjZf8qw+LLnkMJZr8t0OGREDwrnGIlZA
+ CAkl32YoSki5cQm8EtXr8aDVrQYvNUerwsDomjh4uAJFv6XoHUj0zFMrPiF4Duiysl4ShoF4By
+ 5obbhMinDUBlPxKdSkeMSekXZSvhyIaHPjRYT+tg40SDuHh41MWATFwywQ6IV+nY+Zy4HylAZ5
+ LtRPQsKgLQRZJk3slq4uSHXz+5VTbcrHBOAAv736kJ2CdgVhXsPdxnfejn59sr86pr3AAsKNPx
+ RGeK9QKqrKtRRL86R1g4Ge9YlCKwxHUElCWI7Lv7qGrQRtTL2CdJiqS5ajvQpdXUi3XBMHb9zE
+ KGBeH1/B9hTn286TODWdKYSYmzBhQvYwtM3SdpuxG0lbgNS8b3H/a2JJoEU7gEWP+wo8eFvsLt
+ CXmQ44PhCohYWyPt7+8pbeZH7spn38Ve3CjbGQHk6x+HdLP32Ee+ncT4O9nwvR/31vRsqp43sB
+ de3eficS7fj0ieov+uH+ntN7pKzf6RxvsnY0nxZl8PZrLw5e1aeHz8ph0jMAmEsDwSnSdOvJLi
+ wQDDhKaI1EVNAQtt78aEYNUjkxkZB9IDoEfwHsEfUP9vZtfnXgkVQAGhE0XOqTjSYItGMhhqTF
+ NE/wZ1Sy84/B8Cv5Cr846F70UBTUwtRPAD++QLR+pDVkqp4VVWoqmO1aAiYlbcAyDKJ3OjAJU3
+ sFdgZoEAKAAHAx/tFu49jsqMVbRdA8ywK7BIY2dNBtFcm4xGPf35+rr8DhBdLFllB0ondBN1E7
+ W+/WMg6Ab8DrHAfrqAWGgzLZDIiXcSFyQsAjo1zaouukDzmpMPOxZXDOFdy+ri9sahg5G9IRYI
+ wHbW2aByoVD4psndir1X+3AL7jRGvOo4KemmLdkeCVljYLBQRBsRGgXSRiu+ySGBc4BpStYP7z
+ Mpabr3K8ZPj8vLVC9ooaMf2TabixkEbVvy2HJUJ3Id3Fp8N9t6ZV8DeopGy++oA/ffROAq2HsH
+ +24+iW+8g58qT/b3yFtr6J0dlbzwsQ7fZA8ARDKFeiMEZqjU91rrWftZVu9Q+boNaCNxbNR4s1
+ n5jgoGP7wGcZjMmPzHJ4onDhJgdHlMeH4UMARygC3lWY7hGKglAz8QbdNTadgdU5AopsAINwgr
+ UJfIEAmcWeTGyF1CivoA9a6HUMMXBIidYNZPiUmMR0TxzKnkAplV+huQsqBy4X05nShCjIna1K
+ pdXl9LieyGBYRroFrw3jgWghaoG1wX5AlBLcshEIhvLl6gdWQCIloBsMw07QCEBu2SnLLqJ556
+ kH7n68P29ck2TNXnlU0Hlz8saBS7gGjyqHRBF1iVoodRayTsm19vmenclaKmBd11AkvoZ+bdBA
+ AAgAElEQVRphr7BCTvC17NNW0VyWRO0NUbUCRpINpiWhrMX2EieG1BlIMAAw+6q9E9SfA2a8PT
+ ZaXn58iV5fI7bRp3yOZP2loJoC+p57bMruQW+Xwr2Xri3Eq+Vs7/v7805tkblvzdB+wj2nzNiv
+ spr1mVnMi7Pjo7Km5OTcnq4r5aDBFFt9xFhj1DdmpZwLlUn4MveppNtWRKphcFcOC17ZSHASV7
+ /4XVaCFjg4mbQ+B1bac7bxh+Hr8vv6UBlh04uKubeufWEasgLFBtZmy6IQyRATQVYamhRwdlgH
+ 3/67AZw7lwA2NxEVZegmHBtWHXqTlJRN2XnEhkddhiQa1YKDEokLihuRGINO2gYnCx2CTguuoT
+ h/UAPpZUjk7sL8PE6D+jFWZTlRG2oIdwX5Fsi9dRx1EZRx5DyKZJNLLR4n2pwRkvkrqLYmeyO0
+ iLvZiEe87EGKC8mqFTmImFKAC0KFSWYHMgiXSmyHK2TZtYhbkOy/G7zySaAb4qsKuA3EX4Fe0l
+ SOxrSMsjaytDGe9xx6bMh4kdT8levXlKhw/aFcZj+zDn4cbBXHiZgz6vVJlO/OLIH2CeBro5y9
+ bpWl9os7H6kfQ6Vbe3OorHFzgJVw/6OctLa3Hag62SzjzTOpwwk3qfmwjfb6uYOVpAFINSqSfC
+ S/V45Odgvb56elrP9/bI3nZbJGBw3lC7g7EXfyNlSybj0S8Xx5Q8jfnPQR9IPfxUvqiYg8jTHa
+ abF3gSLiSPOhRN84LzxWkSpAAjQCIy81uDlx2U0RnmXErp4MUCWfjDWgCPiBdVEPX31KRcnDFD
+ GeccLpkaSjOoX1S5AChk7R/r59M8xPKG4CpE7vmCYBnoLhnF4DigQ+dag724ko519LouV6Gipe
+ gBQUizcNZiSN3fxFflyV/dCU42EISkf00jMBVAHr0gfShFQTViMppMpAVCOmdLXYzXG/Um9A1V
+ HpZQPF+dMSqbmAecHKSyvgXc0orUEiJyQVuZokU/thFU9uH5pWefnpZk5F3JWQrv5iRPsWhe0S
+ ARPKv1glRSfQl64S0om7q4jv1FpbUybqrHvGp/gc2jamGt2Z60UC3JnBHUYFEgeD5AGn549K2/
+ fvCmznR2b83WqmYZg+pRZe7s2YOtVLQXTUjo87Rbst+WUzXEepnF03VsVTlpW3mKqmveR24euv
+ NrRJNl9z0cP/WawDx0c5JLBnb4UdCkAfJRebl/PW2CvqXHfFykDZ/nw82w0ZFL2zelpOZ7tlik
+ tBwRIjI6dWNX6DC13fEfsGw7u3bSMXCqlX5bpFQpVZDuQCSR5IPhRGZ3NrXiBKmQygdZeFAobj
+ LA930qeNyxt71Mfj+94LhUlQy1Akg+m05MB37JGWiSbusl1EeDLkiAgn+hC10jVqIiEqWZhohZ
+ RsZuIsFAJ9s6ijJTIhYpGOm3ZFOg+1MIdAz4eJ68+cutC7wrw9xtE9KiSvboiUMseQkVdNENzE
+ ZoS1LKL5vuTetGCgd0L7iGGAbT1qb6lTYP5e1zv3377Tc91ZSmtGKxC4mfIMa340eRMs3ZXAxu
+ QyeEDHDfA3ts+11ygYKlWKPva5BoF7JUH0l2qyUGDRDpcUaIaDX9M/AzcncrKi1M1SJPUsiazv
+ WslaKWNoinL0DnYPaRYDH87PDos796+ZXSvBjibksRPQvk2SfmRF2wC9WaNwgbY+3rdOePv1dV
+ vJmgD9pRYJmHenFv9u//Wgr0Coduc/eaCm8hfI+husI+iB/ftEezvHhqfDPbdwJR9rqYOGom/f
+ f68vH76tBwAQHk/VmU0VpQMU67sWdmmxD4pUa6x2ArdiKhQkDoFx6bFLtvAoXhHCh6oegIu1Nl
+ Dami/lywIACxKDCfjmpAdjCaa+uDHCRL9MkYSeTxhZBpqKBE7I3u+pxYC0UzyctfviuAxkUcjL
+ QyJ+gPycb6M8Re+YxGijzm8bkDluJEJHSwnYwIhGpMQXJtG4QQmFJUZeFIEhpaOVeZon34keJk
+ cNXDzY9eetMULjqLt0tPOBB8Q1x1fpHvMP8Ow7eoayp8LKZdg4EaHziF3Jh/OP/AaxPYXr8XnC
+ thSW88oX9SHuHxRVrjmaqCimOxusIf/jyZx/HEi5UzyN6Cue9cNcf5uxZWe01EHXMO5yfALEqF
+ vUAs6lqi/TlPP38njeCx428YK0cbumuPKCdxQKhhv7969ZaEVdpvZUXu9qCcfuuJTwf++id1dD
+ 4OwnxhKp+X0lUnZ/MLz2urbzQi+uX712vsYH+HsJXnerod4BPsvu9ef+urPAPtEswCpl8dPyg8
+ vX5azJ8dlBwoSeuUsyngq73WmA+kQKTUE6BOpWVR/tF5LtYEkjVoHDsv1/IZSRHC0UOKw6ThAF
+ e5jffRkHWE9UfPnUUzB0AlKVA6UMZnMeM/RdKYk6XJtX/phLYKB8gUTOGBHawBSR6rGjS4+0XW
+ NJB3ZE7zZuUjJThwL78+qWRiQ0ZpASV3QOOKgoZ9HFypVpAIMkHzFRACdgr9TtjeSpQJB0lwWs
+ AXvmUgdvw/HIydhEVlfy/kyHv5IyjYRFY4DqkXvK+pIOzFXERvwkbzmAgQnTpw3tfOgmFy9PJ+
+ Xi8tLRsisambHLe1cKsdOekqePLmmrCQ29eUb34G9o/sUOqTIDU9A6R0Xaiu1KNGliupuGqeN8
+ EW7bPHEWxbDohVaHtl8sBdYqm9sjc1FwH73jFKxGDnSZ9TJnaeCHS5UpBW1i3r1+jUBf2//wMd
+ TtNpG+Q/x8Z82tbd3DXdE943pwl0FUbdM5RpVTRvNMwxqF4aN5PD2+2qJoSiiJuxv6+w3PuMn0
+ TiPkf3D4+ITwT4g52HJgYpqz+9On5a/oEHJ3j5N0NRJasXIBdEpd+WMSiUblK5bkkq4LaI5BwY
+ 7dPKSSkJlAi8XKFT6dJxk1yf43IDdA3hDvmYwXveH3q7LMhjStjpZ1DKUxSxcBCYz/oyJH8ULI
+ lF64pgnB2ArCSoaBt8TnSfnkKYcsrbVLkB8uaJW+ufY4TKUDiNNR7UEPKhg3Nw6NghM0kJF08g
+ yucuAkyIWSexi4LET3tsVqjgOdxYARRT1XF9xR8UF5RLSSNFK0O5zIVosCvTx8skXiDNepeGXb
+ BG685PMjZE3dxyqNwDtdX55Xnn47Hx4X9O+kL7+0Jzre7b4XLioXGq6m0WLX/3z+SFrEZOM7Ux
+ teVeQNpGK4u/g7JvRnzaSAXTJQJvE4h1gn8heOz+7YPp5MWirFd8G+2rpgcCFPRU6a2gA/tOnT
+ 8t3796Vk9PTaqCn0+zA+euAfTv1tytYw7XrfQPWuR65MveBfUvLbCtwaiJ4S1ef12Rpy3Ujrdd
+ cezFum4uuehF0KqlHGudhWL9vt7eVoNUA2BgqDfhoUIrnPTg4KP9xdlbeHR+zJRtkhmhPiN6y8
+ ZjRLFR1LJuKkP+GZG3JoiPSN31oxKfs6sMkJvTc4ILdx7VG1a2Hjj1I6FjpxiCgCBCpK9pUI28
+ UVAHkoMJhu0E5spHzbukX7eRVKZv3wzXogD12vwJ3TWJZ+WIgkrKA1TAqTc0rgrYhaLr3LQYpg
+ BYVsJj4sxmMzxSli07RwhiQIVVkl0gsdngfXJ8kAgG+jLYN9kj40mHTjpbQ2y9vkABXoRR0+JD
+ +4XOffzh3lyV115Kzo+0reM8EyHTknIx1rVhUJctigP3F1QXPmVw9Fy8k2dXNi5JUyjtlKYHFp
+ Z6rFUmZ45zwd3D23QKiEYnG411xmnsbeKhKGlths1swI7d0ZB+KpAX7DvY7kGkDBu5OGdnLkoL
+ 3x7s1Xg+MBZvz8XXOY9TuZpaSQq20v79fXr16Vd68feuxlpxR6+pq65CPzOnfp9W/K8pvOa+Gk
+ qlRehKv3fMSvet7YEL3RAtGx9m3KLK9aDzE2d8C/Eew/1x033rdA5F9F2VmadWgBJifnJyU//X
+ yZXmxf2AzKLUbROA+GKE0HQ1K2J5EJmaInmurN0Vu6kWLCTJkARQkg0jSoRpxujNTdJ9GEt4Sq
+ wJRyd/VSiXsN3PRIoz6wSsT9HEuShSzAIa7AOvqCTAoOBpxhxIqhkVTzjPkPfDJ6WjoSB+nSyl
+ oVC9GAPLyN6Bu1FWKShlw87VaVEiRyJ5e+fDMmUtfT+Ao7kHb9Gel0sa+PQAegfuCwA6LY6iFK
+ P/kwqHmJfTPcYN0KHGYeC3wv5/xeaCLcJ/wGWHaFipGr5dbJ3cG8ytSElzg2Axd1aH4HXQb6Sr
+ 32WXiPLYSprGobnLSHEMNz2GXLlJVuh73g30Hw6BJBDBdzUOAP8DCyuuAfxLcScBWsDdam8apN
+ JBXnnZXFcDnwt40HuE5u1tYFmY0MGdznoxvWnygIQ4qodXYhSKC8ZhumH/9219FT5IWA1UXr//
+ OkuFjM3wzAfsQFtwF9psMfXu8DtRvUz9pmh6w53xqwD73qA0XPwb2OvOPF1V91ci+3rCt5XL7g
+ n7s96+/9XroBn6lx7fBnhej2d5aw1wNoKSZKzvoRvX8rPwPeHbPdl05CMoBAItBrw5UkP6Bjyd
+ 1Q9WGgcPApqImdUW6mS8I9HgF6BgYlKXRhwBPO7wU64hnHdDxkqB1dcHvmFiYVMwBoHo0FauuV
+ BUAGbis9NHCIRBj+bv70CZZ2z2myl/o3tWa0AlJfIb5okbJpHxMZ9QI30BIDjzyznR5ogWxrhO
+ blKOnbIAzjpdOtrKROagWetSLkmKlrWkkXJbrq0vlSkCVQOHjJEloBn0eGadhl5FEedob4r2Zm
+ EUSPIlKm73h/ElvwWOeaikt2KI6lIhlv19LNPE7i7FY0wCrZET9kH06gIDU041ScF4dB9wZyaX
+ CP4lZsvXRjqdWQ2QI/87j+BpzMWi03YT7eONE1ZOuV55WLdArspfMkslZWyTXRCqA372O+To+I
+ JuNEVo82hQOyjKAO3xy/uf/87+4s8s9Vl6m2zU+PLt/r5KnfT6uUgf2vL9bpma6LN1ug+djXh5
+ zuj6eBTiRfbVJ6CyNNwmz1Kx1uyiTiE3KpN3ZbF53vB1xxbwOr3Rjk5wdum7SHX729cJuZZHbV
+ V6f6W56I4vFwzfoz/kMXuztfaFVBnFxlBwS9EihZfGT3Vn57vWb8ubp07JragZRGxKpskLXTSK
+ IuPUfpZXwrWg4uVRuwq2SmfohilE4/AhiAHuANyJpWgiAt2aCTwm1tWkDqSTE93E3wOSwIzAUF
+ tGNUBp4RlzoChVpJ1U1ibI6+wZEtOpCJK44ExPXgSBHKaSkoZwYyCNYBZOm3yyiskIopfRS2wy
+ ozkkCVzJO7QaYR4A8FAuWnxfKKZG11mR1xiJ9Yo957CSQf8AOA/JLVMFO4MqIPMJ6SVqHdQ8jL
+ W5swJJKV9Iusay2cqbJW1B2CaoNNQ3MD8jJEjuc+OmwYMw9dLMjoawUnD12Qszui/Zi4Z2T4Tg
+ GnUK9ojP/4J7BmjVedGx+RrO8AJIZhcxP6nCaKN93pzPj8thUm8CHvzoA104Ir1rS8sPFdqbAQ
+ O2wgU6dK4MyGMtCAefKquVeIZX2408/lJOnT3mPpfLKeWTibfHWD5/mxjMeivy7xzeVOlsH2aR
+ otiL4GKxVP6vMA1pj+EjsQnb7S5JVCTNuf3lB5fyVmi94QjDPgmuQT91P9okxq9PivOVn/28N9
+ jWy37ol9Wa5O5Glarh140Epp4cH5fvXb8qLw6MythsgIxkmYBXhZdjylpInR5JWDUTq1LWahfB
+ uPxoNxJ6klgR/Qls1zmIxVBZm9gGVOVkGMIA+nyZST6YhDR4BZOwIEHnJdgHJVYF+dhNMXjm5i
+ MdaRU6tXG26J+G9WGU6lwJGSh2Nrkg1A4CY5ACz0ENsbmLJJxOqcYN0dTF2OUi8gmunDBWKpDU
+ ak1zadVG+OFhcCDagTi4vJOMEOUQaRHTQCDpkq2hiAxHJanx6sIhNJxPuVLDTIKdOWwdde7zHz
+ bVsH67TQGasiYtrEBtmPs8SU0Va+uI5MpmffgOdWV3mI+9JbWPYdSNTFCw1F5cBHtQuj5XDdyW
+ eDybfHD2ooEAc/Kd8tfw9e/GSoxcQadxh04qxKtUVFVRpesICK7XgpPOnG7q/efe6vHz5quzu7
+ tTrkZ2rru+nndu95/+RYilera3gbrvBCS9RKpPv+Lnj6M3ZS4nBz8cd7fZatWW+1i4GdwJ+BXW
+ roDRoRCdu/Zz7kL9Hrlt3Z69/+tvtBeffMbJ/EOwzOTKsVmU2GrCQ6vtXr8vJzi7dLZM0BGfOq
+ Ns3RrpobfGZRXeVLMkde4sgqo8ZVyJ9eeO4dR0VOGOCGW4glDuILjkpRiNOIk0o7M4V1QuY0sQ
+ cOwZw+ZtyPRqO0apXPLoA3Y2mrf0OgHd8vRtGwxZhKWDP+wq43MXKlysqGSRHEdkhmudnAO/uK
+ B7XKDYDukZSAilZq8UWvDyiYXDteP1kPCETcfHhg5VREx4bhmfg6nHeoHIk/1zz/YCO+I+FbrW
+ BiRKd6ZnL5/l6UAEFszdf942etKCKrpWMBs+v6N6aeMs7o5oIjSW3ZAMu7i0j+TRvt0KnWmfEK
+ 18OMw7iK00kUzuBvI6YCNX0joGqG7V6RiZ91DUfA/tal9lU4tYEL8De0koCOyrFDfaM/p3AZW0
+ E54RdU9nhrF9OTo/Ld999X46Pn1Sw507V8lJFtN/qS7vQFgC3q2wfAvt4Mul8fY/q/fCR7wrpK
+ VPd7iWgXXrD43hRDo4kmu+AX7stx/yueYiijAuCt0r82yPYeyA9APYKmuwBTxpsVfYn4/Lu5Yv
+ y7vmLsg/QoTxQ7dhAD4BHrvxmYqlaiYioshQktGChgBsGWZ0cKEXTcHsXVQfldpBNylYAx8Xz4
+ kezdGRFisWe+Ex62hvHByOAgo/GIAdw0vo41rxO5mX7F0BKqXuNauOPYjAGWCFqpR6eLedEYaS
+ gKtQLvkOtBB4bfDfOP+8PlcqK2n5HzKYvWr+dJKgjA2VVMK6ztfs4T2jiYXRGnpxROyLvaxZpA
+ VBZrHYTNY06yqBWgZEpk6xXsopG4RR2BXTnFD0jjh05AlQfC7DxGVmNbB4/UJu2kkqWKgiQhcK
+ C3mFdOgjXS9YKbcFaFkxG76w87hqgKOJ1xXLAynY5lbox7ItRMITlOZWfV2S/DS63oLUChh8xT
+ 8wpQZ6+A2TkomjV4UK89AfACXNxdQ4ouYvdg73yww9/Kc+enVVKMp9Ngeu3BPssul4m60qq/HZ
+ dEO+J7LttVUA+11mvRFByJ3fjy4iGQeKtuvD/rpwnxB2h0Phs54QY1kUZZVxRxO/In2Z/zWLwC
+ PYd2DPhsu2J02zDqOQI0K1X5RgD9fWr8ur0WdmBwoa8rg3Pxk7G8oZowlFjgogVFA/3AfqK7h6
+ /A+yj4SaY2MudYMS+npNKi4SmYGIGcs7WOpma93UFe5b4A8y9vQ7lk96qiLIC8gFc0QSiFZK4T
+ YKW5x2wt24+mv2obKQScZLQ1AcKwehXjyImgD0Sx6BAzNvXwe4qUxZWuYAJx+eOwYsC/HF4Xdx
+ qkM1K8Bmt4CGwkk4B+CMXIiXOagHQHzoHoRwBP8t6xUVB97iL9HNPcR1QmZtz4k7AfYFx78LlI
+ 7pn60IuovEpiZ3xQq6WDASVJ8CcB+9P3T3vpRagyDBZl5DetI74BYiifmKfwOCBNFUHMhpDXcy
+ Pnzb1J7cjyXvj6CayrzsFtLCtoOXIHtG8i60SuGhsS/aLscYWm+sViw6/+/778uLlC+aiuIMzw
+ NfE77cK7LdonFu5yJCs94I9vJfUwyBJXNE3onFSFHf79J1n2wjkDTTuLUyKV9pMOeaw+EqdxoI
+ F+FkKPoF6Jx6RDDa7oprEfQT7TbBPGoUTSMF1XXcj28NDiNrPjp+UH9+8Ls+PnrCQCpF9IpghJ
+ JdNxBP+EaCsaFRFUFoL5PcdGRvB1koGSdh1XHnsyPYXkd5SXcQJBGw0bvdDbp/Nl7b9TgGuaYi
+ i5JlUKgAmOV2m+k4LDj59OPxuARBU4PXgZzXIdY7R4qeKFn8DnRRQ1Jqn82WS1gAZ/3u9p1os8
+ stqFr1fYRIP7wXwlhJBtsnYWZBDxmdJMxLvYHCRoXghkDqJKnml8gqZMAFl7QAEkFL2oBpYeQf
+ y/0j2egFiFS4VQ6bPHIWH1ooipuZVnDBlgtaRY9RAMXLjYEO0TIsMVRUrQWttfQIEm5Clq1iA/
+ hZn33gLMdpsOPuMvntYhozMj0JtOPsknDWOZOeBz4IakQQhqutADcKQVeG8PoMeXTBfvXpd9vZ
+ Q++CKXY+xL8X5RgNx61AZt90Dd1fb3sfZlxXab6ZITly9PpXqTqLU2XxjeiRUtiacPQLNLsAX+
+ CQv0oG9DNNSdTuw900C0PZ7grHMVX5/BPt7wF7+V/UrNE6sdsfDQXn97CnB/uneQRkzqtbTqQp
+ B45AmgcoBj7uJoib43PdA1QjwMGEwMMLxI9QL8GrAaCAwWdl42mt7LiRMGXuShpEWRrHDvq2UC
+ XbFYOqxqu5JrF71+eO8WIAVvx1bHXCXQa8e0Qio9NXComR0uxNo1TL6BKp6BXABtLHwtIVViUS
+ ykSKt4faGXFgcvVStvV05k3+Q782KlA2TsK50Jci7l2x2JXgPWhmjs9h44oVAyVY2W0kbRhdNo
+ egNNDSej8UF+YLsNsjDgx5ChzDvgpAvII2UCldq+1WAxQ5fpANFAVnH5yh/c9sfHp/HtUQ0tKB
+ 67sK1U/eTxzMNEY13PkcbsSqy73jqh5p33EUrZKHSaFezmzRdRw0JbSFM4/ShwollNpFcVdqq5
+ l6X6+Wc1bSwTkC9Spsvve+9P3UBeHhnoHzI5temNLMmYNuCqfwcsCfFGsO+gL2WXX2gyFC3z7w
+ FeOFAv22mwu53An4BuWeSf675libir4BPhicCEY2VrwL2AZ0vvTmfehO/yfPY8xlTUspbAvAW2
+ CcyxefdHQ/L2xfPy19evSpHs90ywo1y9SmAFT4rcsUVz0+tu/0VaRQFsCdIdgku8W3eTbiRuIA
+ Vr3dBFpqT16hOyUscD2Af/TZBzeCIgh/p5Z0HSO9Q67ABXqQ2AAGMztXAXNQFJJeSAoJbxxdAM
+ u0KE7Hh2KBYaHNga98KMCzogp4f1aof+Bz619NhEpGxom6ZrWnVi0TTcoNO/WPLAQx6nEOoY14
+ D7AgsA6Us8urSiU8/tpR+P9W7sKJAshjqnuW1wBnnTMtlLES2WY4EFM/F/QXowvgMlw/X5wKNU
+ 8ihyheIiwv0NXM3H3e0x10IahJWkL26QpjMnhqfp89w0I7LQCwTIOEMCpqTpb8+8xod2BNbGmO
+ z8P0xiSM0J9fgsQ2wvz+y76Lsds61KU0quJ2b4sdBgtY0Dv4O2ga1IvibaCo3xLHf0Yebi3J4s
+ F++J5Xz0ruqVpXc8Ki/c+J/fbCXZp6LMFdaRPaS32o36OY0pnAE9tspkQ74xb+HYsvnbOWnCiD
+ DwWehF38fvt7SbjzLc77y+8kPCupvgz35oC050vbv913zT33e77xnf8zTawMhgHNmgi5SOPfKs
+ a+X5WBnWn6EmdPzs7I7GrMjVXhvdekZij+FaRl4PPrOdPpjZunAz7kXLcYFd/A0kFKUrMSplDT
+ 02EE0xLyCx1CiO8gefcq8f6YLVASj1UN5IJWwx1GzS6YquuentVwRoK0iJZ1LdgqJjhPF43u0+
+ eTLw2+i4Tikl7QJliwUSVn63oMjd1EYqRTuDsA5Ko/BSlvz16Rt3OJQOuYVpajclRA4tSpzAbL
+ 1MY6FPrPobCWdv6wYarN3G7bheSz2QbR+fq5iHrYbRD3CXDYWc2numWi0q+fV/LomaLE4IDEta
+ ebc91OFXUyEWz5YFVD24BFA+J7jfnJX4GbkbviuJisiBirv7khRxVuK7Nkft9bud2qbvK6+1gt
+ 8pZd8DgxuDSqVVui2eU36MPLObkqaXfLYkaCAux5XayM3gl0cvZiMK7j/Gc/nN5fc6b377l159
+ +67ivIY82l6n3dzjNsSq40u//NgYnPXE2WTjqXHJKGsYO6mPLzKFCGoyJHyCvL3yMco0seiH3d
+ Lw62xJdF6bOm6YrV6IQPQVWLpCvga5YeT75KxGhNdU5MEj9oZbEX2bXKsXe0/FcQ/9Xmfd1u+8
+ avqB+7axpkj2RArMPoty3K0t0uwf/vstOywDVvXlISNuZGEMo9Mu4SBImVq7CkrlJ1BTXiizoT
+ gAwlkt+CSl4eD5ARWxOKsMcF1I3us3oTKA8fevv58TvIGbhwO5UicLXFccfJduzwuMvZ4YTEXk
+ maOBjOQYtWLOyKaBGCIfEDXUUk6e7T2Q0TcvY+S2JKS0r6AHYzE/eN/1PMb6LFDws4ifUyB65R
+ 4omEJzebClatROSNY7GrgV48dBqt71Xs2ahxSN7BVMBDjMy6ur7nroD00aIiB8gLsL7DorItBE
+ bG/LDyPKCFdyK65DPh3nhvpLdcZQJLJFsSKCNVsXEVUXESXAhFYSrAbVZK9pAlso5GfAXGmafA
+ m1SnxI2BPImEjQWtlThPMpSgrUXANdBod98bM2wq0Ecs0mw5TeuqEpjEvA78NsMc4R4AEDyM2j
+ unTJweJWvZWYNCj3gLbuw6Vb3Unkcj489Bh2xhNVen5ykKLXFyaxSigkWmewN6/M6qHZbZoQ41
+ nmSG2ihgxOlFAiQ7VlwE59QtU/CUZSwTRjjCdxliI19mSU2TQgH2ifB9au4OWs38Ee67nTUa2i
+ ey7IUAdzcnhPsH+1dPjMkOj6hiHJVIH39tk+xMZR/lBMzIXSHHCww2T3Y6kCsG9YNQJgy1L/0g
+ v0Cogih3YqNn2V8OveqYrmvcuwBEpATJeJd5lSLEiuSS988Ep47zsISOmwZyfE5vR/gPEWGiEZ
+ t02VAvVFX8cTHaHSTXC1wKzqD1loWkXjePuWgRUcbuI6nU9wHm79yu7U4k6UbQODxz500C+uSb
+ dI+sCth1sPH3wd4A/dwWDAaWgH377rbx//56vwe8AECxUNDMDjeXFtIB7vnUAACAASURBVF4j9
+ ikQR391BcdM8/lutA5//VaminPgjmUpawl8ITjQbl+NzrGAyRbZlbyJJh3ld1GtxqjZfudJNGa
+ 52NcyfQN7fXan7c/ui4t1tdHRPd7ovx3Ab5B0uz+3Xu/x4TaFtb7Euzo2IHf9R9CN/kG4hlwMV
+ +Xk6Un58Ycfy2x3l/MC1wcqre2vrw/2ZOX5j/mM3w32asojzh73D4BvkA+FRxhpi6AM7t5Jc2M
+ VPt5MQhaIyvdj52vun0IO/gyLdMk3Y9NRQd6LSqhOZgwfwd7D6RMje27pBz02FAfYnx0dlKm54
+ HCT2sYOVDSRRtiNiZkkaPKPjxIFVAB2BKFMopZhhNS8FtbHeC7km5FPhnoBBCTir1t3oS8HBKM
+ Sgz0i2irHdAVrKmpD5YWWqdp38/0YWFh4sKNIC0ECRW12ItCGygeDNglG+u9HSbRQohOXHXz4y
+ C0GScdA7eQCnOrnw13EQgVllpkhCa7dRp+dvLBTYFHWjZKs8bIHoqkhuuSNUMukexiKsj68/42
+ RPSLzndmM0RjAntGQXxMZGyt9IZ20AZioKQ0eevJAaVMtiDW2JAeFHl+5AdACsNNIsRK6aJEWs
+ 3uo+Hrtkni+TM7msyaS7loYVvBvfF062segb2+fAH2nFFLsma9tMN/Ow20/ro/egr014e5XDED
+ H7pA2x7FQ8O4OC2Z/POQua293r/z4Hz+WoyfHzlGh2O7bg70IUfFYJF6at9S1WlEZpci+c3xVZ
+ D8v66W8jmpET7BXjQeXwSrl3kywhh3O9RUod7r4JGWTmIrTaMY7n8nrmXHR2ZvUY1Z3UtM7j2D
+ /+8Ee2vDnT48J9qf7+wUWT1xZURBFSgc0jsA+IIjvUXDg8SG7RvVqQhPb2iRHmeAy9cEWcLYFo
+ D86CqvgIwJZoI29eHPJW1uyGOA1b8pcAIqCEL1bMx86iZ4ziM7tSln5cVYDU4LS8YD1uMo/MKl
+ pYEpiVtJTORjS6dI0EaJW0Cfcrdiz/cP5OWkQeNagny4+D2ibapzlGgNG9wQIVfgiSsE1HE9GS
+ g6uwcnP6YGD84B1ApPaKlVV1IzkMUAevy7BrUtbj/t2fXnJfAI4/slsWvo9SSrZf8CUUcAQ54v
+ e7qSHxihqA5VjCskLKpO79tYJuNYG65RdisrCOXKXFp8b8sMdV0xtvWWXmuQdsAaIWnmfouwkb
+ KWzrCqdyv0buOoOABLcLn7eBvMakWaa3EHjtGBf7Y9dHSrllWicNPGRsggBw7AMZ5NqH/3DX34
+ op8+eMd/FqvEoWZrw/mtH9q0MgzoaziW3WKRoYcUxwOtt6W7dPa1QpY02l6ZsHNVL4oEl1AsJ7
+ TG64qbkR+RBl4Wyq3bVDqvh3jlCnIBtFgQGgozgFfBEZ89xZc+sOm7wnEew//1gPxuPystnT8s
+ Pr16Vk93dMnIP1kjM4vxH8tethHUDulUY/vNVF8/2d6Y7wtezx6yskAm6jqp5U9m3M/p7ebDcN
+ Wh4o63awM+MSr11VCGMFB/kr5fRc69YkUoVkMHS+TsOXWrUqZUW766tq6JHHAfRWBaSUBmZq9H
+ Rx7AJHZ4AshjuI0bK2K2suFDS6mGhIiPMPewgGD2bdlILQC0seN311Q3BHoVM8M7BMRExy2tHH
+ vtK6OLclcRmIr3XY3IW+nkWOLmgCnw/6SMjYfIO3FmM8NnhvaMagpsb+wDhXsE2AQVVnPhyseS
+ 9Ck3TRnum2ijA5ZrUeeYwoncbxnaXVrHWAN1SMoR60zJ5jZKc5r63bZGtu/ft03k2YN7uFO+L/
+ L2paQICH8DfsLizrsReS8zXOCjhgr075fVC9P/u3bty9uKFrTvkq7P99W3BHhYG3bumDqUDewU
+ duMjY3RXmdebMESGaD2fP72wwFNFDpy5K1E0VE2gvUzOMWWyHlm5h3QLv5GyqY+2S2uupJ8ZdN
+ E41RWvpo0ew/zSwJxvCVR+2xqPy5sUZ2xAezWZlSBCRvJISS0b5AuhQIow0XTREPp2yS21tyd8
+ W+984eMNzmdxs9PGidjrjqSREAbqwXAgFpIIqRxM0IZPVAmgXTew1E76UOTqxiGgUn5GfwYZoq
+ ZBVJyXFKYzIHKWRs7dEkfSMdwc69yHdNHlObvXH5h5W4+DNZFi2YNHTmMVm8s3BVOFnYHutzmd
+ GUQwic7hsSr6E3reQ+2GRmEPzfnNDrT1zAPjMAHG2cpYtgeSdiizZmWs4LFcfPpB7xwKjZuNw6
+ AQXS2kQrxOeyybiAGT4z1cvf1EsWGBwPQRmyn1QJePG5fTAp3LDLpeucOZnrGX13bIaCkegzdG
+ 3pUFvonHzzIny6/eYq9kcTQtDIntXN4tsqMFDlF55s4doHMavlurquTqWyDgkspWIxb3FmGMx2
+ xbYs7HNaMgkLUzR8Dye5x0VUV8f7DHazNmz+vg+sI8zqsEeYdMCO7obFlMK6JOkRV4m91nH6zT
+ y3c8q6POEr1F+WyEr6WWi/AA4zld1O8KNVnLZBZQdTlSKaBPs9cbbqo7t3+9YcLtI874H/+x/f
+ 4CzJ2g7jbO/My3vXjwv3z9/XvbgFskEpUzKqDygxFFgn8IgvDYdjJR0Ed+WSFlbVuon2DQDVA1
+ VPQZxACe+8J3RKa16XSBFOkK8b43A4hzpyk9xwkoC4j3j/YLRJOoDiUZLDO35HskkqRMDNhUrw
+ wEjMXw2eNoAyKKjrpp5modpkQCAIvlLHbpdJvFepFYsWZyMh2U221HPW9M22CXgGgEklSfQ9p7
+ af8g6EdknMWzaCA1I5mhHiGtHY7U5FxScH3cRbjoeDyEqQi4uy/UlGqCMvDuZl7KAXLZHNc5yB
+ Ttk9Zbl9VotyojW0PL/wcIOsI9WvzOa0/3gom6ffxavUcqK8eEiNSZwBbmptOCivtF+MFG7n9c
+ Aa3ffN4uEOPbonWNVTlPtnB2BmeUtsK9ZW16v9utWgpbBQ0sb6NkZzUrOCuwnU4C9PIbwDNyTy
+ d6U1xDj8enpM/amnc52xIPb76l9fzfma/IEd8X/3SvuWC+awxmkG7BPHQI/A6Vvss/WLsv/slt
+ aXpcedPa8T+Dpk6/BThtjVwGJePuGyvFJUXKcPXloGzcw4S7aNQy0SXCkw/th2gZgr+hff+vaF
+ GoxEJXT1UrcXVR1DzB/Kuh/Dq5vbkdvH6F+qM85uO7cRqb9rsP40vghRzuURHVuIgjqTvb3yk+
+ vX5UXx0/LaABgR8JtWCajzj0SsSSq4br0jyZyaAfeBCdYSDNAWWL55mjcySwxWQcDKT8wAThRw
+ ItbolYB2qoLFO6IL3aiDC38AHaVBtJAAFWTAR0zr8vLq6qawMOiUiTN1Ptr4GSBwutpHYw+vDA
+ goxRRXjacgtbn4/VM/lbe03p8V8jKMRMTfmyVjc4N/LgqaYsso1GG77sjmkO0i5KiUg0B3K8uz
+ rtmGIjSufvo7JWR6GWFp3sFDObwxFGTE8g85TfPO1T7w2InIRtm0DfRvnun0V9zZ5DdDRa2cKm
+ 4H4has7jJoE07F/wdx8T5edMofyXGAi6Xp0eQkrTR4LSV9U5J+Kp00X/GHR5QUGCVD++LArpaH
+ OQcvkKNDP/N2ZA/b4N9bWDuQEPlIwIn/CNV5t4JtNEeKgDKbmA6nXAsY/7s7u2X12/elP3DIy3
+ K3PS0i5tTqbk3GyLMLDEd+Lc5jnYJaud+Ex9pPlDy6GdXySXGl3ZpStRqjuF7WWEHiL9jjOHeC
+ vQ583sqWAx2ibIx7MvSplvg+cRNYzSAtbj+FEyZLchepNeBfXh75kzildNgANHnTruEf1ewF1d
+ Twb6TlKVas1cA9qcH++WnN6/LiyfHBLp1f1X6I/Q2ndJyV1y4+seR98WGy2ZekWBqydU2jaoav
+ H6qKlVEQuPxhJw1gW6k1oL4LzI+0hwclZpQBBPa8ErrHToGr1Mz8TQxUSIHShW8N1Us4KcBdIz
+ gUQosn3fvMSvgiwISBYIhC/BFtItdxRQFWGwDKBsC+sXwM0qNE2DGzzgfRNvcpbCyVMVHom7gZ
+ KmdDc4Hih28BlW32T2wKKwWcHVmZLFMQGRPp07zlVww/Hx1oxKY4ouS2cXaYK/ELAuqTMlkW4z
+ zRdUtKBokhsH7zxGRA0R7kGBq1xAvoVwnfC7e+zVqIlRhnNoKVR1rxyN1B3kXAkWSxzjHVOYC+
+ GSxpUVOKinp+AV14eZdWJdq2aV0+zJIM2Xh6FSRv19tIzWNzWSBNsHgPrDH5+QCR7zqlCEYdwF
+ 77JygQkoggHsAsE8vhdF0SrA/Oj5xsVzHMnRcd0eDhODqzrBd4rrn3fd4KKd74K6qoehuaWklx
+ x1AnnkYNKIB2IOvxz/93HN/Auh72gRqDbAMMXT312ogos5WKLr8WwVSTNBK+FAfl1NO5z1EnHG
+ TE2CAc4aVQnrz1//RERjby9zWVfiWkT1XzvuueuD343uyB17d6YzvfmKW83vA3jcEGPH86JBgf
+ 3b4pPQR5fWW1MhPJzOCNE2zlqrqVIcr0CZItsa1TrQLo29PZiSqdvZ2nYhU5A4gZvUslDtW+5A
+ a8QQF/y3XRNFBsStgNSoTmAKDhfu2StGi54KGoDsjHShlFQBQYfcmWCuw0EcRaOyKCeK1R6gGp
+ JLDhT1gabTmra4046JqBASKUNKAO7LN3AsMUS4EtHcYlMlELRbjAQ+AB1iY/JSCAwVRsFem+Rm
+ 096KOVsgNzMHbw9lSdg/4nmIwTIjkO/D5FpfXagoOjyBUvs7lyommLjge3hfnEanodEd03fx67
+ mTsQossayLUA1dJUUkno6QgcGNxxTVzG0VF/LZZMH8fewPxsshRdDJMLBgILgbuZ8wdS6LqZgK
+ pxkMcdHYF6XxGoBJ5L9fMrw72ii5DK6hj2lAdx0ay8+aYsMQWKi1WmI8n5fXbt/TKoWDBO5CAm
+ 5akrw32mzRViw3ZiYqPd2QPgDe9RoBnbgeRPr5bdslySgO9i6U47wTTtclMwL8qdexs2VJnwQg
+ lYQP2ivRF9HSLgmhhLeaa5zIWTKK39wj2ub0fB/s8C3rwVyfH5UcqcfYI9ghhR5Mxo3oAHlfpp
+ XXtjhBV0KSCKN4IJDEZbQswx7NJGU1lyhVVjyJ7qVRwA9MUQltIgbNsizc5c+385M1Dzt98XjT
+ b+CywdwjAc1GyFbCoUhmuMUFrGgD0ExUl8VLHToCKFVWoMp5wZB7Kov3OAefmCqQuCK7adWBlx
+ LBNW0PVDyh5mnPGz4jucU6J+PE71TBXauaSyYnxjkQtrgNtC+hvL799XEM2IneTcNyPJWwOtB3
+ jwoeFAucHsAcQIz8BsOfOBwspHE0R2SPXAU0/rS1Ez1Clg3saALcSx/Nciy+4fzYdR06mo5cIg
+ FzIlQtgsZUXTH4+Xn/x+9y1SMLBRVkw4i+CvBWBTRVt169WYMudwga9KR8nI0R3vAYB76VxEpD
+ 1lWfhdaqW2/IkokUCmrZj7DmXgM8/m85YNzIcjcubd2/L6bMzUnrV8bXhnf9osGdE78AFoI9gp
+ TYsgc6enD1yEIruU1CF+vno95nCC9gLi/kVu2KxUt5xebcfwE/SW/OHLL45+4+Bfa7/I9i3i3c
+ 7jPXzPTROnjgdDcqbZ8/KD69elsPJlJn4wbhPoGbxSDrrrK0msbyOYI/J7GYlAnR1Q2KVJ4ps4
+ ClPDx0kIpWclSRPAMxj21OmozFisOKuTngBFTtW/5BNEhDKkEz+9LJKbW0MlISSXDJJ4GGtAqY
+ vuZUUiPxx/gDECzTjpj5cDT4SzYpuEu2EL/r6tI22l8saMUONA+knTdnIuwOodf7cjTBHAbCfc
+ ptKTT307vHU93skgsfiA7BnD1jYLbhN4XQ6FU1lmaj85telt7S+HdfKkb2oI9As8s/PF68R2xr
+ qOmIXwJ2Wid7YH+sx0TNKCncKoNAylZqoBlrii5EQhNsRllDcLzZUp520q3AZNYpcJl1ogG5bM
+ QTsrfzl8ZJ3EfUj87XQg9nUi+VxtHjHTLkF9lxku+i4hwok54Q4XlNPALCHMmesqs9IS5Fk30G
+ XNyRxx6Py+u27cnr6jNQOfKXCJnw7Guf+yF7UWuTFtyN7RPSrNZK0GOP4p+jetbgurEqeq2s6m
+ MvFUM+NSFoap0b2ZhMq4NvWu/L4pnG6QGrTMiFB4mNkf2sgPxzZY05BY//2+fPy48sX5WA84XZ
+ tCMti93DlBOV+DSGbIihytkjkIjq31UEFb1A+aKwxFJBTtgjfEGrNx5x4ahOoRFfAVMZlAnHmB
+ QyMBHAbhGECAVhA42DQRhYqKsg0C8AdxVHYgdiAjbQB+7WK+8UwHg3FyaeQC4oJfIbz8w9lMb/
+ mDgfnjC9y8gbuaKqp/rHZGs45QJiIFN2lcF4AS/xTW0dRTVp81IJQDcJFTShCtuQTi4sXBthPJ
+ w+AxKmqbkuZTqYESHxeUGo5BhPpSMA64mbT7/m8TKczPgd8vNRR8MDBLgG9aJW0VlNxRfX4dz1
+ XohZfWnxEwSTip+VFTXhrJcfyEW6XK/uGoZzeR20SvWNgPiVdSJzE83hW9K7354h2boLnaYmnb
+ r90/B1n38Q62Rkq5NQWwV+3wV4cccvZi3IIleBOU33ldWDvjUAk9B4K09B4nDuu8bi8ev2mnDw
+ 9NSV4WxX0tSP7ljLZhoTIVLvo3gumOfsK9vHEAdiTwjHg1+p55zCcbFWuoeuvHBonNuU5jza61
+ +IuGqdy8Ene+n4poaukbdQ5ygl78X77t/8ZlLvD23nz439Tzv6eePsP+7M51gcj+/GwfP/iRfm
+ P168J9hgIiOwxWAHIKShaI7LHRMd2HMDlCw7ATOMNVscCnAHW1Okr6qGLJCe3JhLADguAdg1WF
+ UnOwAlLQPUNJgdNL3VVzRKcDDiRG1LbneIQ0wRYcHBuiUA1sBTl0aOkyfJTvjkY0POe9gYr5BZ
+ 0HmlMniRkvuPapDm5IlRx6RiYqBFABW+Sl/FxT5ITkTy+AHjqlQvJJTpvWSUEoMZiQdXKkvYLN
+ FnzogCuHSAKbpi7haIqXVAtlEvSlG1erm+ua3EaHsPigHNIpbDyLKBvQPO4IQoRQWZ0pFysDOK
+ CWvMQapOIe6M2jFoM5WevpDfVUiwBWFHux4YlcfGkqkVSVy4grsBllqu1KG44+6QqtWB3Jmx8i
+ Qu41MsgnL7BviZYO1D5FLCP2FLUoSJWeSiJisI9C9grgJEoYDqblNls5sh+XF69eVOOT57arE9
+ 0D5ecqMe+ImevY36s7WET2bOK1kqc6l2PndsNd7UtXw/AZy7PndEUWVs+aUUOd7n4z/OWH5LzO
+ 5W2WmjzWs1Fg70uiB837dokdPkaED4UhYQ16pfe2zsStPclSj8J7D+W5P3CBOs3BX6eN25Ql6D
+ dIEG9Gu9MxuU7gz36zrLPKfDHZmjQyENeBocnUhlOoBJ8hwMWD0l3HUmVtrT4lROaETL6eI5Yu
+ q2GzQjQ9BpNcMnpaqQPsLOxVwCbsXkf+QC1tAstkkQuqAIm/9wYnAkoJBhtAJYmKaRrDAhK+qq
+ 9IcAKNEcb/QSsBWAx5UqIKbVIAF9cvK4PO+5ASYTKUxYi6V6Q/kDS1mDPhRLXz8VQAUTw3Czus
+ sfP7u4OC6yuIYcEOIPyWRdaI7N1IqSyQ3myAPBQUIY6A1xDTiHz8bze3pkBzEGTYLGBWodJYW6
+ u7F/vRLR2BUp4455SDcUcBeSV8hRSMRqSe9qxkAZLAR59dJSwlVeRjkMaJ/QTNOqVLtRpADQia
+ VTcKKsErThO1Ho3iMcT1TP57nvP+3YHjdPE9dotNEodRKOtqiVqHOwosIulJ44T9MhrYRcM6gb
+ jEosZFj/Qa6RxJo7sT0/VhxkNXzyOOmqjQwGYqPXZUq7uO7qf/Nk3I/fbyJa0qZe6WxCj3IbyS
+ zSpI4WX31EzIQpHChwlaPs98fW8r8JsCTJqPkR3CFF4nCo/HeyZfuUOlUVVPraC/GZxgBcrvKM
+ MYnysTdDWk9lSxnwSyHuCf+y5XSLim8L2Fx28Gw6ahJwABnqQFPuzafn+5Uv+g489RW09qVxYK
+ ei2g7gRML9C4Y1I9yLOGdvVJD+tDSdv3ET+oGQA9uxV63aFkdcJgCodWJOeV+C9fZyqa6pWyaC
+ SBBZSxGjHkeIfJsxoXOZGInacbG10KVk02EufrRwAFxxyq+4ORYBSJInFjrkHD0Q85+rq0i6Z6
+ twU3TknlJU8SqguyOlH2cKlwpMlSiPaJMMRFOBlsIVeHhF8jk2KyvJXgi3vkaI5ADYrffF+1zf
+ U2Kc94/sPH6qiSdGwQuLkPHCtRkmgFnDqysngOecX51U7zxyLm7rgvqAlHxZ7WipAZop77RGrn
+ Yh2KymG0trughwW9ApomAy3OiqUiXBdAQvunXYXDg5SJ+HaDlFQHY+vQKdLWdV5jGvuAI7Al8X
+ A0fY22KMBOROSVoRgvGcBHU9HklnSAlr1G9PRiJQN2xeiivbNm/Ls7Iy1JWoF2eQDgpwcCz6X7
+ S5Dmf3IHThg2AYE4Zyy2F1cH7fbze0RF+0ELz1cXXnW89hYrBlImbrpaYcr4FeuKfgesK90Ha+
+ rAdp0jKJ80S6kdBMUNjubdsFgkOT3aLn/mKNJetkYsP2hYP9njuwz4erI2AR7INiw1yuHuzvlh
+ 1evGd3PGKVBdmWwB9CDv2dXKmnfw9ki4oEOn3cfVah2gNz2tCG4u08nErb0o/d8U7TodH6Q2HM
+ BZxFaIyu8RpqWKlJAVQ5or3QvZhooa2nkwyXW7WYH4vTKcbu5bNFJETD5KLATPeEdA6JZA6ICQ
+ lkWUPdPLhkRLp4vCoRSQES1iyXN2ehb3xddRPCpOQsVoGHKpairmp2B7x8OxNPXSd1RAdxBEZj
+ VXBz3Yc1zWNHKARQLJt3V9VUZkDrTpOQCulqzKYp2LqVM0JSFOYV5GU3kb4/FhR5BzoMAHKSas
+ sUD4jH0YLX9rYrelYiM8oig6ng5f8+wTGI+uw6OCStfKthzwGjBzbGq2xm7mqlYLBSOmJJvA/Y
+ JVgA844nkuTQ580IzRhU0VFL8Nyyv3kKN88y9lrPOaJCHq9bCnwKo+xOsFVgdXddnRtduwNexG
+ 2vzROPVlI57Ie2UuGMQ2APo+3a3hA8OInqCPepuuLBHItlF9jonzemAfeaG5m3A3kZ+xsyWv8+
+ 1wG5fK6lfh91Dc51SS5Ig+6tH9l8UVv8JXnxfZE9uvtcrJwf7BPu3z8/KFKX6mMTg6BwNQ0fMh
+ Cy1FH0aK3F8oGHDYFSLe1Qk5I5LbGDhtoOIvMjdD9mMmTfM27N43GiLSHI0Y4bxRhKjyh9o1dc
+ WPYZMUndw4DgS10KiSDFgYzbQkaFojfDWBJnqUx6FCJK9eL3AnvSLpX3MMVjhk0hLx0qUDEoIC
+ VRRE1gM6BCJaBtUB5KToF48AQEI/FyOd0nfuIAKkTH4eapxWKmqMnw2DmfNgsCDzUXmc1a9Mo+
+ xkN0F4jZE9HW3bd0zi7VwjdbrcvHhvEoe5Ue0Kov13Mod2TYDOALK4PehxwdQsHiNtJByO7zur
+ KgPlWJ7g0op6t6kkQnB3JWwVcvP+gIv5onsSa040KBSKPtTXcS4Y1K2adD/VmCfXBTzLWN4R6m
+ LlehI5Sq4I2ZkL85eYO9dnTGhixO7aHmbxrkrlkyeK4AaiMliqd/XFlUI9Dm3EFt4Nxk6Bk1rY
+ n2Ax/IPfxtwd4+kK+hTfeectjAjQC5FnUO8yqenQtZtKhPZm7rlueuWdlRQov1qieBEbH6v3H9
+ 9k69P4/wJ8PqLTuF+sMfN65WnhwflxzdvyquTkzKJAoaUhpJRuLm0z12nc1XndMlh5QYPKGqSR
+ 7qirGjrmaTl1ncT7Kl5rzy3ooN2m3fjhiCsXK02y7YZdiMSceDi8kXqaqfAqs9QVnZa1K9doVs
+ WBLpdenGAVI6TBjnFpdQnlHWmoIN8Io7fcfWZZEpwqqXbYgFOXdfAfBDll5cXl6RA+FkN4LjOe
+ /v76tAFCabN1KajMe/7xcVFQfOQ5Azw/kzKLqU4wt+xGCDSplIJiwDoJkzM8bD89utvNSfArlS
+ Qjcap0YVnsrpdcQfCWolhqn9hm3BpawI1c7+5uZIBHXY9lqOqe5eUVWpHabDHaHA0mZ2AFupuh
+ yZdvCq6tRnaBvvOQEzy0LjfKK5VstZ3lhp9UUbfDuwhOBiQkkMgROmr++TSPqSvTmVsXTieGOz
+ P5PjqnsRJWnZArc/IudQE5Nv8fOiQCpYAWVBc5m54rUn1WPKKPyRBLQKk48EJuABxVwmbpiHgD
+ wD2+A6n1bTYVD+F2HIQ7J24Vt2TdmSVazc4k8qpNE7sE0z3OGis18HnvrGQ1XxKQwllwfzaNM4
+ XIe2f4MUPgT2blrx5U86OnpQxwRqzVZOdETW7OYGndrUbs/BdEQs5eyRiyQW65N3e8YxwAPYoP
+ EHyJRFbda7sjK7Eyzmy96ANEBCgELEyIhUwKZIUf4y/k0+0Hh+8N5KT4WkJZtaF45a08klMzPj
+ ckIriAIYpm9weaSHgZhuUkbLJSrxZZIfMQijo082Fzm9gLyzAZdLY7f7gL58FDteMnvOLeTk5l
+ TTvHH1mQQtdX5edsZJ8AH/41QPIaSOBz24KK179iIRjX/Hhwwc0ueU+DPLXi4tL9ZvtFVFJtKv
+ Q/Yqcsy5SVzf0tB+M7H+Oz8VzDpWD5LhURlIqeeGyeRvlrjgXUHDNTkiqHqlx1DGskzZi1ATsu
+ 0lvl1CvCeTRvbvQDiMUiPaEoXa4ADk/8K3AnqotgL2rZ2U0FoMwWVvj3iI52x9JZ//07IzJ9OS
+ n9Dlbv3fNUtD5qhcx9dGsBhtRfh7nOtPVJfBqMOAx4BOQk+hUQBUVHXb1LhtwsSI33vwHNoU/g
+ +VE0IcAg0EG1Dadqo1BUN6j2hyEhrGENolWLi5dUnebxhHAb+TGvfs1yIcHdIKWz38E+80V5qN
+ gP+iVFycn5T+w1Tw44A2lvSk4OsvMmKCF30q8pn14Te1HeAAAIABJREFUDHK5L0I+CBBqb7Jkk
+ kwesjkzQA/eN77hdHqUmVfNuDu1VCez+cEkSpUo6/jM0AaRQsLrnRMGNARA2lYKyjMs5f+exKZ
+ 3AklC0yzMFAnPh777SADjWJYMUp6JLkVOXIaCSLcoc504H1SnUtGAXQDtBADgAG3x3FmswKMD3
+ NHNCMoOgD+VLVDbQNmBoik0F4cDJRK33jKPuCuSKkZR3Kq2kTw/Py/L6xuqpAC4eA8W9DD5i+R
+ uoeUuruTVxUWlunC/VjA5IwXRceT4LFAV8TNYIQT1jnY9aVqi+gFcQ271WbyrhGl1quH7d4ql5
+ GvStrJGc04gMlHu2w2CgdFzPPGtaGFYv8XZ/zFgDxWUxjUTq0xgKgga9OGiOub9JNi/+66cnT0
+ XjVMFBptjWfsTMoc0G6y0m4GcdF2jqExQI8GFKstxsUQNusaBgZXmVxKlsKSgVbbVTkgsZ8Ohc
+ 7clBNRdsNvGnAPYMy+DnX4L9vi9lVKmwrjRzRvoRfOE2mmkmKZz2x1Oi17tTqYuBs0TvirYPxS
+ Y1wz/Q0/8Fz7+MbCHw+Wr09Py07t35enenlQgACkoJNx8A5EvK2khEXRhEm4ehgyscVlUZQuDb
+ NlIAdEeAAVVmhTkwJN8MWdOlUMy9GvbC1g2x8Kcpn1h7I+pLvGuQ4Us2v/Tt8QFSQtEpO6NmkQ
+ uPVzsPZ/IVpw7WgDK6heLFt+HDdQ7B774uONcI/WsskAniRnBU8MuDT2ODakk1DRYFKMxZ9LWW
+ uskoKe7O/QgAjSS27cdgt4PipsBzxENSQDU9AGqlbDKFXAHFe+dy2v56vTWpIGw6IorV5FbPj8
+ eI+3Dia6ojzURI5gfInejBi5oc4jFgoobVumiClaUERdw7ITs6U4O29EuJacGbFSPJrEbEzfRY
+ 5IjamhoRyHL5AbsveNL843YazDY42qnpitYfL41Zx+ZLMB+CBM0fj57azIyX3LMw+xvMIFdwvf
+ l2QtE9mjuY7rR16RNsAbMB17h2mSsPqN2A9rLdAZtNZkZlYqfqiS3dkhxHGXWzYIEzSFIoVEPo
+ wUGqi78y6Il8Lfk1MWTTKK60huvpdQ2CVXwPr5X4fFD93APEtdLfxJ9Ll2FbWC/C9y3YfQPBXu
+ pQe74ijb/rgzLvxD4A0Lk1derMh0Myuvnz8pPb9+WE4A9JgzUGB68iNIA9OLu1ZqQ1ISTJty22
+ zeEw93d4zERsBxwd1DBXgZXaW5C21wPAGFj9Og6S0bstdWeIzsMYLY1lBEVot9wzVmcqGJZI7p
+ W43BypVC5kONWMrdTAfm9rABhghkRKkARrRJ9fkoKd9IxVZ0qKZvrIVoEFAeqZUfcpwBMrxBpI
+ wK20oa2CFDlgKJhxLwo+4eHZba7Q9AjZ28AXixcVDUZ8W9oe7gzm1LHT/rK1wkAAFMu3BfsDq7
+ PL7gw4/yubq55H7gzoKWFagtwvvCsx3FV/KKGKNCID0d9d9wCLz8vv/32qxqxp8E4G53AsE07I
+ tJvCcMtzPBwEHcOl0oXaIWGywSPnr5NouOY8aHRHTKCeV51/jfe28eJFLsJL2qVxokqx0li3n+
+ txUr6esGou8ZtACJAdTtQChOGcISdlMFIhVaKpvM8KdNYNDgclu++/6GcvXiuFp0OlrqEebdTr
+ ZfPUbC5nG4RZAxvVRODpOyk7RllBTr+LIC3YoagL6BVYCafGUbpAHr/w+MIIpDHw8/8DmVRNSE
+ D8KtBEU6qemL5+fz0tEcHO5AFwFF+PlOCxbA15m2quuZj2NhdqvqsPxbsA1JbJ5mIfzPB8gejP
+ AezMzW3liRFULPRoLwz2B/tzAT2AJz1knTOqA8KR9IytCTE4A03nE+TKkzoh5P8Y9coz3a+xm0
+ HydeiWAtWuNSLi1sk0FuqWCMXLxaJDKQXV1SBr/jMRMpHizXz9gBX+rHXhJRir6rFpsbe1X4au
+ Y1CyH4/6C3rY0ZWJ5sIFZexYCo7Cnuj0ObYTpvYJQF4Edlz271CsvOqXJxD6ihVDfz2Ubewf3R
+ Ydvd2eQ6SOWoRBfADkBFt4b3xerYvtA5fAIPq256Kqyyx/PDre1vHisLCjgrHBUAlgZ0ORulAF
+ Q37/v4+dzis0oWR2vVlef/rr+xri8QrbSHQO5e0DgqpQt10PAPuKXYV1aHSlsRJZNYq0q6tgiD
+ d9thYrBP1EzgMysI3qYyqB47zJFEL8T5b6iEL5U4GqvdIYtdFctlt1sAsjUs0zqR+MbD6OyJ6W
+ BmnepwVpqY9SXuw9gGJ7mH5/vsfyvMXL5RvMu/tmenjqlgprfvovVOj4PrWGwVN3FlXeaLBHHO
+ C9IyNCauHj3sJGOCZd2JEnudpLuGzgB5kEtY7As4RBliiZavrp03MBn1RUzV6T0K4Lmqhqxj6V
+ UJe96BD78rfPwCR23j6CPa5YA+APS41wP67F2eM7A/RmBpRPaoakZlH9NkDDTMUVwytvQE3Nzi
+ TQdGyemySJkDUXc3KBPayGFZmDbsErZOupjLYd8kl2Z8qkvA2lN818RT5yXZBIZoiP1bWUi6pK
+ lUCAxcYWSyTy0fUnPcGLzkcSQqY4izWDKihdCJgAqGTy1R/UIuOHMDmboR0jb15kEOgiyRaCmJ
+ zD/uC6ysuQvhbCr+w85nszMrO7g6vEywO4A6KCc/nYqfgClvy7jfw29fHxjmo8E2UWZxDLy8vu
+ OhQs88FUolLuGTic+Jx7Kzkuqnm5jnW4cGBvfflfnn+4dfyj//6udzAIK4H7b6qKUGTLW60u0F
+ QkMheTIX2u6DT4lnPRHYbnTEZszm76e4JXt91Hrz3loK3+Tm6Y7B59qa6SlQVtUCV3qs9ayvd9
+ gDYVxuADuzZvMSnShoH9t/TiegRjv0lF2/KXsGDU6Ko3soA+7PnL+TTtHVs0RiWEWO+cBcRywH
+ tmDjHolIxRjJKN1hWN07z8K2HD/l62z2QjhmCphFoa3fsKN/nC6Af9kQdiqIRzZr3UEJW0XpcZ
+ JmwzQ7diWHrfux74z0bL7u2fanFEeBv5Sg+Bvhb0f0j2P8OsN+BL87L50zQHrKHqwBsgW0pKBq
+ UUrkzD5KW4N4T2Qfw83vAPlbHuImyQBBvD629OFVNUAIqJ4qrX5EAZQCVRheIQGSWpsiq8yhJh
+ JbqTK4hqE60MoZJUMgj2ZTbJfwGw5hn5ZhR9WALqiIiTVwmlf3e2pFognCxsaKHXkF0anQTbFe
+ M0taAtIEqYaHjnKOBuPvJvv/wnhWXs9m07B0cUK0E8yy8J9Q0WpB0Puzs5UgLYAY6ReoImbjhG
+ kAKiGulxQw7A/nioMiKqhHviEh9FfH4SOTCwwWPt313Dw4OaNGLegvMrV9/+bn8v//3P8vVhfT
+ 6eD2uIUCfXkBsMAFKTVwywARKJETnVQZp7n4T7K2Z3JjcXTPxLPJxzcy6oMAClKCS3YlpchgEF
+ LBIVirHnWOzc2zeKzYdrZ1CxlVL6XCMBuzp6aMACNYa2hSSTOPCib/TVsFBymA8Kt9//315dvb
+ CDqoSKGQM0U7cYzvcO9Oh4fQbJY26QqWhCqTQXcReATgySSdTlZxFVC5QR0SfVqJ9LEr8uxYXB
+ mkIsNKUyHx8+lSI/rFijtG8jikFXdNAvOYjDOwbzUla5VDL1fvnbVe6jwH/11bjPPBeevgO3v6
+ /A42DyxuwR2RPE7TVoixhAAZ5rqWUigLE82Jb2oJ93bqSB1VRlApiVAAlDbysXvE6AASkjIwyK
+ wh1Zfsp9mCyqO9cgc3RMhE1KdLVqKvS1PZX5fds3r2CfYD8W+hKaL8enldjz5voMEVYjHDpUOk
+ u91hE6i7FDVqs5YfChglKF/NE54/IHaCM9wYoM7cwR4HSBcH8w/sPBP7jk5Ny8vSEER0UM5iUv
+ /zyq7tgicbBNUTRlMzUpIXHqjZhG8ceQR0cMjlhevKjQTm08DfMIeA648IwqodHDvT+V1fl/fv
+ 3BHtw9KFAcG0Q2U9GUu+Agv3ll5/Lf/7v/1Muzt8rOYjkOj3oVWnMeA05BHbyUnCASBf0FHvvm
+ iv/pMg+bQRNE0hpVAX4wkXq+dXsnKZ1bqkdgKbvUcDevRHwOtFU3ddDYB+ATxIxvL0CAdgbw0D
+ KeZy+TOnYrpHBinak4+mkfP/9X8rTp8+UY6r5AOnb+XniFeaINyKIPFXSyc7xJonbAK1oFu1sK
+ xdPubR5+prnEk+vfxY5GOwlr5bEenM33c21Svu41WAsDLgAeO6n0Ug4eK1jKbrqbA4S0XdRvcF
+ +a6e3jb/btP1jZJ8r9AmcPcD+u5fPy1/fviuH0zHNuxTZr8sgldRe9RF9UpFTV/iue4wivq5il
+ koKqk5M66DbFeSWNAdD1CfHR0UcZu84qWVTzImFJuTeXlZA1mznJ+w4e+mukyinERY4YtA5KBJ
+ CRA/fGMoUsT0V546fMTlZGGTelos0VQluSEGjMFNQUKF4kcE5QIOPyDnJRAHmitJDRLysPI0YB
+ YsLaZlLRtTnHz4wgXx6elqOT46ZD8H74vqoYlVKIEkxsTCqUpnGbDh3O2aSSiKVo602k8ZIvF5
+ e1C5fSrYNSBXhs+KYeA9IMhHBQ1cfe17swJ48eUJNOBYMfP3yz/8qf/+//1nOz9/LMAsLZzxau
+ NJJjCIVpNQcuPEE41gb89Z2LQhrMpMi9bBwXRVm6A4CrdU54uhV2IOruUBgYvdTVR9rECzczUr
+ 1D6m29c8RTihtrBqJLc4+O9aAvVRBGnPAZVwvKXIkVNA4hnXIhHUptBqxx9Jsd5dgf3Jy0oB9h
+ DUGaVSxRtFi3r3mmvx3pZQMiDXYMW/PyFzzQmCvPENAu6NgBPZU27gHReYy/8bdACTWTeV73t8
+ BXO1GZ0pGeYOu/qZq/p2g9epcaZvU0VS+viZuDeN3ql06yH/k7O/bfnwK2I8A9uLsn8xmFF7S1
+ RCNJnDDPMFSjXkX2HdbXm0BZRSG6FneK0xWMaoUmIHu4QQioKtYI0lab5XUPNyAp+d2VTSxWCB
+ nT+28ErNUO8fGAJOYUWdX5EWw9/aWEamjXUbLlk0CHOo21pMLuxJM3jT84GeztzukkPgidWGwR
+ 1QH7l6tBVWLgMie0T99Zha0KADVc/DksOzt7ZPGwQRka0NX2MpYSzTOZDIiqNOymKeOyBByTNg
+ yaHuO80ISlqBKV0oZoTHixqLp+gAll+fl4hLFVp0NBbfx41HZ39vnLuDy4pyg/fM//l5++fvP5
+ eoaC8hCTUMM9visBFQ3kSc2YwdFVYzGUjTWlGtaPUVw85adgOFEuuXildum6kuZ9WqzoFoDBQ4
+ 1KVvb6q3laGrQ2AZ7bcT14L1g7x1dwD1gr2gVOySNB1BnCgxEu0ynoMuGBRYEUT2BogPYPzk+t
+ t0HFMgu/ksFqhobixKxpUHAvaNKGgOyugAkcu8q2sWxa8wz6VqTsNLEYxyoAK9bDBLp1wXCttd
+ RQ2UXXfn6WhgV/l7Ujpj4fDcoNUnv7MrsdpMneBGr3M99aFaDifYJj5H9J0b2pHFGw/LuxWn56
+ e278gQJQisXAPbknBloS16I0nuBfZewqVGQkJuDnUkql3HjDol7VaSPiYiIHS3a1KwEW17Poq5
+ BlbL+KGryFw/fRN9KkMpRMV7xAJeU23PocEsoEMTEvmEBmM6dDUvcWo0Tij1c1YpPHZS4T1Gew
+ LpksQlWDwHslytZDtstk7kBD27ZFqgRCZAHP9NfHwVAyxVtE7ADgdySjdQRHdo+AVEkzgnJ2RS
+ MjcboVCUeHslQnkdjdTCGRwtoMiaEb9AYlv73qSJOL9sIIAC62F3gmiEaxf1go+zhSK32+oNyf
+ YkGLqX8/PPfy6//+EeZL64F9pbmKhmvcxHoye8HBWE4N6hxGO17mw+wZ49hR+OJ7qPmYmIxOwX
+ eHN5weyG4MY3VT7hGXaOSxnSN91CLd4C9dTqt46n2ergd2ddOVEIn5ZUsq8TPFCzwmoFGk2oI4
+ 2U6G6tSmRW0yqEcHB2V73/4Szk8fFLBvurP7aaZJkAt2FeqxjtJzbMues9iwEg+PLpVMwygwsU
+ b7HE/ERBUSjaRv//GAIfduLRwBNgpSqjnEH7fzpSmZ5BjoLaeNQbw4BGcN//XqHBsUdwsAm0FM
+ MbMfV8JCDbA/vVPf6uvqFHnfXr4ew/9aQ8kSrj17D+Dzr6N7Jvta3sTQONQevnmbTne3SkosmI
+ SEMBp3psRDm46JJiUYYriSPJHDUsUqddCLOvxAy4sovEMxHHo0pj/zFUS0KFlt3QNXa1SfITj0
+ KHPzocpKFICE9bD89qkOvI879IVEbqMnslmmJc0Xip4DEBHPtuJUYK9JWz8XE7wsSmLqwlBfdA
+ jxhF9/G5WC0WwiOLDq2IHAP4ckj0cFwlSHAetH0MDKVGsDl2YNojKQ8vI6GxBn3qACOwXMF9Qw
+ IXn7KFGYtDn4nN5flFWLH5a8PzoytjXQtDtHhaqQ3DPACQXSUPQyRHOmAMWcEFn8c+f/6v89ts
+ vbHCCf9xBGczTbUyJSu1ucJ4y3FImW6CubmP6px6o+TspDz+nKkts8BZL7twv6uvN+3SFVU7q0
+ qEUfRCsxLEmyARPZ6kgfqfSfgHfWtyTPrOc1K72rACohjzYAbEAr/Zt6LNpCcYXEseMkodDUnQ
+ //vhjOTg4NMWT6+HkaqwEIrckSnZWB5RRVurQ1bBNsRQTqw4OVJOCZGzoVYN7BfztaB+d47ocW
+ 8C+W3wp1Jf6pso4EQJ1YM/HpJywriZRfrC+k1t6/6JLqpXLDvnpXaDPfvsriVxds3vB/tMg+/O
+ fdS/Yf/4hv94r7wN7XFJOrnWZEuzP2KXqZHenTG2zS7Ai9ZDBjsGLyMXysmhreeNcBWmHQtxI2
+ SSgHEPRcGyCZaSkwaIiE3+xAkTRPzeD/X4ZjWdO7mkVEN0Sx0lt4VmAxOYYojwczql6NEVOjiY
+ xYCmPmyjxSzoGPDgkiE6OYnEB2CIyxblyV2A3zTTxABDSUdIWDDQXY9tEieGo6HHNAJUXaANIS
+ keGbtHL4ymkAtSpg+eM64adBBthuNk3rAlwPUgNoTLWgIC/gbbBr4dHhwTZi/PzMr+8Losb7Aq
+ wGMEeGZJTrbQAKuQZeO0WN7Kr6Jeys7tbhvQ/UqIY58nruVgR7M8v3vOYrC2w9TToCO1K1I0sn
+ bqwEAsgQKV1k5djqvoV6e/MmzB4cFEXdd4o7JFktgVlRn3Iw7jojYn2KLmyGEMhJPWthoLTt8r
+ pCOAjxcz7t9/rz1bMaJlyD1q6wKodJ5Q4HL98aM0k+M6u+gmjiI33czAsZ2dnBvt9jXvTkdq4d
+ FYDBHUvLEr0JJhyEWGtUrUpmWk7JlUbZQwj8oF9rbyjbmWTHY9vjp5gbxqIY1CcvZK9AfmocPA
+ 3VNHHUx6KoA7sA+aRi/IG8Doi56EbwsjfGzb9EEGrdy65cYEF3RCNlebnChttZP/1kPPuI/13B
+ nuAzwQ0zvPT8tPrt+UpSvYJ9mpQIm8nACJakgF80dzakxuR+1anmijeMUjopWMPjkTWALVEy7h
+ zBFx3niHN4yghMddgiMIstThkBM8GIzI0S+tCRruRAGbr4MYYiNLZXJtgjzaHPi84EtpFkxJCR
+ Lx1YvTLDH40OB9LNgG8AQ4sLrwWQ0V1jO6vRbdgIcT7hNsPcIB6oa0x/M3tghnXSKoXLOsDcO7
+ sqA/uh/MLQhNzBUu1OsSCdv7bbwR1tClM3cB4NCz7Bwflw2/vy/tfflGeYI4WhlLD8BouF/TZw
+ eIArxtw7Nfm+3FNIfvk+Tk/gv68ACtQQb/88+fy4f17etzTrnkNRZWqbunuaGmudjqoCF4W+hS
+ 52Uhkebk2oX2izW4rsrnIw2vJYE/O3ZQLF12Cu3IxbGaCMUHaRnkDLshwLNV2rLph1gKyRPve6
+ bZJzwogUQIZnuQLL2kixzZkrhzfiaCVxJ/tTPmdYE+6Z1RevHhefvjhh7K3vyeFGsHeEXoD9ji
+ P0IYBe3HmnjPm9zlvmroTzqfaG1cAjbfBGOX8a/rmtvw9qZ7UApiqUf5MFbKijQzyWQy8g5ddg
+ o/9rcG+0j2PYP/xteyByB6UwWQ4KG+enZS/AuwP9gsaL4hbF12ByF4Jw14ZT2bi4zNIPNAIROo
+ jUhUBLIgCkFtNQQMy4q0SRIyIWHSC54WbT+m5ozL2RlRDarbBW8mLRaZcUJVc8ol0uJzflLGrF
+ gUA2tJjQNPu2AZgiLiSZ6BEkG0FJbUUV4/uWyg0QjGPnDVlNqbJzgQoLYUF7FhQaH8QEzWfT6w
+ MmDS1WRm6euH32pzbQCbOtzA63N3b44X85y+/EugD6OH6P/z2W7mEPn5nVqaTMT87X7e7U37++
+ 8/l/T9/kSQQgIyetY6I8J7j6ZTXTI6VWkABFLius8mszHZ2qNRYrFZlMkWyHgnlZfn1n/8ov/7
+ 2q/h6cvZS2khtp2hXiVotnNwRak8n6a1BLZ2+kn9xWlbjwPULXEiR92nsDxiROwkcUA9/zdyN/
+ evTn3dunb3APhW0LsLyjMl+4z6w75Ky2tkyerZckR72Pl+poEQ9gsZhi0f2L+7RCO3Vq9flL3/
+ 5ngVzuc8K4CNp7MYj94WJeittlIrYcPaudq02IvKoEpA3YG8p5YNgbysFnhspIS1oNWHsfED9G
+ +d8wL6N7BN9dzTOl0b2lYK/I8p/jOy3of8BsAeojIe98vLkCcH+7OhQYO/KR9Ip2J5b8QKwZ9M
+ NTE5LqzgIsKX0KOVjLrTglhfA7ggYgbe0vJ2REiJ7VhY2CV7GZJiwVPWIHpAXzQ0VJLAfQGSMK
+ FuRqKI4Sv7Yc1bNQij7pEzPlgGNq2XnUS9aSUllMC7OJLDaVM3Aw8vHcZITGRbBtGQAJaKm5Hm
+ ciyUA3jbP5ORNj/AxN+oOHy9KSS0eEWEDUD+8P9d5jfH+siUGT//bP38hnQJgZqvCtRqh4Hz++
+ fPfy/tffmPUj/sCzj1gL7dPWTaTIgJgx3UUaqLxqKCYKgqn/liLJJQ/v/7yazk//6D8AIritEW
+ ro015Q9xn695d6CTrdlfJgs1yYrbjyAV6SQ4qWd9nYQ/tEazA4d2t90X3R0l3VzKnWYkN5m6oB
+ Epk34B9Cqwa2vdOsI/axGZ/XFgsX6ViaQKLABVsiZYTtQI1DsbRih7xPe6kvvvuu/L23VsWz4m
+ a0cJ+m8ZR8RPnUQISUuad0Zi09N0OI/p6gX3H06v6VXk0zcWocwTEkjvr8exOEgBBRikeXgt5S
+ +VoJ8aW4hSI8li0sugKpVjX20Tjn0PjVNKmCnTuV+r0HmmcNnxxVUqboG04e5gdPTvcL397/aa
+ 8eHpMzh7zhCUzayV4FBkvyaEDkLS9tJeM5ZO84YxyYrSkZBCeRjkkeW1opCP1ExBHZK/kroo6C
+ GyI5BA9u1MUqAMAKEAHnu+0JQDQwTbW0SVAl3YEl1cspnIe7v9j772/7DyM5NC+OYe5kzABOQM
+ ESSWuwj4/P+vZa5/dv9nHx2v/sOvdo5VkSSQYQYTBABPv3Bzfqaru794ZgqK40koWnoYHxGDCD
+ V+o7q6urmamBbDAjahtWr5RCTK6fIFfwweCRGSmNIkr6me5HNwthENdAvkdqRAYifH4BL8seA1
+ VTgQCgDEycEkpoeDISK3jQzl4nkKpxN4BXwfoIprIZQmy4PxhWAZTNUzjoplawFwEfh+BipOuR
+ 9Y962jqGb+H5nFIDX1JOGcTSIPBcA3qmhkrIgQWNHl57Km2gQ2CpICnp6cMrFQYgSpR1JaNMYF
+ Y8wWgmSID5+lkT2YB2jHIFvy8QE90QsLZA4AUdd1KY5HWJZSON1eZEPjym2j+0OoDgcwHL0T9+
+ T5h/zw44Hj+5b8jGw1gDe93KVx07SKzV0Iiqwr0XdC3QKWFv3H0AISgdW7eumW7uzscXItrJ7B
+ RyZR6XqJxPHtPbIMF9ryP/HtysnQgR5DxgJRQOT63okxf99/F7D7oKFE5bkUiKY0WFPnQ1DnOP
+ r7GYBWv5wLYMwFcWkLu9/S34ewvinHexNMv57R/Afs4Gt/UoAVQ2NxalZLd3d21yxsbVi6IOlG
+ DUdI6cq3TieVyRcthOMrthdV8VKbFC8Mll3FTRICPzB5uXPiedMmSQ7LLn0gbvYb1C3Q0E4UUP
+ umkcwB8gz5liciq+Rw+oIXP8f3RABOi2K6kCdrIYKk0ySv7CsDH57gRuf8VzVr/eYBh3icN+d4
+ zGWbTTFSR8bqEMoCZy8tdKQBQE4+dsuFQ26sQUPA89O7xwKvHUfaEn8c0K240PB+ybw1ziQYCD
+ wywnyPIjSQhRYYJNOb+0+nUBp0uJ3ShBsL74Xn0bVah1CHYu888XSxZwaASyFiphPfn/ZDU1Jd
+ cpO2sfcalJ5B0Dr0HwATUaTpl9BhkQ0Pfdwq7iiMcLkMmKeCJQSqBWwL2aI6Hw2QyjKXg6WygB
+ w8tjmFS4v72UcIgDoGG4uIU0jkuy/RtWAxOv0tmzwxWcmK9Rk9eWAV6P8IptEJBFSCCLzLl8VS
+ 0X7lasTt379jW1jYTB1JatBL2bNuDnQaNHLjx2njqFEypkEk484Uah8ovN/JjZu/DgkHnJBn8k
+ kmZMnlJjxcZfpiohf2wZ/bh4cPdBN6oZWaPc4fkwh8jPPRDX+/PlwDyt2zQfhXsndq6yFpEDvC
+ XzH45s3fSNhFFOOkRnhk2s3qhYLd2d+z65qaVecGC+lAJDoUGvGyQSaNBi+GooDHAt3N0H1lcs
+ npQgE2wCYKFfCsWGUuhQKpHPn8qbZmNwBdc6hiV7Gb9EfznaWnlJbuy4sFwSNUJ5ItsfsJ1kct
+ RVLqTh+aYviZQJ2hWMmMVEOHGA+jjcQBSDEAeAEJVAiknZKgRlACIUKzkoWxxgzGAOqWEvuoP3
+ 6OeH8Zn6pIl3jzKCPPuXimlC+cXfAANPHsRfjTeaCSF4k250OyzOYldtoORhtGyUda7EmYwTKS
+ YIRnEcdJ7Lsqi2M8FAxv34UpFBHVBl12QAAAgAElEQVQJqjY+F2YNcnpMNG17XU39QqrZGw1pW
+ c0Mk5JJNLLRlB6z4uCWo6Riky9QsiQc2718H6qGqiTGeBPY0zPfAT0ybzFCfn140zYJnq6DJwV
+ JvyCmEj5oJ3M/XlseZwI7vo7GIT3Ie0RXKkGUW9t8Tac3n3Xe8lYookJEpQX//xGrmkqtavfu3
+ ePiEgR7vB8EyGTaNajQaIZ6lbwM9sriYymI0z8hJnArDn7f7yOB+BLP7xYi0XOKqlxZe9A/bks
+ sUs03US0tIvHArcCHA7jI7NHT81UoTk/pnvlDgP03ZfW8Lnbv3E2gzXu4XxMXvs2XFw95gRH5N
+ g/yR/7Z8BDQZFtw21wY7g55mAUt5bJ2Y3vTbmxdsnqxQNkbZHXkodmM8c99IQPAIZQlWu8n0CJ
+ 370oGycEWS8Bxwyuz0EJyZhfeEGXmxoXNcnEMN0mAMbI0Olc6DQFqAkoQ0CeUGvbgFQ+1EIBFQ
+ 0zy85aPD77HgMC9sFLuwH4ZAW001uOoiSh+E0GAqokZgGvObJp0EcC+XEl8ZELyGFk6F35Ay47
+ KgoodHGHw4lI1RTVEr/hMVpOvUAfBMyiVYVaIqdUEjAHqvpkqmsh4IChp4DSppmdY03Js1QZ9K
+ XgIkj5N2zltM5gha5d6KIaaoorRLACAHq8Fjw0AzRYUkHBcwP3jeRlkh0Mei1QGvLV6L1wawvc
+ J22g/77E1bInacfbch/QktWSG6nSDiGJfh4jf45YrfQRHL2WOSyq5gSyxR3OQWdBGIa8dYq/vB
+ JJRp534gMn/9Ph6EmWssRFtachI0tjFTmbPLNgfAdiXy+hn6doeT6HGkRz2/v0Htr62ztkFXCv
+ UwJNbcT4+Asryc9O5TNk9De+WJmbjdSwaqh4MPDHg/ZcMVPn7eYMiJ6kuvKoOuieVkh1K9A6SI
+ SvSOEHRIFBrkIqqVB21hL78qvRSyikmhXxjflZ5jSxssfX9pV7QctD4usx+5/YC7C92dP+1aHt
+ RYhn//l2iz7/2OX//32M+a6kZbkpt1IGvOb07PAPGvwqZtF25tG43tzetVSpxaUGAPSJ3wiliu
+ jJfkn8GwA1r7zyr5ZCUl/YsFX3IJuyGI4OmzTHcFAn2ARbuW5L4zbuGHo1Wn5QFyKNRqk1Q3lw
+ EdQEqxy0XwEHjA9UGJY7OM8offrqw2+WqQUiDPACgYcqlDWlfSAG7XjjPAhBHbJCK7y/yxsMvx
+ lYoVt2JCkjKHtJGvs1Jk7oCNPwOAB0AymGcUHdkEUjK8uzxheVoxuLYURXCBrPmAWIZOWntqVw
+ zIQ3E76khK7AHcAHghr0+HwMcP85RLF3h6/QeBY4ZhoRoBOdrBnOFgt4LG+FD/zOyUWjouWIPA
+ 0SiRej+6Z7uapZoy5QydN9VQImkry70SoyN8VhU4xkq5bWJfp7hJOB4UeWFJDOhEcTnQaadpr/
+ SjNcoKjJQUN1BP/G/16LE8yAT97GCtABfWbCSAATPMAuTCklzIuixCOzL5O7RWxpO4ftv1mq17
+ P6Dh7bWWnXTNPVqFsAqlc+y3j7AOnT20tHHAJboI91fXtUlE68LeihZ+JMMQoWMctEMj6Cm5w6
+ 7BY5Ei1pl8NM6QjZ8Q1sfU81aaaJA6eIKVvQO/MJpbVgRmywQ50KdJc09dwsvfZwD+6Xf+zosT
+ P0F7OPQKAdKzWKwwadQPV8S3z2nV/r2atNubW/aeg3yS9E4uHPkISN5Xa5QtDwatNjD6ppxcqa
+ 4RiQlUPbujVoOvFL7rHVxGEYJoPMrXkBBDlnRnu6UDjLDsWiW0VjadS4GcV6d/DXghfQS6BE0J
+ B3skZWjAerZvHTvWg6BTBtZNXXiXorHshFo5+XECbCXvwt15VNl9nxMb/bS/MpbDDTG4lo8VVJ
+ 4/OkYr0HHI25cHEN6iYMK4/Wv3gWCBfh6Ugfg9Eln4fVpyAXBeTR0jTsoNQa9qc4dRvY5cAY6S
+ bJQ/MfXQO97KJZE0wBYhg7w5OZxbEfoKSC7jz6KAKlQrPD1MKOnVXKfE700HgM4MJhmZFcdNAT
+ jq9xFoczkEBznNcKvXcZ0eG66dLJXgR6CXn+AXtB76h/EzoOoS90mwTeHJcoe37zGoTz+kRkZ1
+ VQwrOPWMjl1hmneUvmfUA8hPEioE6dRvgL2Drqo1Eol0Ti4B5jZz5XZb6yv24OH79jqSsuHseS
+ GqYpGlRn+kAFPePkUrcAJjGE9HKsFl4NQfO7BIugZvn7X0AcNmEzEuhIuMV2LQJNM72rD+B8U7
+ JHYLJm4LYO9qEavIhyylsFe4eJPwNn/+Wb2yFClm4VBUxTBirP6PxYkr9YqdnNrw3ZaLSvCpjW
+ 4XXLyACQ0oIpWLFc4SZvJYJI2p21E7lLJhm5Iw1wGmWxBoteOLuoANNeayc7AeXrcieKoJ6Rfq
+ K93J0cANQaBJnDNDNMx6LJdIw8+WwA+pYoFXji0CHbTM1x0yJj5+AB97opVNh5HY8EfY3mLgAl
+ gDNVPZM3Mc3joZPXIKidpOkqbjkxXzTIPAu6DTh05OW+3k2XQUC8jXEBJmwHM/WbEa2Xvgtp4z
+ QzQRx5VNGkuZPboAcgbiNulYK/sxxKKIxwjBEhk6dqwJQDGY8FyGgFDVYQosEqpysAOqWu7c0a
+ FExeReHNPA3DS5BMIfXEHvo8KbDz0wOBBJZrXlKQywfDF195gd2bD9x8I8Ejp4f0JHnmsE5qL1
+ cOCjtHPqIIA0ONYBahpchdBVC6spIQ4kLWAewKjZ5IB7MyQo1EZ8kUKClx7n06z/0POnhYTDva
+ mNZBbly7Zo0ePbKWx4sok7QmOjwD7XGTWoXJLqBVZIXCpt+vyWXEky0NclRPyzGigJjLMxcYr0
+ ZTxZzmb9yomMuw/ENjrUOI4h72xKkB2QBKmhmSy02c6KppfW6JyviG7/zdR4/w5gz2NieYB9p5
+ l+UGVG8LM6sW8XV1v2bWNdatCJiaPADYE0dyCnw3UKJVq3dJZaO3dvIse6ppk5LITXHgwgUKTj
+ pyqgISJu5uixfAVziODD61vlQnCGkCAJtWK/Nz1mpGNDwbKWBlE5rK7jYqAwz2+1g7Z9sjpHlk
+ DQCGh5iGrghQaqVj5h61QWhBObt1RB19LuU0B+XYvb8OvPlw9VDKrrI7Xpd27oZwImadKaHywG
+ Yw/vtQlbkSpmKT719SxKiNWJp7JksZxMzLS2+78CBBnY53VyIRVCW4ZVhIZAScXlNC7ZkTAx8G
+ i7j+8ffxY4v3Uyg0OBXUHA3t9cEC+HluX4OWDK0bBFsqTCbP8LHYhkCaaWJ8GbuqDUCGF53IVD
+ 4OTe7+goS0Q04yCFoZPZVeBfou/tyDuQykUDfyg+AMIdcxCAgqq0vtAruxR1eEDX64GY7xwcFn
+ +m5z4Eo3DCswHq8JZkk1331gFmaWcMaY2nA14zHd3L9u7775rDbia0mjPjfgc5dX41b4DqpQC/
+ X3AKVEBud5edMuSjYF7NCmZXywPYZKwZEOuALFsR75M+UTw8Aw6LBJ0U31rGsfXmCQBLXT2XLr
+ ifH3QPwL983YJiSX4Uka/iA2LIJAEzH8LNc6fL9jTQSwJm9IoAH/VHMNOTKBcMZuynWbNbl7as
+ GalTFCAnz0BF+Zg0GHXG1apNSxF7xQ16GIgChc5s1wMxZCf1gILgqb7nsRwkdYVeuSnb454ZoA
+ U9dwABq8SKOnzZq8yPYCTFl2DwuHQFFFaPjO42MDZo1kGQKLNMOwf3C4BNwKqBrxtBJQhMlaAP
+ 14HXCm9pIbh12yqDE3btiSPZADzrFiGa3HjhAoE/QLJLqlt4O8oqPGGpucPKBU9LukxDkVJEho
+ 3biIZpFRTjeswfZMXD6aDIcsEJTVmUMVjI1jifZALdipA722x71d7cqFQWmS4aoqLQkKpXSiU2
+ chGdfT66Ij0D2yYuXHM9Lzy7pGXEKgpZvWTqfUHWrgiagHWDVBRSV1EQPPjh0oGx4F9JV8Oo8h
+ NjoTvcZHJh+7e27NIHEK26ftmRRtBrSOKK3hj9QzU7I3XkTR8XREWnLICiA8jLU2pRrYvFYub9
+ bk0F26X5VKJVSUqrMl8xIbstWvX7eHDh1ZFNczhPl2vovZcWeOKGtFHeu6oeOP4YZo4hqMSC4M
+ Lw1ZB45DWimUlrrjRcY8BKb9ev1IpuPKMC0/+9Zw9FHdhYhuc/UWdfXD2sfBkERkWqx9VaZ+nc
+ N7UH11YHP8BnS7/PMHeD6ODvWRvbivMxlbapqQfsEN0ZuuVkt3cXLdWtWLzyZgj6wAguiOm0tZ
+ YWbFaY8WyGPsnrSHwEh2ABeVqYqEpBk4WXDCAiMNTvkGKGbRn+QAOTZRqAQdAjFYIBAcEJHnOJ
+ xcyLgDPXtU0hMRNpTNpFN5DAm1J4GQrTABwKiAkpbRrmMvqGJkivebdax0ohqYseXPSPDpmBHv
+ 6zAC4fImGZ41BgYSbIy9VByPy+X7dSuLpfv+J7LHAJSLgffG61EyWh4q2a0mSimY0s1enq6jAG
+ Qy8qev7WH2bFVbWYYAt1EaYLg7qDh43QZHImXLODF2Nc9FO6VTWCsWygeDqQgGEYOPuoThWml1
+ QP8ZoZIbJUVgvKDlAAGeII1PiCUayI1gUF22A0d9xeiah0wh88GiSDw5B3x+Lu1iTY+tySF9AT
+ pqLiYKWu+ha8+PiayOjOSoOSIKFCDA8FMGBh6rFLUHwdVaRrPLCskAS4xJll6h4oO/HcRpxefz
+ tW7ft/oMHVoTCi66wEgIsEgRVP6wYmEgvhhU5RevNUcr6E728e8gnCh3/ff956uAD8KNBG0C/J
+ OHExRXBJAIIj9cfAOy9lRU60a8YoQXY6x4574r5Fc7+Gyidc372F0F6EUW+3Wd/7mDP242Ljed
+ cN4gbDCnK1PnLTGpqrULOrq+1bLVWtflwSFUOMpbRaGKj6dSaq2u2srpm+VKZy8dxI1NyiZsfw
+ OU0DfhY2gk42JOvnQl82RwjAgSf7av2EtWGcbAJwD+bjhJf7uSGZFcU2TWmZeG6j4xU7pzjKXh
+ qZdHA5/FEktG44ZU5iboQJaBl51y8QeoJ5m5a2BK7aMlpoyGKTJrzAQoKSgiVpdIozRUT2kXrl
+ tB4j1wS4T0THi85Ci4sI9Q8hcYegMGGNKir0KqDHgPwheySHvkL+gZ9DdovMBVa9B9wAAAW4ds
+ TCzZYKfnkKfT0ZNFS8vEPsMcbwPGjxTR9j0TPYDKVgc5dTPH2tZwEU6NpBgb5zOMcy0APx0Z8v
+ Y57BBoOnRHAFk1LylTpR5TltaksPQyNFTR4bL05GuqY4OBJaVFjr+CPr0tqKvBf0DQKAhfBPlG
+ MBCg6Z5/IFBOzPGX2AMt8AfSm9gCIWkSDdsBtX/fu3uNQFZexU5LqbqBBGzmIS/Gy4NdZCSeTr
+ 04fxerBJZBWA199Bb1GVVIXaZxkiCr2xS5N7Qa1GdUGq+hEm695mG+jxpF9gncDxTklVA3XNS6
+ pcfRT5zn7rzZoF0qe357ZL+P575nlvylosBm0FHm+Xfj4I/y0N6BwiJlneRZDUI4yF+Ul8XNi5
+ WzadlcattNsWBaNtOnUKuUKzcu6/b41Wi3buHTJiqWKzcMsCQuWl6yNmVX59h6cWOxoxewPjhU
+ kcFyQTRM0NCmx/QlDUbIzYIVRKNh4OLMRphBtbBlQM+TDfUqT51ETkpEB4xxgcYZUFn7RGyoLA
+ AZ6DgoInsI5aMgNM1QpMeTDMZxEReL7az1DBA8OQNEQ0YTNXklLoQxS2RnL1KWEcK6fwUl0Gjn
+ aTJYSzPgdBEbq73OySiD15Lx+AA17CzQik7pFFz4C49B6vT6DDr6G98QBNQK5vH5jUAv/Zu/Em
+ +qkPbziojPokq594tbEAG2caxqKRSPdz6f6LDgb2tGrjFLZ72wmt1KeV5901bJ5VXFc8+ffP5d
+ tM1DKd0Usonu7O+jwbTsQ8vgG0DEA4v3p8alpd/owoXQ8yCe3bDJg5cHIJ6ApS3aTPgQdVjqJT
+ YEqNm34StEhtVgusuojhYa1m9OhXbq0ZQ8f3LcbN65JOsl7IvpLqk9YtfpwoyZmXaGz7Drpe2V
+ DWUN4TOSbwfUvqgKB94KHT2SYS5y+mtFLk7xRUXh1gaksLjNPgoJeW3BPolfEzvtOR1398bhJD
+ SmwX3D058GewJ9o9KPA+vrm7DeCfQLGF8D+22T8kQ2ejx3Lwq1/PXB/U7D4Nq/zK68iwN5No7B
+ 7KT6U3YvrVr4/pQnaerVi19ZaVsMNPBxRgQOKoTfos0G7tb1t5WpN5T44e27mkUe4jKuQRenYZ
+ E3+L7CrxccAcsCJphsBbLTvBTfOLF4bhnK0F5bKJzUfKFuFCgeyv7GUQZKHwY8EAC/LhWw2zyx
+ BfLov+Z65CRt51EXWF6oa8dhe5vvkqhqeyCDxXPo+G8toIiqlVsWCLVDDIUGF2a7b7ibmXb4nN
+ TJKDYapKikW5DGkcysr3CKsEjJYGIIGNNREkFTqZg7PH9A3DHWYRoY2Hgqb6ZTeNVhYQnsFB2L
+ KUBl4VVUpo19YA7O68SCtxvhimE3cdiwEUZbOoTf10n1WA0vHJZkl6CdyQgzkYV5AGT5AAo+H2
+ Yjg1Lmu0LNwHgNWL46IXjHFiD4no52TV0XmhNySOiYkiqD9Ys5B/kML5UpQSXquiPuuDNESWlI
+ YqiCnLpF12s5/h8/jQYcy3RyG1UpWKGm/MuYaRI9N7OrVq/bw4X27vLvFUAichzw4rlfx2KIo8
+ doWenuArG96C16eib7bTISXUGTwy8NUDriwl1osLVfDP5q/0TSXZNWNzbyHgLcusUFIP4M3Pw/
+ 2OkhueObHTgFA1HBw7REUvqqtFz4sJm+XAP5C8ryMj38B+98WZ0JaxgyG+UqiViPQs3MImFdtn
+ E3PrYnG0nrLWhjd7w9YJlYrlaSx1Vpbs5XWKjX3lskhRZcunM8Vma9KZ3D+BF4odnyiM0BEpk8
+ A45T1hyPSAAA3yP2aqy0+bPtkj3tQURHgtUtzrmEuNIihEKKyg3y9bhqBs9rQKSTeyCahr3aVC
+ ukKB77wyKfqyDNlqV7ElwtYlcnzCJIjojmOtO4cnoKmHFn+2DdmaOKR2S3XJmJEHprynGfGsg+
+ mMRZnEuQHj6/ho99HgBM9xuZnyCPH8KaRbz4bxoadpwVWGEdHR9brdjg4RfkfKLMx+P2Jbjzf4
+ CTnS7HVi/foRnUO9tEYXtBtOjBadiPA15YqtyKIfye+82q3wlMerz6cQBELkXXTFM/poMQF0ye
+ q41JWQzEW3KjZnVBvbnkRzV4AGakMUBThZ4/zMfcmtTs48pjyeoyJW5UIir/nwT6VnsnWghbYr
+ uDxLFdTtNpnAE8cDFNh+hnHfDjRwhg83K1bN+3Rw4e2ubnKoMu9r6Ec47FTI5bvw69hZvYE9bA
+ jkHtlUsl4Ezcmjvm73gQndRNVDuwS3G9e9gl/XLBnPyApweSSeX6QSmDv7zZIH33tt4B93KLLk
+ PcH30H7Z5vZh2bNDy3nZnHREiwWOmVRPLhY5laBKmdlxTYbdcsOR5aaTliqluDtQVlc2lpr61a
+ pNyyHQaB8QTeElvmQPphSySKfFDY1XXnClX7U5M8FklRzZGwwnVu+VOHjNhoNAjb2nh4fPJVsD
+ xdPBmZduPkw4AUOVI8bN4Mnyck7picLmrOoBlxBo2XfUq6AwCDYJ0ZrMZrvtMeyGsCpoySrZBW
+ Ax0Y0AYrpj0BfHDXVNsz23f+Gfu+6XPNs6JV4wMJeGUth8PNcLEKaKyvpKykg2RHgexF84qaAK
+ Vyn02EFpT2zTlfNQcugwexgv2wX7INXamf7kna3J8Y58hzgnA49ynaqY7yxyQbtEr3DQIEsHpU
+ EPndqAs/DisB18OEpFLQROxrAPkKEU6OkT0Ql6HWyrFripYXbQTUQ7BXFkuw/ftMx0jP9CHdfD
+ /bptPYY0BwPTfxkwE3NURihAewxTAWnUFRUaHCPcN3zRc/t/oP7BPtWq06wR/8rKDkFGfVxgta
+ WmZnLdSOzdyCXEnMJ9JeGoBKVTjRonfYJ3j6ar79zZu8Wx/r9RWYfTXGeqG/I7P23/Nx5Q5oJg
+ 0ibpHuOf0EWfq4Je1GB8w1DVZfv3k84lv+/0zi6/VQ0CeK9SetRlIHMJXj4rJA2rifcWm1ZBU2
+ +4YDLJJr1OhU2nW7Hsvm8NVZaVl1pWrleJ+WgfMl94x1E4S3DrJ7SSr9d/QbmchHc0Jm8lWort
+ rG9YyutNTs8eG17T59yO1J6jmXevuQkg6GVmNz0i9+NuHCXqGLQZcZ9s2zMSY2TgP1YS8BlSYD
+ sHHy+zNY4XemvkbDvHvRxYTqOOHerxt90hA1X0s+Xsb8VS8L7PZqGZdzdklJLyBUDvPBv7HulA
+ 6bODuWMsdbP9ejMAl31Ep42fJ3uromv0QhuCDvnoeSj3j+gcohLaBZZbEIvcTB66etJs9aXgOB
+ qYBBWZSPO3D3MvYJTAPLxvGiI++PwNbBa1DXHBrBfgawOlpQ3UV0wI3X+nSICZiQxa6AeSshvQ
+ 2kiv3ttp+I1sgwYzn2TomNAEE0kfl/Nc08j35jZY8iQi3fgEOqrIfEkaOBrAA62F1mrVkra/Zt
+ NC+y55pHDGvbeu4/swYN7Vq9VZI/NvvwC8KPHcZHGEY//ZhonhsQSKiaatUtqHR1Ll4+KwHfvo
+ SVtvVNAb6RxlsAezXMFKJkMimr7BrDnufPeBKOtrp1zGXvC2oj2Wa7oks+jMlhK499Eef8ls48
+ D5PymX9kugBMox4fGtyVfVKk7t2o+a2uNuq3n8lZ08KmWSpSToSF4enbGplSztWK15oqVq2Vmq
+ +KX3f5gAr4YNsEZec/z3+J/mekjc8iXrb62bleu3bTheGLPnj231y9e2Gg4gDjbcrCDlUKdGbC
+ Wnctvn0qEHBQdUrcQfLxYoWafWbZz7vTTn9qE2nz/43QOMuExsnT6sfie20QHnkkO1XJDjfw7+
+ gAYXvLpYVBdKytNAkL77NR6J6c2GQxJMcFbPtnuQ7dI2TIwz+GaO+2IpV+OqypIXUzlly99veC
+ Te2E5KzA22EmgmUujMkgwl4IBmocB1opdCmDRW4nJXlIy3NvqPHrQPDM1JkOEQH4VzdrIzhNDs
+ ggK2IEgoGbGz8xQSi12CkiLSUJK8HcgVrbu2TzpFMksAXg6r369eoMxKKjEOwd6fiF3otZxGxc
+ ps7y6ioAQYB89gjfRONgpy5V+vlSHKiUHea4BzKQop6zWyhIwZGHDMaG1MXsy5ZK9/967dvPmd
+ SuDz0fSE/1NnnOw+FLzBKYxaVDeq+PmzVJl7mq6RrAIdZNonGUQ959dpnGo1tESoUVjVifk68C
+ eVYRXWwnv7j0VAS7+fA1nn3D4rnxL7JsXoH5uH+0y2J/L+r0AOJf1+4W0HAD+ktknaL8UURVOo
+ xzWP2KqDQZObg+Vmlk+nbJqsWC7lbK1QNPMU1YsFGnsBO5xf3+PgFwsF6zerFm1XuFFn+U6P3m
+ jqw2Q4k5beLBTlw3QRXWBdYOZgq1vX7bdq9cNroSfPP7IXr58YakZSl4s/AbXLe5aGmUvcXEDu
+ jSNSztib22U+w76smmAogdDVlgdCL8a8eyyKMYyDnnNk0Omwscti6mukUUEH3ZpCXo0/RDVKME
+ kmMCHJUVTslq1ysXT4/7Izo5PEyfKOC4xM0BvIS5vl10wLAuoXJEHghQg4L6delKW6ktgUvDpH
+ zGbBOgjw4ePPUAJHxzJR0N6aQFIUrW4Dj+oyQhwAfZqASMDd8sMr3Yo1fWqyTVPifompohjLwF
+ bFuxTgHbzVCMBTtFkhHVm5qqqQmZJuSaqvrDP8P4CcwQfvY9sG1Rg9DW+0osIc8WlhEdDcMrsf
+ yvYQyIJK2IurUePQr49sacBfD62hCFrR+8FeMUdBAD7VMo2Lm3Yu+8+sp3tLStQTRZTw6IkWOU
+ l5mMuPw3OPTj7JXOyBY0TCp4liWWiv18M90U/g+BOz/sFZy/A/2awx48s0zjLBnG/Deyj/7HM2
+ fOxkmxfdI6upsjsf7cG7RLGJ5++VZm9E71LWfkCyOOzJRZSF5OXtbpAQ8cq3pP9ReROIaNjmuW
+ r+1iyzyxrM3qXbFWKtlNvGpZPr6+u2+XdXTaZnj17Yq9e7dlw1LdyuUCdcTFf4I7TYqEsmoL+O
+ Rn5vQBIfZvTGCqNbMFqjTW7ff+hjUZT+9Uvf2mnxweW4uYfue7htYIz5Q3m0jC+fir73GMneEz
+ AIoeBcGOKcwZ/PhkNeEnBHoDNTVIdCjzwbgHHSuklOWjRD5jOZQOXGRgyLF82DdUMJoMTLb62M
+ 4kjRlUE4MHQTMoa9bq1mis2HU3s4ODAhgM4T2KYKqeJ2VyeG7KQ4UemxKarV0bM0H11IU6mdP6
+ ulmG/QZ75yPoRJEAd9QY9ZeE+1YmgFluxFK98lZ9XLzKei2pIE8aUt7IMQHEdXkCubSenLFklw
+ E7mc1gGk+P5hhoqFlxLcaLn03pIeero2ltMEkcPRMHNl4lzpSM8kGB8J4krpng5NczjjMpOg21
+ S3izot1CEkIaTFWNizRC8vRrtbwZ7OduJ9qFVM/osvjWNmT1mMbAfF4G9kLdarUpfHCUJ2I6l6
+ /f2nRt27959W22t8Gd558EqIUzQEERIbfi1HHbjIcXkdefN24SWcTVOQgUJsKVE8s8TXl+JUYA
+ 9LLQjuP3hwD687ZXpx7EX2MfiFy8PXKMjbxy5uAbYU5PD7N2/dsEF8zzH/1W4Pwf2b4oGkdUuM
+ t2v/amv/8a30O0v4tYbHu5C93n5J8ims/xW2UdtMvl1NDrV7MQhUkmOb0kfXMA+TAx7YFiHy5E
+ BKuJAxxP4bU/pFTMcTmzoTpIqixUE8CxoKLUKBdtdW7WVatWu7uzau/cx+l2yz7/83J48e2pHJ
+ 8c2mgzd2AqLotNWzGX4d76Y51YryAyLWckI0c+cpnNWqjft1u37BLHPP/vUjl6/8nCDH8twWhd
+ AiHeHSoIUDidAXXmU3QIAACAASURBVHbp+zUJzj4khOOG7D38X+QRA84fqhgA3xhTVrQJABUFs
+ OTaPq9XQTu42NJpB4F73CRh3crXE1YEyU4AyQfl5YKbOsVdsuVSka8HDVT0KCA3BX2D4alCvki
+ PIS8ckkDMYSSvGMhv08ESfQbZHdDnB2Zm9PRBA9asc9a2LjZUuQyVVslj7AIQxx+GX+rPyG8HV
+ UFQLHg/WGSin1P/I7zqyfnSDRTnsmDFUtEKuKZyXCqb2EAAhPG7XHRDFZKCJJvLBHMFYXHSQSk
+ sGrAimnlADWcetEdYTcBYDZvJ0KBGFTjALAICNs+nltRQHipol2xWjloLj6LwUHelVTR+SWeQY
+ /K/+QJ9Y1b0BhlgNK1LS4703ErFrDUbNSYzPHYsqdJWb9Ts3XfvcRUhql3tgmUZI/8fp2TwZnU
+ deR8hGYzCa87xvC4mXBfgrWROv0Mq85waZymAOK4w+DslJtpmQQctHismd91jx89FqIPIuwd94
+ 8/v5UEQTws5q4G6W4xGLSZkvSohbROI6OfM92so0C65YPqPeS0iaLwApr8b2Duq/l469pCw/ZZ
+ Y8aamwu8aWijQ8nsEKm/62PCgu5Y90YDLRAqAiInXZqVkzWrZ6qWS1WjZi7IecjjcHDM6R+KGw
+ Rh8BztbAfxwRGS2JNkhALaey9lOq2mb9aZtra7a9955ZNtbW/by4LV9/uy5HXc6dEScgCIZDmw
+ +GRhmKTOUruFmxYakilXyZYLHZJ62SmPVLt+8baVK1T7+6LEdHx3g8mAmSI6Yy00kwwQNQ9rEs
+ 1X61DBozWR1PIb9Mbx0fBoSnDcaoJxm9GUpbKpBKTO2GbPhng17PRsP+8m+WazaG09nBoEn3j6
+ mitWQ82UVviIupgPR6GOWzxtJKgpx0DHxiSUekJHmCZB4X7ScmMmSGHI9VEBSFInDV4PWlS6Y6
+ GXvA5UGzgkWfWs6Ez0HKHBwzMnBz6Z2dnZmvV7Hp1el3kHQCV7b55pEnfClIuCPWd5L845egJB
+ Y1D4a5zmrlEvWqNesXq8Q1KArhxIFdAXWPkISC88cUi5znC+8Xtg5MAUR+PJYsiPiwKbjRaWS9
+ xIi4FIiyOzZOIgnO4U0zwP1+h7cB8OxdfpQIfWs15cNNufHYjaAmb1jA28Zz/Rd3hNNTP2QZ/P
+ iV3zwTUEHXDuBBlYQBHtUNHAnxTBVxtZaLVaJ3W6fIN5oNW1r55I9eHjLWq0VDs7Re95N/rRMR
+ fx7ZLb0xXeaY6EsWmT20RMNvv7rwD56PeLxl4IDlWHqfywCyEXAX4C9fl/8urZ0qceWgL2DVwT
+ LaNo6i6qVhbh+4sgmdgh6AeGLn+A2lUlup+Fgf/6cLNAy6KdzCfEyZ/9NwPp/Mtjz4ES2pbTH3
+ 47sWhONHAZsUmmrlIq23mzapVbTWrW61UolK8aCEPc7R3mPrAggiRVz3eHAesyWJjagw6VkYag
+ GyrmsrVRL1ipXrFku280rV+z27dukCp4+f2l7h0fW7vXkqogA0mnbdNi1yahHSgYTsASJTNHSq
+ ZwNx1O7+867dvnGLfvo00/t1ct9VhBQ+SjDndMmAEMlAI3UHCZquvhoSev7ReHAOByhaYmJWxi
+ IecZcLHB1IiZYYPGAlYryhJnZaNC3Ubdro0HHBp0zG/Z7bGrSSXM8siGkjRx1Nw0LuXEWLnY05
+ ej5A3tiaPzxmh3sucKBp0UuoaRGWJ5r2AvgjqE0PN5wAGfKjFXKVVY8lN+FMVoa71nqEWTzGEh
+ DUxvNY7pBgssGHTUSfQMlECs1nEPn7HEDSgKIsKVmLydgeamoVAb/LJ37hDMS+B4SAC4bmUPTn
+ mVF1mi27Pq1K7a7s2Wp9MT2955Zt4tdtAN6w3f7A55PBElmxU5J8VnYr4mF18pCydM6Xxzgx2Z
+ tNIdxo4fjZ0rHkNr0bJr7FQD4qBh5PWHTGHx/+iM7aZ/ayWmbCihu2UpWGer9huJHN47ktaoSF
+ 5YE5zACWWlQhc6vg85TRi477VIJ57Bo62trdnx8ZGdnHV6Hd+/fteu3rtvWVouVXfRnIrhxpSG
+ 5G52PWAsIw8HzTdffAeyXvHMSysargz852AOr2Mz3PstSUJXmXucmwDuIoMjsl9P3izTORZbkr
+ cnshYARSv0C8ZtWN4+aXSh5a8Wiba6s2O7qum00GlbH5hysQgtFi/PA5HJ9JB9UBoAOGTLLbwK
+ NplOpGMmClsEyZZg95W2l0bAru7u2ubFpL1+9si+ePrPecGz5aoVyzBkoEoAoaAtk3MMzGMjbf
+ IreQd4qtaa9893vWmcwsA8ff0TdPhd50wsc3ugKNmpSeqbl5lnwgEFgwoQpbjpQIfV6k5VMLle
+ wNOgjPE4mx2lbWAggs+bGKOjX8bp6PRsNetZtn9qwC4lkh1QIKho89xgVg8sG6a7pHji8COnom
+ bICt3UhgAXoIzAqIGBQTRcj/SiUvaFJnRdHry1bUwJ9tVqjCge/geEwLl+ZqLeB7Bb53hz/dg8
+ fefQM2WcYYdcsaBoE6H6X07wDVFagfEjPAezD9dEzbA+mqhS0ZzYZtKO1T85y+Yptbmxx+vPa5
+ S2C9Mnpkb3c37Oj0yPr9DtaduK6eQaIaLNlYFmBSid2m2Z9BkJFuGSgqlx0jAD0Mpkbc8J5sY+
+ YO6/cgTMFCSRlqAKHHM9B1sr5nFW4OSzDvQenp2d2cnpqZ9iVi41liXRU/YDIPMPBNMkSXY6ZA
+ L7TFfTW4R/0kMTd49+obqrVgq00G6zQ9vb2eLzRlP3JX//YLl/d5qwhLLbj/KP6i0ozkU/qm9p
+ 4FQ1MFpQeJL3SSDL7hPpaNGJZFb5pgvb3zewTGmeR2Sdcf1IyiU4KVU8Ecqp0XHK7qJyCkfCKw
+ jX3yu69ae3XkYLweUj/bezI2wP2YWaT8I0+YsyBFV8HZzOuFdxoNuzGpS3bba3RoriIbUj5nE2
+ zatJyupAboDD0NLIJXSPH5K1nk5ggVfUQizWyOQ2QILvEAcdoOIaeti9t2d7eS3v69EsCU6las
+ 2K5zAYWFBIAI472jzo2Hg1sPNCy8tt3H1ixVrXfPP6IQ1Ms19MZK8EVEDxzcHbuBDieZ0gTdHt
+ d6vuRhYP3xmtAcwx7VbnmD+UxskLYBaTT1un17Oj4mDRAp9elhFEr+CY2RiWDDL8PT5nuYnAJ2
+ S49dHyoyhP1GBySm6XorZwP1QB0Cv45l3EwKKCX4vHKsxd64cDGGDteuTIxa7VGkxYJvNzTAEZ
+ IVNXo4wAZzM/cxoA6ewdpZO2DXofnD5UJd/DSGhoSTShZ3NFyaTer44rPHyiwKZv3XtA8bfVay
+ 25cv23vv/999hqOD5/aky+/tP3Xr6zd69oIv+PKJSQCAFmZcIF28T2pXF+nSiz8+kNHHmqi8Ok
+ JS2z2JPDY3ihn85XNbvkfccqUqhj1Kyh1nUkxBsAvF4ukm+C0ikb10dExzz0XzbsoICIMaUJu1
+ hK0L2fESj6dXgkqI/4G2HulAbBfX2tyvywSj/3911auVuzhO/ftxz/5K6vWSjaaalUmzi2PEcO
+ cjOkI5z4VG7w9rxsHbX1NFhHi2HUtJTz9xQYtKygPHMkU7e9J4zhdrOf015F8LcJi9A1kdEaqj
+ Cou9W78CPvnSw3caOYK6XUe/KdFFUl2Gucn+Ycfo+V/82feGhoHpR5pFVygzmt50kiVAAPmzCq
+ FrF3d2LDbO7u2BRDB1iBIFpFd5IvumSIFA9Qb4BmhOaf00FUPQT9QdeDe7akcZI5ofMKsDIMm2
+ OlZsEKpaM9fvLCjw9dWKeatXq0SsJGpwicEnuYoN/Io2fB847k1m6t25/4D+83jx/bJZx/bFA6
+ VbqGV0Z1EoEYGTIfGuVl3OrezXsc67TPe6MikcJPBVyYUOXhNAAZkh6R55jOB/dER5Yntsy7fM
+ zLIoElEi8xJhZB/16Y4GYe5BDWko5KJ+QRnAhLyMsljihLLK/I5fo7LlLkuF4yLK16M86vJycU
+ kKbNiCYtgavK2z8ICFysPqVVxigEmca5CcetcDIGBthkMezaDAVq3SzsJyi/7Pe2nRWWQSXFgj
+ NnyUmMU7ymxK+BYBYIjegg1e++979iPfvQTKmH+5Rc/sy++/MhO22c2wMYrvObQhHPTGOSxbhs
+ QG7ayKs/ZlzWzEvfyygqCNI73F6iSwvlAczktgzioWHDeyPezI2U2dkMzufkstPx4O1QSQZoLu
+ iuV4jXYqFVIW6LK7XS6dnxyYm32Mvq+iSz6EQrmy8B+Hlz8DOB9+u5ZeBoD0pD4FPIZ27q0QSn
+ yq1dHFDtcvX7VfvSTH9qd21dsOh3aANe2g2OA/QIi0QKIyQDP7N0tVkmuMnuH9wtgvwhGv61Bq
+ 6f2hvO/grOPLD5201JDE/3CRWnmuv1YRO4UxDymBRIIDyj3UOd9HCellykdxVtURIvMfpnGeZM
+ y5y0DezQlZzIGA2XjPuyiYaVWaVaLdmtry25c2rTVspppWOuWyZcsW6iQ40TmS6oCy0EgY+OaO
+ 5mD8cbjlKI75oEjzWVtxgXKMxt1B1S64CKEpStuSdxMey9e8ObaXFnlBGkWGXkxb9AmYKq2gsn
+ Q6dSK+apdu3mb6pOf/eJf7Lh9YjbHcJNr3V0xws1TlFoqq+2hvOdQ19wKuaIaXmnsPV1I3dAZ6
+ A2HNGqD0ohUyXRi7bOe9XvYPToUCPkmKXk6yEOfpBVBD8CBhi4yc/6Pn7M5iIUdXEYiyZj07/L
+ HwWUN9RHAvghwQ5AkL60pVDxVos12TT2pGwwCZbPWaK4Q8HGswJVHA557BuAayfcj106qTiALh
+ QoFtE23Y+2TI847oPI5a7et3+/zxsrm8RyynVYGrRkKBS35EXGXSypvrbUN++7737Nbt29bp3N
+ mjz/6yJ7uPbP2sCM/JOKiJLBc9+culOLTM1y4TbtiVHXOiuO4lKA28gChJTb4gya1Ai8qPwE8h
+ pFEoVHwyixaPRrOFfjGMu9QifphBeYkgavSMLxXq5StWasaBgBxfvB+Dg+P7LjdJp8PgA9WVEN
+ y+hczfn6mRj/3MxHokVC5h33KSOEU8xlrNGrcsXva7tj61pa98+gdZvWlIlRNXZvqEnPAF6PHY
+ +HqHmb97oAayq5QyYia+e1g/8ahqiW7hD8E2PM4uNY/AfslEF40ikW7Bk0mJY4YgovZvUKZrFm
+ SjH6JsuFv/Raw1+9coHjepsyeN2gqaBb50Kdm3vSCLVgmZZvNut3Z2bLLsC9A8wqZSD5r2XzZC
+ qV6klEK7IfylXfnRK4E9EEb13FqkCmXMcvJxxwmW4Nenw0yjPkjOwU/+vjxJxycuXvzju1sbrP
+ R2h8OpNAZjy2fHlnWplatNuzy7lV7fXxiL/b3qczAjTWeQP8uuSQoJUrpkKHHnlVUB4UiNfwcR
+ JlIhtjrjwjwQ/y+ywgRlLQou8CgcNYBldPnijyszMsWC5JGpmWl0O/1PeBhjR1kiDANU6lNgMs
+ YszjJJPOaF3DwAlIiQwX4gUJDXwN/yOO7tDDjunYanTH7BSeeI9VEUJzO2HNotlp8fVr16P456
+ FOwYeo7TamZ1wJzPG+v27NO+8ROj45sPOhzYvfw6JC0DigCPM9s7jtc6ZEjiSQtIDJ5BhLEurX
+ VTfvgBz+09x69a59/8Zn98te/soOTQ9pLYxGJJIMwgZyzka4lGx4MEdjyMAODxh5g70w8d8AC6
+ ODIqb4HqaVkW5W+H8oazjyQxoF9Bk2BPLHTOLRcJ4TsfDlOt3HpTPRIPBKASkM/ZK1Zs7VGjb0
+ V9GVe7u/byXGbFGYAEH6FMwVRdSRVEII+GrK4GnSlsjpLpdi7guyUUlVuQyvYw0fv2Pvffd8eP
+ Lxr/f6pzeaohJY2sXngjxmE4NmDpiBnT9pvQXWIxnHKxynsRCefUJ2ibhIZK38/MvpvyOwTKig
+ qBR/Siq+H1XNUB2GqFhDtyC7R2oJjD5APukoHN+gxPZcatILtEGFqn3FQNwsaKIqIr+fsU39cG
+ kcvMfKFrzBKF7/wrf+tonJ5ubIatcw65lOrZNN2ZbVp9y5tWqta4hYd3fAw26pYrtQgQGs/KRp
+ 4WjrNaT+/yDih6jcMMqssOGM0xvJZG4yHdnp0TCqnDCAtFi2VyVu317ePP/nMptOU/eSHP7Yff
+ /BDTtOenBzYrx//yp4/f2aT/qFBjo2hLHBOg9HYsnm8Fm25Ag0F2gBZFzLwwbBPrnUAq1hUH5B
+ vYiI3laa+ejQWFTPsjQn2eC8A+FKlZLVqhTw+/kZv4fXhkX365IUdHbetVKlbqVq17rBn85Tkm
+ rAFxvMicDJnR5CZyotHKxtTVi4CoJG5atALWTkqjzAjo9vQbG7FTNpKpYKVYeegVJEjSZDuofl
+ ayJWUAaM3UcDErNm417dSvmTrl7atWMeqxxzPB60lOICMG15TrFDY0I8fn+M4dTvW757Z2eGx/
+ m6f2sHRIRd9oxJhfxsTsDR7A5ii1pIkEusDxzOja+n3v/N9+79+9GN7+uRL+81HH9qr9rF18Tv
+ zuRXoWy1PY/R7srR4ls8JpbCQRUKKmc/xc3nYq6pCoEexKM5dC7olifW9u7imQb3B1wf9I1Rwb
+ JB7tj/Fe3brCvlvSArp95l2EEjmGqAZGSVgA+dopV61jY2m1WpFHqNXe6/s5LDNaknePjSw1ut
+ 2WwXSTRAU+OAPnwcVHG2W5UaJP7gGMUy2e2XXvvf99+zBw3vWXKnbAEN8XFCyaM5r96pz3xor1
+ aBjEDV4X5RvheY+tPMOjucM1IK7X5Ddel0RKJZmGL6OxomViA7siW2Cy42j4ZsEI3/t0YBN6KE
+ kbC6GNoXYuPg8q3cvpGjk6lcWGf/C6X6hvSdlHSjpQ1jLtM6C5xcG/lFpnG+N3t/iF/SmUT5ru
+ CoiIr/OgzKzaj5jN9aadnNtzcpZTFsOWeaiIVgqlq1UrlilUmWDFNkxwBUae9n7qrwEpRAlGy5
+ mZKP8er5AmV375IQZJQalcHMD7EGlnByd2vHhiV3dvWzvv/eebV5as1lqYi9ev7CnT59Y+3hfq
+ w3TWeue9Xii4YUPAAT9wpuMo/HGHsJwPKCsEHMAKO1Ba6CRiKwfPGw/lp/IpYslPvTMuzvbtrm
+ xSrsCZF+oMPYPDu2ff/HYnnz5wkqVhhUrFVI9kxnki2hs9shrU1ZJq2Vk96KMAFqoYppVAL1M3
+ rBqjyw3fOYd0KHCAdgDhDC8VikUuBMAry0Xni2ZLLPNcqlqpWrFcmW4hM6tf9phRndpe8cqKy2
+ bgWYDgCEMgtNN58mpU6MOMISefYJhqr6NBz0bdDrWPW1br3NGnf3JyalUKE5LpLDTNpdl8OyPB
+ rSDEHYDeHJ278FD+/53v08f/A8//DVnJiA9HXFpthmW7KHZTgrCA6AkqKJ0UD2AGuTgFKurscz
+ kCGZZDo3hWoFnkvYVO1AhGweFg2A9kIkb97biP9A6hoE/2AVjjwE2lvmyEzc+Yq7NRmtIPBN22
+ zlhnR5cA5VKwdbX6tZs1DkH8urFgZ0cnTAo0obZaSc3iFBvwL17aGvNPcVaTamF4XqvuCY2L23
+ Y3Xt37P3vPrLdK1s2S8n7Cb8DSjDRoSfOkaFEMZ5f/agrflyGqaDw9WB/TtuuaOE/v8zln/fKQ
+ RA5p7P/Ctgrx1bloMcLo7h4PVFliB4KOiqomIV7p950AHd83wPRG5q2AvskpxfNyJkMfahSiR6
+ A33ZLVgv8mT8mjfMtsPtb/6imAbUwgXxp0izUoAoagI1S3m5ttOxqs2nzYc9OTg6pQAGnCmviG
+ oaranUrV6qWQWY5h7pGjToGEsoskbHIblcWB7qRU/kivea7na7cHXPKcGFAhqYiUvOz9hnVNxi
+ +2drZtFq9arP0zM46p9Y7PfK+wNzax2cEynKtRo4PGm3vs7lbo7TfqDjINVOvDlndxM4gk4SWf
+ DCyAfze5zPLAaDHI9tYW7PrV3ft0uaqFcqgmFI0pmp3+/az//2p/fyXH9p0iuwyz7F7DhvNVDm
+ gYqJ9Afhm57dJO0wmbPpdWinxxoaWvNuH7JFEt09rys6BkkwYvkGmWkCjFpYKaUtPxdWmMjk28
+ 6q1ptWbTStWywwm/Taosa6tttasvrrGAIisO1fM8zxl0+hPQI45sE73xHqdU5tOBuS9IR8FrYZ
+ l6cP+kFRF+7TNYSsOYOF9YgozjanlMak1NkGxzGWesksb2/a3f/d3lNz+7Oc/t/3jAwLfHL0eb
+ rmaWdb5em40cythBEWAvXx9MCQHXT96CIBoZLt5KqRKFVVX0KSzX8RjNiMdBHkqvJJGXZfCglY
+ EzWPI7Cc2AoeP4IEGOtVF6lfEsnl56EBZJjknwTGWdidy06AxMOmat431FVtbadgE6pm9V3Z8f
+ MpzOSVdBv95N6CjOlQVZ3gLcTAN3jg4F1D3jGe20mzarXu3mNHff/eu1WplG2KgkMOLqOhcebP
+ UKA0qgtDn+4ATNVCAfuJnv3CJPG905vVA8Nz/VmAvlPWKZGmymFO9HqaCynFwj4axQ3SSwQfUf
+ 5XDV7ATui1SeWDc0sYBd8z8qola8htvD9j7yaUqZyEZiwiIwZ3VatnubG/alVrdBieH9mzvqb1
+ 6fUCOFJwlZGnVasWqVdyEZSuXKlbw6U2NMqMhpjJTpXmOunUOSuWz1MSDwolGHE4QVTkFbFaSv
+ 7zG7OdcpoGJUQzC8EacDy0NkBlN7OjwlLwkaBFk9dSUM0NTSc71dmGM5YtDyG3P06xELJ0jOJ2
+ cnNh4MEQJY7NeD20FW1up28ZGy1obq1asFNivQGPti71D+6d//qXt7x9ZfwTfEqheVBVhojY2C
+ KG5R1DxDBqZXrNWsUtNrZuD9h99AgA+9t6OJ7L4xfFCYCSfixZHLk1VDuxv06CH5uB5cazKVq3
+ VrQqlVLnEIDPq9ax71rZKsWytlZY1Wiu2trFllUaDP8NmdCplxyeH9sVnj+3F8yc2mY5YgZyen
+ pCe4TBZLscBsna7bb3egL2Ibr9rk8mcFRH6FJwjwMDYxNgn+OAHH9h7771n//jP/2SfP3tqPTT
+ LSVOVmAzMMJA20z5fP1zJYmz5vGtCGeeFUtEcrrMKpaR4vzguOAbhbURw9h3F7GWA7hlCQorBt
+ r4M3QzzDnKOHKOBTqrGvfwJ7NLkczMWfh/0H+wuOFDoJmXhuOBDeE7IW7mYsU1cH42a9bsDO9g
+ /YiWE46J+mNRukAFR8uqtZlphc6XixOgvgxpubra5sW4PHt23B+/etas3r7C/M5vDzkLHC+2LJ
+ FNO6BJvLvp2qkRKmQC8a9ZjIYlD4J8C7D3Jdw/9JS09bp0lyii4+aBpGCMDvMmJLbL6qA6Wcna
+ 1celnH3l8jL0tmrgUriYNXYWFxcLytyiz5xsTNi0+kjCI0jJlG42aPdjdtt1azToHr+2LLz+35
+ 3svbdDtsdEEnrhYzFqpnLdKtWRrq6u2vrZB8KGFAbxyJlg2PWODkBx1DvLHLAGgh6xwOmOmRj7
+ YN0bliyVy+9xXSe2iBkfIbftYeDotm+JRf2Cnx6cMJKCIQJbIQ0UDLyyZvRkXOvdYdMHl05BkF
+ kusBM6YEQ+klx6NSWMg281n5ra62rKVVtVqjSrXGx4NxvbJp1/ax598Lu6+VJPLpNsnTLDNCZY
+ MoLfgEYRBo+nEcoWcrbfqtlYvun9MyoajmXW6GNGH6se1Lf5eIa+D9h7VFAaLCoWc5XDs02pgl
+ kpV0kjodwAM0VSfT6SXr+Tzttlq0Rd9ZX3DirU6j+ksLeMv0CxoMO6/fGX9Xo9a8sOjA2bwqCR
+ aqyuk6dC4Pm13rdPuUoY5HIxZBaEX0u8PrU+abGq3796zH//4x2zo/vrTx3YyOLMJXg/6NJmCZ
+ ecZmw0nlpkMeZOBasLfUFpRlQSPI9fbI0Hg9VXGmkX4/MTFiZoU15Qkkgik9CyCBGgqSpI20bj
+ ufHkMM3r0JpDjh32y7yhgcuOTwDjylNFibgIDXi7B1HUX9hayYo4pzkxqZrVK0TZaTSp1APivX
+ r0m/UVb5thFy8ay7DJRXYYcGVVgLpPj4nDcB6RwHtyyB+8+sPpq1eaclsZCHkhyYy/ucnbsn/s
+ EsRZ6X7QmXqZRFv4wf0qwZ3jyRHCZQjpPJyWElJ9+57IS8Siga8HJK8H0AdELNA4e1/fneUw4T
+ wFJErqoAyhHf1sye7xjqgEuNmh1tJhBb7Wa9nBnx3aqNTs7eG1PvvzCnj5/YT1o0+cZS+cKlsu
+ lrFBIWbVWsK1Ll+zK1au2vrbJqVaoVUCRABToI47MjZ4eOVJIoZmHLjyGlhBEMLSFxivkialsj
+ v0E3KS0u4WlKv8ek8uGLLB9ckpaCZJQOgT6zla9NV9uOoUZmTi74FGZJUINAyno3Mjbww8Fr69
+ UrNKjpdvp2Onhoc0mQ84crK42rAaeNi9VzoePP7Evn+3x/WZzRWbWWFoy7GM4aaBlHb5eEMGpW
+ i3Zaqtm1SKqCYEWgmKnO5APigc8LX7WDYHXR5+ctLF5B+oG9rewRihADosmOZQ5AEg6QI5tcHZ
+ qqdHQGqA6kKFWqlaur1gG+2mZHipDRP8CtA2as6PBwE5Pj22C3sYQCim4bNasWCyxMkHlAe5+P
+ IJ3EKwTxgyQUMb0h2POOqxfumT//POf2cFZ24aZmU3RHMQC9FnGsmgtY9sWFs7j/VAg4/40qOI
+ 4lOQzFzgvBfm/ALRRAeL5AI4InPicQZRWHfLrF7jPGAip7vGVg1RK8foTzcEc0c3JlCVLzygZp
+ yg/XEeycZbkFueDUlk3qqMLKk3d5pZPmzUqJdtcXWH1eXJ0bK8PXvP6l3+Pb05geupgH1JNVHG
+ pjK3U6nbt8hVrwi9qZ82u3Lpi2XLG8iVMgEvFhWAIwE9y1SWDMyWwGqoiBCaDSs7dewZLm4U/c
+ WbvnMJX3DIDrBPAX6Zxku7pgocX9C+bny27ZXpD2cFfjMVi2U0cr0giSPtcdMV8W8BeumUUMpr
+ K4d9sqgAAIABJREFUPJ/ig29O2/baij3Y2bbtctW6x6/t2bNn9uzFC2sft202xVRp0fJ5GJKlr
+ F4vs5l57foNW9/YtkwaYD+w084ZQROqDen35bc9dUOrHNbolYqWhh7cm3SkL5zrz5WKatQhAwf
+ wM1lCljujBv3w1YH1ux2rVKs8mZzgxIpD17xTVQEQQeIXWRYXqbgnDecE1DhEw643GPAGRQMa0
+ kzc5MhmT46PrXvWIS5wbVwZAJu2V4fH9vTFCzs56fr7AqxMbDjo0XYA2SYqBdyD5VLO6g0spSj
+ yuJMKgQpoOmP/AtkkXAnBW+MYqIqRXQIoHygtAGSlGgbAVqgQypUrlikWWQmBxqCR23how+6pd
+ V6/tnR/aE1YXKxvWKZUNTRXeWzQAAY3PkVg6jEzhsa+fXhERc6gg0ngLgfUgMqYeC6UMNgGzl+
+ qJ1JqM/HM+XyJZnRfPH9pv/78M+sis8aUNRa9F9CSxcGXCifNPRyQXMrGmDQKVy5iIExVnrT/A
+ lw0WhGQqKJKoSLUseOYGI6Rzynga7AX4GrMoDscxMN7Bo+tFa0+gepmczKai9mBuS8Id2tqvBY
+ ECPcwcnPqxfKV+dyKubS1ahVbXanTc+rVwYEdHp1Iy89rXpJgrpgkq7NYqlLIZO3erdv27sNHh
+ h21uXLGctWclVfKtrK6wkBOh1p3nYtiPOHqE98aXSfRnA1hxLLKRZp8AeYfPbN3+aakrgsr6Hg
+ fia/9mzj7hVQm2qveiH4D2DvAy1nTPyA3XgJ71QQh1XSO/20F+yhfYxTErYUSJQ7UIFuthj3Y3
+ bHdStX6J8f2cu+FPXvxksA3GWEJQ97yBbhh5qy1UrUrl6/Y1es3rLV6iWLyHtwD+12Bmm8ymsx
+ TNpzCM31EKScoFFATbCJCgOcyOkoJkUnBsgCBIJtluctrfgoXzhkbl6/3XtKqoVKryq2SPi9ok
+ KrJxkEdb44pm3VVAC92yPvylsLyZ9r/Yk/rgGZucMoH7QRARMY5gjSRa/E09YusndQMuO/Tth0
+ cnlDC2euCioG9ABq1Y3rQgEfG8SwX0WQUaOON8LiQK/YGMn4WFgHuZ0NahgNVAMWCfHByeauvN
+ K25usJl7YvMPkclCvjr0Xhg/U7bjp4/t1m3RzdRgP3Yh4poS03Jn8zN6HQ5GpESax8f0cyNvD+
+ 5evjB9DURjfTAB4IQkNCgbzUbdnlny3Z3r9qz/SP7xYcf21Po8pmK5rV20JvzbnNv6REoOQwSF
+ Ui/4RzhNVF6iiwe+1axZBue+uipwK/dp4ph/3DSnVi7A6lr2grlMoMDVUjuVomekiAAx1NUCjl
+ cLpPXnAcb3T4f4D1DSXBcH0/+mJp2VF6xXnJOwzpZEHhOyCZ6ikopAP5aq2H1WtW6nTPbe/HSB
+ oMRrxEfsUqGxziBDLiBnUSpbB9873t288Z1yncHs74ddQ9t59qObW5tULSAPhCOxQDXFucK3Ob
+ ZG8c4xuixVEplK2QLGhT06Wn2zbjOKuSTnlu7d4+CweJrXiQk6pmLQeVrJ2jPqXHi8bzZkTy3V
+ 5UB9vx7mYpavBZRPUkdsETl+QlwDj/2iSVOmqFHYvWW1EE+uKjzpiX3i9em3oAzPApFbw+Nw3u
+ Rlrsu6eI9oU3t8A8BT7hRq9j93W273mza5Kxjr1++sucvX9rh4TEdCmkznM9bpVywtdW6Xb963
+ S5fu2HN1iZVBzC3QlbPjWo4MeTq89abmJ2dndh8NhDP7BpzKESiXAa1AyDgv+E+iT2ssWCc2dK
+ I2TF2ygKwq/W6jWhNPFaT1TndUNaieM1RcuzmV2y8YWBH3jGcbp2nGIQ4jDWE1FDqCU4lYmq4U
+ LBcSUvQQbvAUgANxU63a8fHkN2N2MQEWNFTBhLA0YDSUjorZjUaTzUCbYcRmBAIRReozxDzqLH
+ QAoCft1q9bo1ag1YI6Bs0GlW6HyJYMijhRqP/vgbKxgC+gxP+vbK2aqVmQ5UNAiVAAP0NcNm+f
+ GUMaWqvb2enx9ahmVufA2+gsQCi1NRjMG06sQ4C2WhqmVnKrsF2994tu3b7tn32dN9+88UTO4a
+ nTgbXEJwYfb+v0yVUGE3GsrzAXASpQDXuR8OxHR9C0z+k/HI07BOwsY+1iTV9JUh8M/bqpGtPn
+ +3bYGxWqtWpycf7hvNnB7TSWP7uIO5SKTxXVJQ5ZnNZXreSueL9g45hxjuX3QSufSbAXMHnu4+
+ dIsKloAEw17jPpdjhBq20Wb1ato31Fu249/eeW+cM08IIGrICn6YUOAhQ05RlZ2nbXt+wH/7V9
+ 21to0Xq5vnhnj15+cSu37pmGxur5O37Q6jGzuysiwDcp20450lQc2ACF4IJHqemrddatt5at5X
+ aKgMqBAiyUXBYXM6svcK4COhfJ73U5fs1dgmxN8A5deHnebBfriyCSgmg/Upm/5Ug5KZdgd+ei
+ fPYevIWwlNVkgvKh7/iHknB/Gsx+wLgz4E9nvttoXHChptSMFyoF8AetE6zmLfbWxt2Z3PT8pO
+ JHe2Dytmz/dev2czDzVAplaxeK9n6etOu37huly9ft8bKuk1Sc2X1A6zsE0sOvjhdKNlwnrWD1
+ /vW754ycyUnzQXfyGrheQINtVsYIBAUkQHKlIyXK6UJI5tPx/b8yRNK8qorK1JcwJdnNHKwV/m
+ ND5TetFzg8Ip2gOo53V3Rh79AJwDU6BHvy1dY4usuh0eBSyRh3zsl5TIYDa3dPvOhshHBHY8B2
+ SJW+oF7WphWaehG3u4zBgR6plOPr121eG2UqPrCdhyXWr1hq61VK5VLWtmI0f2KHEEBgNrjqqA
+ BSSSohAmy39HESrWqpcpFUlzgqmGhgKMCv5s+pJZ9UU6YmO2cndnZyTGBH1OzaNyyksF74srFi
+ XWnOKczm49mdu/mNfurH3zHGqtr9suPPrVPnu1ZF9YFGdkToAezPGcBzOc+KFg64xyXyqR3EDj
+ bXLXYJ9UB64x8LsX1fGtrDWvWq9y5iov11SEmrL+0V6/bpBKhLacOH8NTAMEJTPCQxeOnJwRLK
+ Kgw3Ysqq1yAu2SF55+++ZRhYkgMy8ylTFMFqOG1uFc0nYvBJjekc1CjWpvUEKw3Mra+vsKeDCa
+ RD16/prqHbjtQI7FJzLjMZCufztl3Hr1j7zy8Z6VaydrDM/v4yad22D2xYqUkGeoMFh2wWu5Y7
+ 2zAa5z0UKJOgT4/RZoNS082Ntbs2tUrdu3qVdtorVupVGEDmNYDoXlPMutY/B3UTgwtvVln//u
+ AvXT2YX/gFEsyOe1BxDPsRZN5ueL4ZrBP2qwXgJ5VAtmFAHePGMtqnCSz9+d8m8DeRQWKipR2h
+ WcHcoCJlTFBu7Zi93d2rAmbgKNje/7kmb18/ZpZHz7AP680q3Zpq2XXbly1nZ1rVmusklLpDgA
+ U4KJxI6YsWypaplIxGB0cvHpt/bNTTeQ6zwgABNAjU4UhGZunGJkH2GMjFU2clKGkkCENe/bsi
+ y+p/qmvNm1IsB/5ikBOVSXSTS6soAe99P5q8ursaqG4yjhS/GwIyp6ZTVTQNeCV2cDT59rtKmk
+ dgwOGsrxJyJV2pCNUJUDfHws1AAqkheiZLy8XNgd98xJeBMp2NGDJRfOelolbvV63eqNBqStkr
+ yUETw6pYeReNxEqMzwfqJ/B6Zll5yk2lDHQhIAFr3wAPagJWBfD4wVyy2Gvy6EqAvyZ5htwLGP
+ rlha6YNJ4bP05fHTmNhtM7J27t+ynP/13Nk1l7H/9y6/s+cGx9WbKpvG60ZYNKSArKV91h4CTK
+ 5ZI0XW7Azs+OuYgF4IU+kUwgUMfCEC/sdmylWaNunYc28PDU/v442f22ecvbQjPM1Z8UHy5OI/
+ lu3vuQB2Fqgn+RbBmsLE1ijmr1WUDjfkIKInwp9MH8IOuCkRQOx9melRtU/IHTx/ZTcc0tN6q3
+ BkxF7HSrNhKrWKp+YTU56AHgQKCuaTEdE7ynSeNStU++N537dq1XStVi/bhp4/t02df2iyTscO
+ TUzs9OWNQBrXILWMuzeU96xUiB4i4B2Vu+VLWyrW8be1s2O27N+3Rgwe2tbllZUhfaRexGFxSN
+ v/HB3uH0gV9w8O9ZK98Tk0UafzFZiyuZwUmnnWe82VlvRp0ywob/exCcROyzng9S9/SK3pbwN6
+ PUlLGuEW6tO28kieWS4HKKdu93R3bqtdJ5bxEZr+/z6UKAK1yKW+ra3Xb2V23q9eu2tbWZStXG
+ +SCwdlTRTGU6iRdzJsVitabpax9cmazAaggZPPokAoE5UEDtYl01aBvckUMBRUIflqBiJtrasP
+ emT1/8tTq9YbVWg3K9ujNgwEl9+QJKwCoKAB0iyoizCjVhNMeF5U5aKgCjMnV+rpBmW3BWwcgr
+ WYbpnFxI3Iwx4Gb7p80X0MTDmP7Y466c+m3G1hB401JqhzD3BrX1UrOK5OaITiCS9CNgACAxmy
+ 9jgUleblhgsvHRCvUOxzRx/q+CRVN3eMT9h4q9Zr1pmPSYPACAvj1+jA4w4TsqbXbp9brdMjT0
+ 98I1QYdS326Flp1NEp9V8EQHkAAnd7Ebt+8Yn/zn35q/dHE/uFffmVHnb4N0A1nIomb0Ke06ey
+ oNZa4qaggyudtgIrxCINdXWnwYQdBsM8Q4KF+WlurW2u1zqoG57Xd7tqXT1/Z48dP7bQ9ZNaOx
+ iaayLLuEDCwN8HjgbEqnd5yLmWtMhrsss3GeYF9Qn84tdNOz9rdgZa6sJnnrvwEEVdHzWFSp21
+ ubJr6+j9VLxqCKxeztr7SsHq1ZEevX9vh4aFNMG2IB83m3RpPsLO2umK3schld9tWWjX7n//4D
+ /b65MQKlao9ffrSXr58zbkBqrMS2wPHbFQdCGrJJCgmpDFFO7VSo2DbVzbtgw9+YI/u37fNtTW
+ fdv0/B+wTmjxko0kgkkx1mVpakC1Bwoh3F/fOvXuywHa+Xvi2APYF3nnZFkqeCz8Sz/kWgr0ie
+ zBXBDzn7Zkp2czqxbzdvLRu19dWrTCe2tH+ge3v79kJOeops631zZZdvYbFFFc4vIPBKvCuvUH
+ HxgNk9uApZzbPpm2YytrJYGL97tAwcASpmkpgZLEapwYtUyiqhMVwT9bBXtYLvuzBxlwUsv/sB
+ a2JK806uW/yoXDiIiUiZ0cqP3CfpXxJOZQofqPiexymcbDHhcN3Ti8W+b8A+Dg5Cg8dVA60g0D
+ 2rkpC04/is5kBg6+HNNBpBfwb4qPg49mUdTdObo9yv5yYFQBdhfcN4zX4EOF9gUKARUC5VLZyB
+ cEQmvocwZuBEZUPx/FliYBmMMDeJjM2MafgbFPYzIRm9MyOz47t+PCYjcR+r2P9TpfUDd4PFtD
+ IkE3vkZusCPYjVjAj6L5nGYL9xnrT/p9//3+zef2Lx5/b6WBkkwxWD4IC0aKYUMeQQiMVZwR6J
+ BW9wci63R4N6jLQyYPCoerIrFIpMpmgiqlZtVqtYgVw7amMdTpD++yzF/YSQ22DEd1bucM35/J
+ cACArQSm0cAARIFv1kjXLvtCG1Q3WH06sP5jaWbdv7U6fdCAlk7gukyowoGRqGW5KY66e2AGEv
+ h0LUCBZvbTatK2NDTa7nz55wjWHAOytK9dtZW3TXh8f8NivNGrWqJXt2rUr1mxW7b/+j/9u3cG
+ IjffnL/bt5d4+l8tDUsvAgql3Ntw8T0X1xGAk3xg0YrO5lKWKKStXC3b34W370Y8+sAcP7orGX
+ N4Z+6fI7JctEaInGuqgRC66nOmfb9iKY4sma2ymkqOs6MyoBJTTs2KPD3nBRNw4J9sUq+GrIv3
+ H3qrMHhyirI19PykOIiVKUiWg4ITCYKdVt9sbm7aaK9jwtG0vX76w16+lIy4W87a1dcmu37xsV
+ 65esZWVNY5/97Gir99mpjiDLhvgafCRNzvsQiM9p3IAS6Y5Bs7yXkoVgpc7HoK+yQRn72DP8zU
+ d2cnxoe0/e2mt1ZZVV+rS0XPp6czSvleV8IeKwc228PjSLWtIBZFc+0WVKaA0Zt7m/B4BD9kfM
+ 15l8mHfrO1OKK+RBQsYwxIBlQ2Wo9A3nrbJmtyUmggBxqWFfgHSKoAGcwJ4ZOGsbKA08UCB9w3
+ FRT4PzTeAGxYKUEQVueWKuT2WfRSzVq2UrXdyygAFsAcAywURxm8DOz49tjPYIPR77CtgUG6ER
+ mwC9uL/9foD7If0oB/TITNts/6E6pO//usfW284ss/3DqwD99AMpLI45tKH8/wmdIG83AHECGL
+ UqiMfQ7QdT+XtTs8V4zAVJrlBnXCFoPv749qAKufo6NQOD07trIcGsiou2HUjCOL7hEPfYpUvZ
+ K1erVizXiJFhJ+HhxGa6wg4AyQgQwRpVQF4jWAJtAxE1sBaFQLO3vc0axeYNyy1FhDXFAB/99K
+ a3bt10yb9nn3+2WdMjvBYP/irD+zWnTv29MVzOzg8oF1yIZflvYPj8V//x99bfzK1Qrlmey/27
+ cWLPQVC+ZlpKjSOJRVGPqHOXa4Kpuw7ZrFWM2PrO+v2o7/+gX3w4x/IbBC02pJdwR+bxkmGl5a
+ A/mKDVmB+kdZRIrrYDgPcXjRYRQN57+ocuC/9DNcZCuyjORzgv6zGCVrn7QJ7Ni3sHNgnkY+HR
+ BLHlXLe7qxv2uVm03Kjib16uWd7L19Yu9vhgM/O7rbdunPTLl++bLVqnTdSp3Nig/4Jtx5Bfog
+ seTgz60zMDnsTy2TAQdfob0JLZNwkADfn7Ck/hN4cvH0RNr2YwpQzoCYkh3bwct9ePn9hG5sb5
+ OxxowNEYSCWQimOaoJmXw425DZd3wzvGm5EgndNWN2qFKfPuHvUB/8OSgOcNXeRwgMHpm/YWDR
+ WQ1Y0jsbsAfLM7rHIhXYCkmzGZirFI1QHkt9RdphV1o6VguhXxECQfkaghccHDUWKGsEYfjCgv
+ bKickAh4BjkK0VbW1vlCsfpYMRm9xAhDD2AyZw20WedMwK9eHls/ILCRiok/B10FZuyGGIit+1
+ gjyE3OE/2J7aztWk//NEHdnB6antHHcNa+CGFJ+4aST5Vnv44DxyciSlPTAXTuTPPagzLbjBJS
+ 8DkPlYcB+PXoZrBsaUdAv1k0jwfbLAOx1TywFBOe2tFIfmBo4EdFGOoFPA5rg9UKRj4oy/SEFv
+ VsCNYwCkpzqJJ64y90wXCHMGGTMkI+HxPyJzh3p22aztb9qMffJerNKHKQY8LPZ3NzaZtbq7RW
+ ZV7Ffo9q1aqtrl5yT57+sz+6V9+YdN01orVhu09f2kv9l7y2EQlgUa/PHIyliO4R7DBQJWsHQi
+ omZTlClkr1Ar27nffsX/307+2tfWWFaOH8idq0ArsXZGW/H1BjvkGsNfJXAyE6Z/LYI8v+B5eB
+ 3u5AyyDvcQY8fF10svk+28TZ8+ihRWhMnvBijdtfNAKOuVSNmNX1lp0v2zm89Y+eGV7z5/bcfu
+ UvuM7l3fs3v37trN9mWX2oHtmnfaRDfpnNoVtANwlJzMbzNLWmabtBJK5YskaNW1T4tIO7lOFl
+ lxZLQAQTTwEA2ZyPjwjHhZgP7JXz/ds79kz29zYsNZ6izM7BHu8Cze1krOjTLJ06iVV04o7gb3
+ esd67/D7FBbKZ5oM9mKxk9kivHRmaoZmJTJ8KIChQMH2Jr8NQjNOx2r+rJeNxjDWeL826Gq+QH
+ YJiKGMiFu+fWbiMr7h9iSvwoBORqodbmBBgRtCfI5NEQxt0R4nDa6lCyi5tbdqk37f+aZvvvzd
+ Bk9D4WjHAhalmBi8GMNA2WstHwIfu3sE1NlpBsQLVEao57AHj8RiO7NrVy/b9H3zPXhwe2v5xx
+ 0apnE1yhHbRNczGcE50LD1n0wAd6CoHdyirEB9ioA5ZPKocNUXlc6TFOFpKQuUNLBC8r4JmuRr
+ l6oU4M5BMg6O/gUOKcwc7ZjTdEYxJsaGy41WggT9V/gELsgAXcCw2IXkk4bUWS7gRmbg8Ppux3
+ c0N+9H3v2OzEXYbd9Vcns2sc3Zq/X7XBmPsMYYpYMp2di+zt/GPP/u5vTo8skK1zr7X86cv7CX
+ nSCYeVDCPgL2y+IO5EymdvFus1+29Hd5D4MKyM7tx97r9h//87+3K5V0rpL1hnmjYv65BK/pEk
+ 7jnM+1l6eXCH19U7LJ8MX4v8Z+Jid8LYJ9k0jEIt6TISR6Dby3AXoAd08kJfEfFQmXaedmlzuj
+ 5SoD2z4tfVqBf+nirMvtFhPNmZUTEsFBw+IOOeK1etZub67azUrfZ2Zm9evHcXh8c8GLavXzZH
+ jz6jl26tGOp2di6x/vWxaajPqx+hwTIEZpglreO5aybylgFzcZSmWCvcy8jM2S1HP13rxvYK3C
+ Bhe+CZRAA7zyd2Ou9fXv25Atbw4j5xgZvHPCzvD7dOArcPO9Tgj1gHEU6nk8VQixoiBs8gh4tZ
+ 30kXqP4qk60Ag/0g5qX1OUT8DFEJQoJIARAjZ+n0yZvRFk+s0kJFVIOQ1YYfpHBF4BOem+FHrw
+ 2jPBrtyyqGlUHtAsgPQawh487rI6Ldnl3xwrlvHUmPStVCtY5PLSD589t2O/bcIZhHcgn8f6z6
+ qOgseyrBQkgqGoA/sO+TTD17JvH8N6HoD0GI3nhTFPaWzAe2vUb1+z9733H9g4ObP+4bdNMwSa
+ xWQrNVnq6KwALhCF/FChynyzBXFPOHGfgzgNxr6GWigGmCMryplG1g2tLHvd6PzEAlYCBh3H9e
+ 07raTbGfchvsUlKYADr61jGIgZKOtlk/oHXlUa2SECFPJNfQnaN3cE5W6lW7dH9O9YoFw1Duyu
+ rDSZG8L0/PT62/qhNvTMWs6yubtBy45e/fsyAXMGy+2rNXkAM8fIVgz23d2EpOY4bEzSN94eUk
+ pSYX+vw2AG1h/mDaWpkl6/v2H/625/ajRvXrZDOXaBIlsA+AfXIvAH4vqzdK4EIbHxeNow1nMf
+ ZkTeB9IWvx88GwC8CQgQUTzj9tSyD/bmhKYJ0bF9Z5iOWQT62FHhovqCrD1GKR8vFAiFdLG+ZG
+ udcHIvsdlHosGnhfvf1Ys6ubKzZrc11K6HBt/+Su2KnNrIrV68R7NfWNm067Fr7YM96J9h0JNt
+ c3OzjWcqGqYJ1U0XrZ3NWL5esjs1U4CT8xg5XTC7ygOGXD/9QssfM3hdOA+zheX94ZE8++YQab
+ Fg1QJ/OzNazFgJGzJZfAPvIMAk+9FARGMjaXDU8MmI5IcLPBNm08lX0AAD2onYAvFiyDu5e4/D
+ I4MBhswecRhYvoEfjFGCg7UvaLgX6hcADXhq2CPC0p33ARA1qqktAc/l4fzpDu14MEE36I9okc
+ NPWzGx7e8OuXr9ik+zUOt22Pf/8c/6BA2S2iGE2bOIaWCZdUHUBgPVVjRrHx7Qv6JShTQaieOR
+ FM+N0cBdeR/DwmUijj57FjRvX7J333rGDoyN7fdaxeSZvsxwUP2qMCqB1TeFYcdgJ1CHlor5Kz
+ s3qAASYUKU6ixu31CSW9bCbzONQOnXh+qOEr5c3joKx368J1JMOQzBzD3t+P3pVi/lW+fL7ZK2
+ uhQB7fx/q5LNm0XCc2w4zmVS1iOBdLRbs0b07dm1n22bTgaGFUK6UGVBRBfYHGJDCbMbM+oOJf
+ fTJ5/bq4IhzB1oYX7YXz1/Y/qsDXle4DjCUB7An5YWgjXtTGizKgqlGm8oOGrYThWLOUtm5Xbu
+ 5a3/zd//Rbt26YQVONfuEaDR5Y0jqjwD25xU4i6rhPOi/IXicy+yVHDqflqh2zsOZFDrLYWB5i
+ Ioh2xNC/Z6w5VzYeJtonAtYv3TJe9xl0iW/FWxM2l5p2L3dbVsvl6x3fGxPv3xig3GXYH/vnfe
+ s0Vy1cf/M2gcvaK8gsId74MTGaAymi9ZPFW2YLdhKo2o1aMmjA8/l3LJHABiCvgHgQ5UCSkOlo
+ zYZKeOdW7/btc8+emylfNauX7vCwCHlhVsiJKIF7ltkBhncp5JnzxWDKkiKd80bMOEMsHeOnBu
+ SyMGLQ0cmGWodevC4jJNmbJ69Eh5CicJrVM1SZ3t9mCrG+n1ABxO/CAh0A/XXClURJl9TZkNw6
+ 4MxqwPqxUdDK5eKtrW9aYVqzjq9tn3y0Yf2/LMvqEpa29zk9iSM8MOOGFO0CKb0hMdRAVWAxjb
+ BfkBfezRrQZdMYG8xHlsXdgpnXWuPPAhORnbt+lW7ffeWnXW7NgDQlCuWLVXstNvjHlVw41DPk
+ GOfjNQUxxSvZ/XMozllpgYkd+3Sk925XZfQIrsN4FdlpsavBtTiuC+Of3LInKKL4TrQQHicyNq
+ 1WSqCvEtwGQm82g0LhaVBOIWuhSpEHLT6EaIGU1xQ/t7Du7a91rIvvviUbqJovJfK8DzC6kZId
+ iHBndvBUduOTztUbEEmW+XawzyTqaPDY0KWXE8zlKZqcNCpN6ed0MdQXwX9sCyXAWHDWaGctTs
+ Pb9l/+bv/165fv2qFtHYA8L9/S7D3av3NGf9FMP8d/s0EbsHZnwP7JIIsI9o3gT0SwfMqfNE4S
+ 5z+2wz2fo0vvd24+DGMMrfVSsnuX962q+trNh8M7NkXn9tZ58S2d3ft/jvvW7XetEHvVGB/dGw
+ TB/vJHI7ikF0WrZ8u2CRfstVmw2pucsaSiUu40ZSVCRpAiDtIaVwlXp1DOhwMEdiDX/70ow/Jz
+ 1+/dpUXOG1lo9Bm5HZihmCvxepaFRgcelACUQIEpeXZvksjpcUPYHBu32kEgdCi3JeMi3rSpNE
+ big5tdVKwYLKp9DH5ADDTjRPUjr9fqmi4slCLMDAgRFXQeCaOP5ex0WTEY9pcqWFVh520j+3xb
+ 35FO4lqtW4379yx3mBojz/6mDJDSFsxWQmZK3hsWEzM4AnERSBYYgLjMZmKobkMbrvT69vJadt
+ Ogd+wSh6PbHtny27evME+BeSUzdU17CW0Jy/2manCl16TtNL/83ai6ZmWlZAMCbD3XbSkcRL+V
+ qAOtRP5djpY6tgR7JcCLCmXpP+y8C1XH8anlpEMoFJIhul8T3Ionpbwwh/OqRwlBmamAAAgAEl
+ EQVSnAngNoG+ja9bVi8SIUMMQ7MtF+967D2xnc9X+1z/8g33yySfcXZCBZ4dz4Zjonc3S/DobC
+ tm0NVZqVq1X2UhGYG6ftF1aiWY8OuMzDbwNx6K+IDBIp9i0BtCjV8TrhYN5eavUC/boe+/Yf/m
+ 7n9ru7q4Vf6/MXgKYcIeUC60AMuHxPS9ZUDrLdglL07lfoXzEyUeQXvx+BAJPjfxnGOsD/L3pG
+ zVXPAjnL5bRzOdo4hRfzOwVQOLP2zZUxUs+FvdezPMT6pj0Rno2s1o+Y3e2L9m9K7tWTKVs78k
+ TOz5+RSXBvYfvc4FGr3tix/vPrHd0aNM+AGNsBPt01kbpkg1SRZsVK9Zq1q0Kt8skmiKTRWYPy
+ kaNSywsgUIn1BHieGV6RTplOrMvP/2ELo27uztWrVUpPWRc8KaakyiasIwl0njfrsBRVicu/hx
+ QMCCI0mFg4eeeAYrG9IxwATj8ffctx2vl6D3oDGaSkhiCrgEHjpI7LkRRNKIuYqRcWnvw8Qo6o
+ bog24KbGwoN/OHdB5Hs1GqVshWLGcsXs1xM8uGvf2XHhwc0Kbtx+659+eVT+/jDj20yy1q5UuN
+ ayUJhsfAEAJ/m9O2MnD2a4AyXsGCYzhgsTtttOx3OORQFawP4/IMegCQT9MrG9rbNUzn7zSef2
+ cFJ28DWL/xQpJSCHFQOnn4zUpopF0ZuhmJ/IjhpXZcJncOpaIE17SV8qT1mNBbnx/s0TvZHs5i
+ +9b53lh0cP+8MGA7+UUXxfHkQFm/vTUFfLRkqKb8QBHZofqIySaesXsnbB99513Yvrdv//O9/b
+ 598/Bkz9xnVg+65j5WZM3ggiaZMwx6iWeHQHDL7/b19Oz1uWw7KA3fy5OwGp5uR2cf0cMpGNMC
+ DGkkDYUwYIMFtlOz7P/yO/c3f/gfb2tk+r8b51pm9/IKkaUfQEtgnnP05pU1UPuebtst2DUkz1
+ 7vpIbdc/rqqpjgrPiEb3fcE7D3CBJ4p/Hha59Dvv8OmrkeUN4N94OBbx9kvgX0cr3OZDbUr/A/
+ QVUqbXdto2Ts3bthauWIHL57b4f4LW2m17Pb9R9ZYWbVO99QOX35p3df7NoUtLVbZYbw+k7dxp
+ myDdMnS5Tp59gp2iLpULDizsPJFZi+wl583PsLmQNOpUzIzr54/s6ODA1tfX6PeHk6ZCAUB9tG
+ iYaMpGYTRftoEKNyznHSCfwSf6wmka90XjexF827haBi639ilCYDkqL43dkU7iAKRukYfLM/d4
+ llgH0uoRVvhBxkE0+hVzG2E+gWUCzPkND1VBuOBtU+OrFrJ01Dr4OClPUbVMze7eeeu5QsV+8X
+ P/7cd7h1YtlC1WnPFiqWKbACYOY9s2O/aDD726ZnN0ACejDRtQYM1szE8//t9OzjrWyZf4IQ01
+ lIis+fegtHQNnd2LF+q2S8//NhOMEiEYS8Po5S9ogqilbP6EcCbZL8As33ZZ8juOuguZdOglFQ
+ ICeTDvz44ekpyMUSFBqz3XZKs3o+2gF0TzirGfMmN79FNMlSW+EtVV6T55Mfdw8iDFa4l9WdkR
+ 4DZgGalYD/54Du20Vqxv/9v/80+/ez/Y+89nCS9ryPBV953VXszHjODgSFhSFCACIIgQSetVhd
+ x/+tdXFzsxcZqJXElLiU6aUUDEENgfPuururytZGZ733fVz09IAhdKBTUDGICPd3VZT6Tv/fLl
+ y/zE07qYtgM2xbuBPlcAnvaRefn1gbYt5eoVnt8f9cO9w6tQIZPDWzcj7TgcLDHjgDPhEEwzAi
+ IPsRgVZEUDhaPt997y77359+2jY11K89FC34xGifcFtI4QV2rn79B+/k4+2dx+Rl6L+HsY+gqA
+ 1wO5gucfTR842rkgNoijZOlcAiHf1w0TlqxJIcqI0VCA4SDQLkZJ1DL85ltd5r25Vs37cramvV
+ 2d+3Rg0+oD79551Vb3ti0s37Xdu/fte7jBzbtw/FxbGOwxcWKjQo1G+brVmy2Kbusw80STpDhs
+ e0h5WxE0TJhEexxYeGxAExKF+dzBos8evCAxlaQG0KTz2ZnIiuVoZXXhz7mLptk8fGqFAUmocU
+ JHth/S3IeZYhyJ+Q7i+D9w8KW49tBIplyaSmfDC7ZAzFIQ6XsDbNqaWgmdQXpDfwfnkG0U1ClF
+ rJTcO80ZxtNrN1uWGOpabuHu/bk8X1bXW5Zu9O0u3c/tIf37tva+gZptnsPdu1//dO/WG6Us0Z
+ rxVrLy1at1rnoSb00Y+DK4PTEijhfY/D1Y74OjdVgLIbYx7nZ44Nj6sQx+dpcatuNGzc0WTsZ2
+ 8r6pjXaK/bzf/mVHRyfovR03Yq04lBRocTVuaTPakZ9pCQyyiQpzdREKIBO5ycar6rQFWyvHR6
+ 5YYAo1UoAe01XqlTRAsFdUewKA+wTSki7hZSOCPleWuHrKnAqj554ojA4JIeyHWUvn2Nmq+2qv
+ ff2W7bUqNlf/de/so8//pS7Q3weReikYRpo2qN5XyjnOCWMrGXkKQDskdeAXTWHqjhMVWD/BDQ
+ OJM0uJqa0OYbfIFPGEBmUWasbbXvvgz+1b33vPWsvLVlpJkr0C4O902tBeXw+sFclGVW9jtE59
+ U7C8TvkXvjzCzj7zO8twv0iZx9y6+xjvHOR+VbK1//xgX2GK34a7HWCcJPhRsLFVrSZrTSq9sr
+ N63Z755LNTrr28N7HBODrt+7Y2vZlTpo+uX/Xjh/eswmGeljZwysFlT0atGUrtZats7xsNcQUU
+ muNikgAixPANCY4IoKzd109lTjU8WoqAAsQwH5wemr3733KSv7y1cuMdiPt5C4Z5H6TQDIZW9G
+ 90ENFoGJgwy508Kg8E04/pW1SiZ6yQqHOiUYwK0rxN17DplOykZrF6U7X+svjB4MvmvhD9U7Fk
+ Q9zBZVD8ze6cQocuQAQGERlYWBqqVOn3/ze4RPL5aa2ubbKwJFf/vKfGTsIIJ7ni/bP//wre/x
+ w11rVli2v75Byg1cQqmPQDgD7Qb/LhCubw18IcwNSGJFrLxSpEMHMw/5Jl3GG4O+brbZdvnqFU
+ tJ6o2HL6+tWb3Xslx/etU8fPKGKRzsbHGMMuk04pR2GaASwjNRUVtCSoGrewKWB3hiPLb4UUaq
+ wtQio2c7cBBrLif+NZDIndjjFHbQOJZVE8LQ3lfK2kt8tNFT8sVx4OAVM8okGawrj0Tmaz0a2v
+ d6yb77zFhve//2//Y3du/fIA0vgrOMp615r4bnQd0EyV7OlTGcca0yHw1sHWxC6XUCVZUX3KEI
+ Uo4M9dnucP4CnJhRWeatUy1Zplu3StS374Afv2zvvfJUTtJigViEe1fnnkV7G78SgWbayl/wxS
+ 8EsSjATeYHHMgppBPZZisar+QuoIF8qvCeebY7rHOv14j1l4TzTcF3A8XhPixy9v7MUCv9oKnt
+ 6liwG7Ea1oQPooAie23k5DCYt1cp2+/ple/XGC1YZT+zBpx8yyOPStZu2dfmG5XJF23t4z/Y+/
+ a0NjhHnN6IxFaLpRvminc7zVlletdW1TauVkHgkx75Q5eBrDAhhG0sJHpUcqgIpMQyOvwCN+dy
+ mwzEr2O7xMRtQcIakYse32LKoFdiwrqP/BZqCAtHggoMKwL8BIuSDyc+mag1MbVGlA7U+71ePf
+ MMN76ATOnxVl6gypcqBvh2fh9U6gcJpqVBwuMUzKQ0Hdc824iJAsPfqaI6+Bc7JbGKWG9tJF/4
+ wPVtfW7bV5Y49fPjAfvvb31izXreNjU1OYX780ae0OFjurNnyxra12m1q4Kega6ZwCkV4SU8a+
+ 9HApvg3J4QZRkBgQ1MXvvrdwcAePXpke3v7TBnb2tmxYq1sV69fteW1DSZW7R0c28e/e2C7e4d
+ 2eqasYZqLEbAEGrieZFqtGx/fw6ekNBXDYmWAvh8P9jrQARDIyg7adfDRfHV6ha/jzbYU7HUPI
+ 4kWf1Og93o9Ch+Xg2oVCIouAyC+C+SIG3doiML0MQ7tJ81mQ3vhypq99/ZX7Hhv3374Nz+yx/s
+ HNs/hlRGB6cgTWAPapQzTP2Q5N2hdjb8P7t23g909WXvg3eA9QfYKR9VwwWToztyGuG79bWJnz
+ FSxpaq9+NqL9t3vf2Bfeum21VhUeZGSTNBmgP9C6WVK0aghHRPo8Xsq1LRT8EbtQvM1reB1RLM
+ A/zT981k6/ATY/Yli3kELtBq8KTERr5uifLa25Q7cffAT+ibh8/2V/mjAHgfHjfjSS1lby7jMU
+ TmnNZNuzka5aC9c2bLXbr5oy+WKPfj019bvHtnmzhW7dO22lSotOzzYtce/+4319h6aDQdUHMy
+ KZTsrFKxvOWuurNja+g7BXpPpLmZjd3/uQdoIHRfYq+uvhlBMweUKGK3P0bZg//ETUjmrq6u2t
+ bVl5YpTInG7ZsA+iR3GlBPBAvbLUR1q8EeqD2WForIlDUCfl0K6ANATRc2npMp0c7OI00OFqUY
+ tdOljap9RvYEnLtCwCxckuGnZJYii0uIAKodtXVgMuNsnQ5eYFIU9Fl4YhA6893s2mQxta3PNy
+ qW8ffibX9v+/h6BHzbNd+9+YqfdgbUayzSq66yske7ChAOqd7iHwtaC+vohbKkHzKGFZw74Yfx
+ BAxGeRXAYHc2ntn9wwOoe7211c92qjbq9cPumNdtt2907slm+bIdHPbsHVc7+YaISoR+835I4m
+ gnY+zAjfiYay9icB9jjOBVhBcGFQnpo7bTcmtq59ZBgcpF03YEonDC6kxMpIBeYx2s7qe79qvf
+ p6rgnaCyXQQkpslRBy0NJOz0oaxR0h+tkbK/cumRvf+XLdvfDj+zHP/qpHZ2C0sJro+/iz06w5
+ 9ABgR5UHsAeoTStRoNFzMHePhcm+FdhdcthoG0ytRGkm2P1MAj2vstBUYQp9kq9ZLVO3f70W1+
+ 3997/Ou0bkGLAvUxmIlYgmVIsKigWAT6t3FNKNEvjBMIqGOgcPZOhY7I1d7ayX9wJPIPGyVTmC
+ 0qf84tWRn2TWGb4Cz9NZHAJTU5G2rz9IwT7rOTPET4ZEg8XiWyjEtwhjNFg8vTm7Tt2pbNijx9
+ +bPu7921ledWuXH/Jaktr1u2f2uN7v7Xuw09tDipnlrdRsWzdPOSXOVteW7H11U2rFtCgjQaLb
+ ydNodqgY5inyV2FV+oAQx95LsDSFnvb6dROTk7s3t27pHyuXb1mjVZD1sAul5SsBkgZJZiDaOi
+ zMwM7pAYc8DHwQgqCInC5Cmp6FoZbvmV3WSD19t50TTb/DAsvuWPkhNbNaJxRZlouakrPOXqAP
+ e142SPR52UzkTyvS9y8p5EvlFhVQumE9WY6GTLoY3NjzY6Odu3ux7/1mQKjkdbB/onVqku2trZ
+ tm/A2bzbJw6N6N0zVAtSpqx/YdITJWVgcY/BHFtX0tMnnrFpRZZ8rFe2k27V7D+7b2XBo7eVlW
+ 1pu29alHQalIKIxX6rbYDixew/3KMGEdFP0WrqtB9AH2HsP3idD/bj41DA15gQoLZ7RVgtqhs8
+ IpU0sANrDJRx9AvZoiMI+I1uo++N8fCqhBPgQp3EyvdlkxycBKBZnHGrvwZAiyVHN9MaXbtmX7
+ rxgv/jHn9ovfv5LeeEU8K68svchL+rzAfaVCmlLDF5lwf7QwZ6T4FQmKKAevvtQ5PBbmINgOLr
+ sFKq1itVaVVu7tG4/+E/fs9ffeNVW2y0e67jPE9rlDwR7LFiJGicGkbwiTsE+W7EvVvZx6P8Qs
+ I8FKfndZOeQ0jSxYC20XEMp5L+4GFyCM+47sVgMMkodvuYfU2XvhGWmsFeNrQlpVdvRshSfPrd
+ y3mxrddneuP2ivbi1Y4cHD+zRvd/S/uDKtTvWWNuygc3t4PF9O/r0ExvuH3CbOyjX7Rg73ELeN
+ tdWbG15zUp5aMm1fefzYxuPkXD3xKEc0bW85HhZ6Tv4o/IDAlCdMLRH9+/Z4cGB7exconUC6JK
+ o9LyEF9gzncT/8oZWdR/crOyHBdygHqCc4U1Gnbh7q5ATFthrTF+ul0EFsfJ06gPvAxcZFggsX
+ qje2JQG9554hQjcgC0KohYX7CWY+Hzva2AXAImfdh0T0lvwHwqr3E8+hcPiPm0HnqCB/vCx5XM
+ VhsBvb11lZY4TO0BCFeIDx5BYYkhnQHsLVPYyd5tSy8/hLd+xQA6L0I88fOiHA3vw8KEdHh9bF
+ XTR9qbVW00r12qctO0PINWc2OFxz07Phs6vw1hP6e44c6irUN3LLVLHgCoqWiM7fVPUMBHl6WE
+ Dk2jzVJdFs49N6+RMZsDeeXxy+t53XXC+9QZ8yuTIKkOA7w3ekOaC1sMug/0BPC7mMfzemSGLN
+ mfv/MmXbWd92X70w7+zDz/8lHYTU8bBKZ6Qck5tBgj29ICqVqxeF9g36w36Tx0F2PvukyZ6kyl
+ 7IZzcnsMiREUJiwgEw1dLtrzesZdfe9m+8/0P7OrVHatVSqQ9s1V0Sn98/spe1ufOkcegk+8EI
+ kwldg76f+qfnwBNKGN8N6edw7N09unwWtwPXnc7Wx/DDhHmklbqxIp0hdC5clzTrX/OJfMc6P9
+ xgX2s9Bm+MjUQErhzgCnKAZsx0GS13bTXb96yL1+9Yf3eod3/5CPaIF6+ct2Wt6/C5cyODw5t7
+ 9NPrfv4MZtHo0rVTnitF2x7fdXW2h1ueunr5QoHnBgAISp7VvW8WtxDhZ4yAEnRHbiwAPaE+/n
+ UTo6P7Lcffmj1as1uXL/OKjp572HoxOrIR809OERDMc7sokqPqVjXcIu/F2+PSVKacGHnTcsGu
+ ScyFNs98KO5ygEoaK5LJbdLENeMIShw0eF/HjcGqQA2nnUpkxKgLFVB2fraY90I9iwp5W0+GxH
+ sJ6Mze/zoHuta5JR+8rt71u+PbW1ty3Z2rtjKyhqPH/yKRqMB5xPQlNWkLCp9B3xQOvCvd7CnY
+ dpsRhkswJ5Zwja3x4+fkM5Bg3FzZ4v5vDCvQ07HwXHPegOEmcOlUusrdi2oPElHAOhzGtajHzu
+ 3/+H54lU64xxRKefpJMlRBA+ACboh/GEIXA72wmhdtYl6xv9NoI6mQRzrjPdNAEoodxhlF7YJf
+ qn4WJfnHkRVr//jZC+3G/buO29YKTexv/lvf2MPH+7zGLBX4D2nAHsuYFSfobKvMlSlXq9Zo16
+ 3B5/e431Ezt49mVjJj6c2GiAUZ2wjXJdR1fP6KjCt6urNK/b1979uf/K1tyjnxPHzEZR/FY1D+
+ XJUwO6NcxFnn+XRE/hISJNMM3UB+NPvp7+fcvwpyEcd5Lu8hLP3XYTfQqmxma4tlRhqatPG3Xd
+ i53cOyfrwR1XZXwT2Xu2qDakDTS6LlAMUMDNrNyr25Rs37M0XblFaee/TuzY4PbSd7W3buHTNK
+ kvLBJnHDx/a3qMHdgZeOJ+33mROwNhZW7flBjTec1b22MLHzQmNNcCe+nL+ScE+bI+pSGF8nU4
+ eQADV6Ee//tBOT07s0qVL5O8BtOTa4f/iFwTTpjBlyDgjrfvsTLjaA2COm0n6clVMMT0bYSbk4
+ n2KN8JLqMhxOogBHXgMKlKAvRtkqfKCH738SWL6UxPBTj3wWKhKxO/KB8YbtA6IUNfIYVHHD4x
+ QITezo4NdGw37pBru3XtgB7sn1lxasUuXrhLoy8hwBVjQy2dg4+HABn0Afp/n0aZDTs7CegHHC
+ Zp62DgriAVAXbR6s0FQwus/evLE9g9B0Yyts7ZszfaSwbhulivacffMesMpp3WxI6IqyYO5uSN
+ hmIzH+mYqe+xIcHyws4CtceT3grEj5Yfr0BuBVOz4zo+nkqCn60k7NpfIqiLguQa1DrDXwunEP
+ nZe3oyPUlDnI1XqxPUJ6Wlw9vq5ksbULFY83vXrl+yrr92x/cf37Id//Xd2fNRnzCDAPqwWsmC
+ P2RCCfbXKiWaY49WqVXsIsD+EXQJyCNTcpsYfLrIA++HYhvAc8usOoS/VWska7bq9/tZr9q0Pv
+ mk3bly1aqXoKZFe5/4rOHselQDoDNjzTv2cnH3gytNc/bP09bEFECJ8Mc5e91SWt1+kfPx5k+3
+ HHxuNk9n0ZhsVQe9w4MXBHlsgVZETa1aL9iqsbW/dYQX+6ME9O9p/ZOtrHdveuWKNzoaNZ0Xb3
+ T+0R7uPrDfs2xDSvvHMGpW6ba2sM5AZVTZDRdhwE6OmcA5ZJsSWHGAHPp7e9vBtB0UDfbgL6xC
+ 2jJt999Eju/fpp1Yplezq1WsEJpi/h9kXb3ZIAaEb9x0L1TdwYyRKqNmqBuuc7pUxUMSiDZrzk
+ WxxuVAV8owtxONw4ZDugJQRU78+BIZKFwBB4MbN4QsAgJNpWLBBDvtkAJoDOT4QtvaanE0j9gR
+ g0J8jT9esUs5btZyz7tG+HR/t85gc7B3Yo4e7Vi42bXvnmm1sbFGKx9B0NChBq1BxA9fOMw5Tj
+ cnXD9ScBa0DL5xej4CPgR1O7ObyVq3XrFiGiqpguwcHrOyhDKk16swUwFZtMitYf4QBn7wNRqg
+ 81fgGuHKIyW2osVChScypWQdw2PbCwRNN8uFw4NeIOHsGdWBXVXQZqsszOXwFBPI5iVjCk5koV
+ nG6ixOwp5TXd4YcbhKIR63Da88r+sg7wPcYFu66f1kweKwkf0GWzW++8arduLJpv/ynn9g//Pi
+ nNhyCPsoz0IWdh1AROYUJawMAfRkgX6vx88PyGmqc44MDgiiOE6SWfG0He/jwE+yxw5ubVYp5a
+ 7Sqtnl509779rv29p9+zTpLjSQMJmlYfkGw17bKlytuOTmSl9CNzwb7xeo8KukU9B3CKcK4qEE
+ bpfozwN6L0kS774AdCr/4LTbTM2COg5bC/9PDWX8QjZPIu7Iv8O/q6/P9aV/5/dsC+9Cv4MZg6
+ 99qlaLdubJjf/Lii1QN7D1+bI/vf8Lpza2dbVta3jQr1OzodGiPDw7sqN/lwAf471atbmvtZWa
+ MIvGJ6gxxKSyqoKQB2CNtKLbgmEyMio+OmKARyPuKg0SjFmDR75/avU9+R7nalcuXbX193UqNO
+ oExFAXzsYZRUDHj5gbfDqUMo/N8WjWmKpUTK9qGw1FQ6AD4EO4xnRDAAb54HIAMzwNaB54+4eG
+ D5wQGEaDcGA2/VyhAQ4/Q8rENEN2I7FxOkCJXFlPDBavA79zVSDCIw3HC64Nr5HEoIvC9YPPJw
+ B7e/8R6p8c2Hk5pizubFWxn64qtr29bo9HkhCbf+2TEhiykllj0MDkLJQ6qfASORyALqBtMyJ6
+ hsofMz6tXLmToG+RydtRFWPkxnUaxEHdWljEObEMYfEGSaAU7g2maTxJzxEnImZh7kbf3yp5JT
+ FCSg7JyGwt5sVAn56157Og8FQrUBI3HfLLS7Zq9Jl+803wXB7DXOINiBwW+rtZJBrC8V5V4IYU
+ 1hi8KPphFsI9NIa7eed6WWk37xttvWbVk9vd/97f2619/ZBPKM0XRJTMZuHY9PQzXSLFSpowVA
+ 4oAfCx6D8DZ7+8L7Ols6v0CqHEGE+vBXtvnDRCFWKkWbGltyV5941X71rfetTsv3iQFJjuOOI6
+ 6xy9s0CbN2ovVOJIlCY116EJqqa/DLjzutWQHkDRU0wo9Uf0s9F9SJdBiBX9+EYjFIXb/XqM/p
+ d2PGQBdCpRtJ5S1lFDB6cvpNr1kuIT9ITTOvxbXY+v4rOdJuLMv+EILz++64uxTuVRZkjdWZWp
+ WAoRe2N6wP33lRQJ3d3/fPrn7kc3nY9vYXOcUZaHStLNR3nYPu7Z7dEzJG7bh9WqJ8XC1Ypm9c
+ OjRpUhRUw6VPTht0DlRTdPDHtmeztuHX0740eMiQ9U3m43tcH/f7n70kVWLJdve3rG1rQ3LYfL
+ Tp06hU0afDMDKhugUFSQ4a9BBWoDke4LQ8ZGqfsgnsTBx1H7ugR8DtziQTw1uWNAdaGiiMgO4k
+ 9aJSVinCaDGgZMnXCfxuREiMkBQCCZLeReJ1weVQUdKv0AjCxWvhefEMajXkOFrtvv4gT289wk
+ nWntdBKrMbH1ty7Z3LlujDmWSYhgx4AbtPMI0wNsH2GO3BtoG/+73eqyoUd3Dk59hJWOyzYk7J
+ GgpSFKR2dqHFfJkzLe+srbGCMTBeGrjXIlg3xtiMEtWxzPaT+t+IwAx2QnNWal0wqed/Dytg0X
+ ZsGj36hy/X/E5Cs1eKAqQbpriblShp7jkhR1+U9WoZvM4pZRaJiT3uktyXdYZcxZq4eO5kYiA3
+ ZVbN9AqWtOaeK83rl6yt998w/Ye3rcf/t0P7cHuPpPP4L9GPovXnRc3+BycJTEal4G+qdcbbNL
+ i89y/L+kljgsoTF6WOeweIUqYWBd5v1A5zWZWqxSs2q7Y9s3L9t63v2Fffe1L7IuRviTmeTsyo
+ 2bS+ufg71V1BJDEYhCPkZggrbDjyzBF42Ls1b63WxMaONvQzWB7QqMG5mRln88C+2y1fl5nn3y
+ ahMN3kzb+wK+PRHGTfPTYB2gdSJmu/2hgr4oqwB7HAl/Dg/361qp9/eXbtrm6YWfdE/vd3Y+sf
+ 3psnU7L1je3rbG0ZhOr2O7xqT3cP7DhdGKgW1qVEg3QoLFHtS5AlD8Mm3YY9fbgElWBqsIYJs1
+ tu3hwRhYyn0/UAC5oLCYI6th9+MgOnuzyprly7RptY+ccbEIFHzxt+NjP6bMew1AAJlbcCNcew
+ otflaQ8buAhLgMuNDhZrTOUw90oSQnNuFjhD54XdATkdPgDzTp2LrRxRhTf3GgzoGDyMYFeFXv
+ JKgB7NJlNQSFYaBh64na3CB1vNqrWPTmwjz/6kJOWCBaBrr7TXrXtrR0qb8og9MH1IisX4R5oK
+ I+QRIVKHoAPXlyzBaBXhoMzKm2w0PUGA+46UrDX+WezcQrP/DMbYvgKzz2fWruzTBsGhI9HZY/
+ qkxQQr53IblUjlnw7pYpuXOdupTjeWAA0QetFhovdAV7IeA1gIMWVcPduZeHhJCzWkulO3fDwn
+ yHOe/SgHCxF32hh0VaWyzrUV77IMVaSX2uAjpPXDMrBzg2Ab3FLpwIAACAASURBVLQUfvO1V+3
+ mlcv2i5/9o/30Zz+3o/5QMZfYUnCwDBX+ObB3LxvkMeOaRXWPa/3hvQe2xwlaKLkU8oMFChQmF
+ nRW9uMRq+xarWgrW8v2+jtv2jfe/1O7sr1ltQKKpoKS2WJK2a1JzlfWAtHU0TK+TsE+qulAQ6m
+ lknQqDF9KbeHSTFfCJDr7pyWYQeNkwf5iGudZ9E7quLlYpMYCdm7CNgPmOpaZUt6LkOx3/k0r+
+ y9YsH/uX/tDK3s8MX4HPP219WX7+ksv2s7WNqvF+5/ctf29RwQXcMSr6ztmxbrtdvv28PDATvo
+ 9gspaq2nLjRZ5dcrwvDGJmxjGWAA0uV+WFFrhjVOaZjlHC4oD3D0qaJpKYTCK0aG6oeAKeP93G
+ CQ6YaN2fXOLnDJsG1ile8i1SkZRM6RqaLSlixgDRZh6JSAVZCtAW2EHBlTB7Cv4IgC6heHk4KY
+ xKYuZAs93bbZapGXAh5PCAaCjCUkzLyRgIUFqwuYmjeCKRYK9jgFu7KHoKvD+4GbLOVuGlfF0Y
+ nc//tge3HtgvW6PNBI8VXYu7VgHARgI7MZOBIsWI/zQ3MP/sSjAORETsqrwWXVTcQRaaWDD0ZD
+ acFT2Qzas3dZ5NqeMEHa6vf4ZjxFoHtBqkGAur67RQ2dILbipysdiyGsnMgUiFlIVOYGdNIikt
+ /xeQQs/xSukQFyKSUojpR/DMC1cMrkYu8to3M8xAa7hNICShsR0+jU3IamnHBxVQXuegSvFILP
+ EtTHFrAVtRHSuRYjILmN7Y8PeeetNK8yn9sO//Wv78O7vmLuMhYLT16EWIqLMLY8wG1T2uK7Ls
+ CQW2IPGwTHAUNXuk13mJTDIhzSgdqOj8YzZueMpfIvMao2y3b7zgr3/vffttTdftVa9pmlkl7p
+ GMzoWuWeBfcJ7O/AvVv8pvSP/fgf76K5xZsab6BnJpRiIRQVOZk+RqaYvAHWnlhJsDqpGy30SO
+ p5lOdJeQEb2mVA8KTySBM4w137aEzbnPxTYa5WVzjiaXvgaN+Hllba9c+emXb1yjQCEmMKHDz+
+ lymNlZdU2Ni9bvtyw3d6Z7XZP7Kh7wqpys9OxrZU1qyKhybfmyGoF0FCJQz8ccdK8OZw3Ra4nb
+ 2jfhoLzhkxNE6W4cdzgyR0Cjw4OmU/bOz21LdA5m2sMQyFw83WL4pLdHiF2LbGLGZwNeAMrPAS
+ V24wVb1SpuTlSl9BkxY03ZoUK0CAw5/O8aaG9OD095U0MUAKo4/3Toz/UOJiAn4gSKIL+8eoeN
+ I627shWdZdINJvzOeu0KmzKPrj/0D755IEdH3XtrI8EKiy067a5vk4FBjxvCPLYYTAMXYAP7xZ
+ U9AB8gD12KRikwpuADw7+jfcEUMfQ1GAkC4kwdTvDrgecPigeLK6YMXCbh6VOx5bay3Y2Glt/i
+ Oa19OiEZy7c7kjqahgCO3ZlfvOSt8fCX3QfIN6baeVNmWY0CRnNl2rE5dir3pIK2cy0KG05sJu
+ D1FPadhUKDvZhfxFgH0NZ3qSl2QYoHFb7en7KcrVHsUqxbG9+6Uv20u0b9uGvf2U/+vHfs181w
+ 46S3kB6P+oyeQ8C17T3cgj2bNDWqcTBYzAVvvvoCRPDAPbK7AWliOSwiZ0hJB7t+lLO2stNe/c
+ bb9s3v/WuXb68xes2jiUWS/Y1WL2nPMWCGsZ3Nlkq5WkqJ6p7p+6zuyYc64xdwoK+PgPQKTWe1
+ c/HIuJLwDk5ZvxOLBrpc5yTXmaqdQF+/Nzfd/qLvoFbBHuepOS9/htz9p+7RP+CD/x9lb0QPqq
+ oTDWVz9vmUsPeun3DXnzhFjlmDH/cv3fXuidHDM7e2NqxQrVhR2cjOxkM7Oi0a8NB31aX2nZ5b
+ dMa1Rq3wtS9e2UFkFdgiYI74o+oFG80UW8u978KRv6T6EIfxAEPDDpnOLT93T279+knNPxChb+
+ 2tcqqc1aAckZ+NXx9yS7EyaIKxaASlTXQlqsJi/eIiVIAL4OeOYilo5NU/KR/UCXPWJ3hRkaVT
+ Hog0xiSbDO82GWNywlaz9cF0DPxyD8rdggKZC/bEpKMank72H1kd3/7iR0f963fh4pmTLvhra0
+ N9kTK1LNPSd8AIBmQPkRUOEAUFT76C1LfDAY97s5Q3QvsEakIF8W5DYYjZs+ieof0FMdEWbbK2
+ R1ASYSdEj87poQrbIxjJqFHsFdjTDJFuWuCUhDwOgXDVkVqthXqHMxcCJAE9jFfoYE/wIkAXH9
+ 0DvEvLKyyUtBBD5mlKmPJVVkkJDy8c/x0ovSsA3/K8NaJBYsUjlf00t8DVGEhcs2+/61v2XzYt
+ //y//2/9i8f/cq6CISBOsmbzbzC2KUF8KpAYSwn5ggI9giUqdEbCovWk4cP7cnjJ9yp4jonvcU
+ pa1xzEy7wUPiU6mW7emPH/uwH37E33/ySdVoNuWsGTx/HxXcz3s508M9IDoPGyah1VJSntJkqd
+ D/kC493z5ysKdpCNb9I4zjRssCRp0Cb1dundIveSuwSeEUs7hiiKL2Qsz8nrXTaKjBGdii6XsI
+ 24T9QZS9eMTUGSW8cHBC4X75585q9cuuOLdVbdnbatQf3P7G9vccErbXNLas223Y6nFhvNLbjX
+ pd8+vLSEit7qHLIe6LBxMa4knjgeAjAV0Mo9Yonn+9qFvaB8FhSHVUZZ7HBi0aWwk/Ag5/1z+x
+ gb9f2Hj1ibuelq5dsaaXDcX8oJNTsFDUVXvbcx2SAH88HXhTVFGSIqOSr5Qp1+vgav49FA6Aqh
+ Q/G2aMSjyxVVb8EHYAw6QxN6GKdK1drDGsBEEiaCeUNEoikHceOAAqlTnuJRm+nx7v2yccf2f6
+ TQxsOp+TVoUpZX1uz1eUlK0OdRIyUBwyODUfsSQdBGQQFLcAadgiutYdCh3TMVMogyEzxmQcD6
+ 5+ByhHYc/eCLCw0tidTUgl0983l2efA8yPfoFypUZWDCpS2Am5yQOkkB3Pc+dKDtON8y7dd6hG
+ pblLnS97e7K5q2ppgn5gnhGtqAIIW6HheAX3w+zFpKxSUK0Kkl8X51y6BDpo+kCPjjJCPosrHv
+ 9FYrdu3vv4Ne+9rb9nP/+FH9n//P/+XPTzctyEWBg7wRFAKdnCLYI/GLAZFYJ8BoKcSh7vaue0
+ +fmy7j58wmQpZw5HghWMJ73pQjThQrdUle/OdN+x7337fXri8ZbUqZjvkA1Tgfzo2MZinAjfTc
+ A0Q/QzOPtv/0AKqKjjL2XNy/N8FZx87kCxnfw7sdTF9Zpn8bwr25xsYC+8sDJy+YFXPFcxXsqR
+ ETdgqHSz9MxwlFsG+XS3Zq9cv25dffNnWWh2bDof25MlDe/D4AVOLVtbWbWl5zXrjqR31+tYfn
+ BHQO0tLrO7r1aqqW9AKniDFmxz2rBUYpLlFrXPkOC8B9nh80S1hodTGDY1mL7X4MbFKqSHyVMe
+ 2e/+BPbp/n+ZjW0i0ai9ZvlKXyVjBLYsBcN4M5vAKFSRqigLk8YeNSDSoUY3hMVCwBIfvfusAI
+ apzvL+Ao5ZYMKAHgN/lrkABGrg70Kzl93Fj0hVTjWjQPXgf+Nwrq8u23F4iLfXp3Q/t4MkTG5y
+ NbDCY0BALKV1b6+vW4Fi82xFwZy1tMYAJIK7+hGgdZMKywh/07ax3SpoNYI/FinYJAPOzASv4B
+ OxB2yBmHvQV0quGE4I9d0VY9CZjDl2trK1avlC2096QOwQKJzmrgU+phT0AmHRD0Awe0hHnmwD
+ nUsmgE+Me9RrPpZkJa+N9FVX2quo1lEYraUoRtauCpiY4bP4rekTOOMnWwGW3EejmxxMHFd5JA
+ HsM8X3nG+9au1Ky//5f/4v95Gc/sR7C6Glmlw7MkezyhgB7mfhsON/0ny9ReomFA3QmdlC7GFp
+ 7gkE5LOZQpGkRx9wCmuagDEH/bF/fsQ/+/Nv2tTdes/V2k6HkqPiRaMadXOwqE5vwZ4C97560H
+ YqFNQXJxSrfKY+E9vDKPmmepw1af7VkJ5GFrKCSHJ5T2MkYqMUuIF4/dhehxvG3m1BUaVP5s8G
+ eNtWfgZ//pmCffIgL3lBKqnxxtH9a2nnRR198pdjC1csFu315096485JdWV7nwAemKR88emjd7
+ jHBp722Yb2p2d7xCZN1APAr7SUGl6BiRvUIIAQRiuflxQywx4QplDoeKOKKLr/YNalaLMP/Gw1
+ MVVjQ5VdBARVL8qEplzmeid/FhOjj+w9J6dTqNVtf37Cl1RWqXXCzoXqTqkScOiWWE0ghpdoAr
+ aHqWB44fAylklDcjFU9gptlZevRdx5uQc8c38azqete9JhYZVAhPXYwN4CeRZlbeTivkrZBg7V
+ Q5CzD2nKH/RA0wh89fGS9UzhSygwLi9zq6rKtdpapbImgC/EU4e2DRQdUDpqzAHMphdCwBOD3e
+ 13SOfT64TCZko9AIcGHBQ1ZVvawvpiNaKk7nM5tMJ7THoHnCr83hu3C1NY2163VXqa9Map/Zp0
+ haJwe/pppiOAL9imIfgIn8fBSzYi60E3L6xX8Mylvp20czPnBfcqU1xNX7qBwFHoj2gjgL1URf
+ 8GtE9h8pRVGeEI57QB5sMcXJnw7exDg4ufWaLXs9de/ZC+/cM0+/tU/2f/8Hz+0x3uHNsnlbQp
+ 3Ur7OzCbwVvLuF9Ux3LHoL+yNwdeDpkNgD0PlZxiO22eDFg12LlbYDWGRmU4p18X7rS/V7NXXX
+ rI/+89/ZjevXrYGZjzwilpVw9E4WRDDqfU8Xx/VfhRZT1fyzrGfA+FojKodoJ6AnjsUMQlp5Mq
+ ocyqY8/x88uMoNjP6d39u7R5welNOPoHteD4+j89fJLVr5n1p47agv0zQL9oaV196JUG/36eD/
+ +Iw/O//N7H7vLLZsbdevGO3ti9bMVeww+OuPXzy2E5gxFXIWa2zbP1c0Q66oAgm1qpWGdO23G6
+ zmgHoSM6oNpfUOAVSOTH0BIJSE7CymtW0LSZSCwx84Bwtv18U3w9HSN+y5ksF+o7gQkBe6r3ff
+ WKHu/vUHq+uLdvK+pqVodKB3h0NRW6X8VoF2Sn4uD2qLAADpZSozhn+PeHFq+aqaAno2NXMVR+
+ CXD2pGpmrcafACh7PJVqExma5AmknWAiz6YzPNZ9bvV61ZQSzN+o2Pjuzo91dO9jdt/2DIzvuw
+ aUSjWGzVrNua6srVq/UBKTckHnsoesHaZiGoSqkHEGRQ2fPCW2SsdOAMVofw2/Q/GPQiuqjsZQ
+ 3I+WbhjHccDQg2I+mZsMpxvXDWkLPi2PdbDZtc3uLFEW3f6YkJT92ouEE9jgW6IHwuFN9EyALp
+ 03srArsuTBbQE509ImhpxjOfhjklVAcALCnlsM14zhO8OFCIhmngs7ldSRvBd3wUNewsvdQmgT
+ scF55jpk76DIdzCnj2iozf/eN1+7YuN+1H/3wr+y3H35sZ2O4ouI6jclwGbBNCEQuhSRFKbCHj
+ QU09gB5SnP9GjncOxDY03tJVgyI0oMCpz8a2Cw3s43tFfvGe+/Yd7/zvq1hsXd5rmSk6koo1tN
+ 59wx1EXLLLFBmXUUThUuWw0/OT2z+L1oEUoCWuifD+2fUNfxBZvFIqvu0VHd2QbMC3Nll5JIMw
+ fGFJencxHtNwN53JplFJbuLSJE2U+WHMdtzsNfhwQ2z1qnZV27eslevv2DNWt2OT/t0Wjw+3Cd
+ o5Kt161veusMxK7JGuWRbcLxcWSHoYaBIAze4BSAtFMiRkkFl7taDMg0D++kVkdM94R7JoIp5n
+ rr3InJaqbYo6C+kbbyvRbtAj/7k4SPLjQe21G5ba3XVyrD8RdOLw1GoAgXI8lpxy2No1DE8RIV
+ Omc9Fzj6eG0NZVNiIyxdw6OeSdIqzD21fxCIiQJ2qIMrnGrS6ZaVXLtvaasc6S3XrnRxLR98f2
+ FlvbHsA++4JQQjRjqvLbVvptDVlicWTrmPBV7v3u8f1SRcuoIcSB39hggYKZ+T2xlLjQF6Jin5
+ M3h3ySyxsqDjxPQRogNMfYVqW3v+wl3BOH5RdLmdrqwipWbGzwZiAjx2Aym23KnapI8AcswlKJ
+ RMokfihJl5Vv5RACpUp4lh7ycXBKhQHDvZceKcwDguc8CGmUIoo7MzpFNXqauamqVfsFSnZR30
+ jn5LlQBRdhiHHLdvW9pZ97auvWadRsp/++H/az37yczvp9gT0dMYk1LoRHqZf3YSLz8uQAO7cK
+ rWypq5B5+A4QGKbMzvaP0zAXl59qmXh3wSwR1rb7Zdu2vd/8G37k6++ThVPIaIRw/03hp0+L9g
+ ntsBPm5UlfZWLFD0LFf9ng72w3x9zDuwXKZmI+dRjk6yN2DW4kVmyoGSqdW3YvOnvlHSWMnoa8
+ M9TOjnLPQd7gT22/61a0V65ctneuH3bNlbWKAU7ODy0o8MDcvSoO3tzJN/rd8Anb7Q7toKUJGi
+ xqVuHP45WaFTt4DWxSwizMOqhffoTFA8nAuk66VF+lFaowo9qcQ6ZHRQz2AWAk/fJV9A8kB/u7
+ +/ayZMn5L/ByS+jKm6Bxy9TPTHnENWc+maABigq0B6aoMQ0cZ5yRkkipRJiRZnL2XAwJJihUkV
+ Fxt/z5nPQPmqViK7AXwxCzWEy1miSxmm1W7ay3KIt7dnpiR3s77NJh51Ft9uz/aNj+tYAj5YaT
+ dtYXaECB97+UwRR00pCwd2kw8jXK5BFDpqoxId21u+Tr4eMdE7wlzkawZ40DpwVg8aBdFOB1n1
+ SPWow43kB9CN6qstbHb8zH2OSuEx1EOS0UPScjaY07Ur5eYEewF6TxeClVd3jTwSXQE0UEkmci
+ wKre11TAfbYxUkSqao/fu6HWjGC+C9A0OVRrr9IJL4hm4rCP+2bSSwgm9wSeyhfeePLduvajt3
+ 99S/tb//6b+z+gydM6aKs06d+p+4tlWp/VMOwWUqwh6oMnk+aO6FVCMDecryP9nb3SNVx0Gume
+ NDBeEg/+0K5YG+/8xV61r9485oPqTlPnWxt0PCOSVKfSPZy9sLK3n2kFxq4C+qc85O0XtmH8ib
+ B+ez3HVqzssZkwVh8vizYc18S6h61txx8fJ/Cb+j3Yz+IBygH2kkerapu2Z6hhIRimbmqp/n75
+ 2Cf7HvQqDS7sbFqb965Zdd3Ltt8mmOoxdHxsZ32+9YfT8nZw64ARxV+OCv1hrWqkpfRfAra80w
+ 6EU+Na9FRmVOTzt27zMtoOYsbnoNLRcnXSAd4So43vQrMbyXpzhNNMzJMtuagZBjT+RGTtqBGU
+ HshBq7aqhPwy40mq0Vw4vMJAq01LA9gQWQFKRry1wL4kGaiGgaA4rOhUqOOnQuaOGqZew1ZHeL
+ 94XddQW6VSt1azZYUN50md06wLjjtnlJJg9fE4nR0dEA/GiwkoD+WWi1b66xYo1al4gimaqQ3c
+ mhYY65ADpkCKhm9Aewnk4H1T09scNajkykAfzwA+MMUDVO9onwQmi7Al4cQqvceB8FkIBdgPyT
+ gg0+eE5xglwyZZbvdpDqnVK5a/2xgvcGIlW9YXeAYYAGnCsljG0nn8NR5I5fUmFff05kCuDO7v
+ nw5zeXFwQXQA/C9KhFIJOouWV54zaCeRqKp9+97Uzbhr11Bo4WjxEDwV16+ZV9+5UU7evTIfvT
+ D/2G/+c1HtIaA4Rl3C7hW6LApS2XQL/gL10psUVWMqBmPcHAUNxwoLEGGi9kOM8yK7O/tsw8iw
+ zX1YCgBnk8ZUPLd73/LvvPd9217c12ZAI52OHaiqLJg75PDvw/sHXHPA37iV5/h1Reonph5cNI
+ o+gDRBU0Y9AylIr+glNd3DBYdGvkF+Oxum5HAj/soxW4nAD5dLPDxZRHue7xkUUi7slnqJtaRt
+ G/5HOz9aIunntl2p26v3bxuL9+4aZVShSP02MqCU4Z3x3CeI3jiwkOMR6das0axREsErNqgccj
+ X+9SpTK8Uwk1jMAR+wCES3DJucE+SEo8PJYsaWnLFF30DLT1VOe5Ng6fkvS+JhyYWC2WGlfePj
+ 0g79bvH7CuAO8XIf60piwVUrwA6eoswB9ZLSlAXkI166Ap2AgBjgPlyp8OdBytkSA+9GagBLNA
+ myNmtqGk5Nzbnltsd67RbdLLEc0AFg5ASDI8BWAH83aNjZu2OBj2+n0qpTN/zpeYSZx1IKdEyH
+ fy3PPMBKsVyhccRFAwANw+DoJk3ZXunNp+OzCbInO0xhxYDVqB68Hhy92Np7aHTB/jDqhoVOo4
+ XKn3QOGjWAujp+U9N/0BhN6WCtZeWOGiHm6x72rfTsVNMEbbO/+M9azdCRRKvATWqgVvR5AaNk
+ 8NuUKpXhdugN1OSl1KAPSMc41plRzekvJ4Z4D+k7NZtORLdGSd2tVPUs0jkiWuy0WjZi7ev28t
+ 3XjAb9e1HP/x7+9W//MZO+wO6T0pxIz4eYEWnSzJX+Bxo2mLboclWnF8seqWK+k261vU62LUB7
+ OFgSnkyLRlQ6ExEf+bn1l5t21/8Hz+wb3zjbVvuLKlPkTRGXYbuUtaUs0/nVy6q7AW8aXM1EeV
+ 55R78fxbIn37801V9PD66B8HXn18QssobOe5qodRO6VyfwPsY56t68fgy1Uur9/DGieVi0eXyP
+ MXD33tO4+hgUX1hU1uu5u2lqzv2+u07ttzqEBSgwDg87dtJf2DTfMlK9TqbShCirTdbVs9hECX
+ l42iVAL4dVXpI26hegDlZnl48qFRoVQBwJaCpOkQzF39xammAhQoR1sBePbIRid0ABec5K1TLt
+ CvgNCSUNFA3jAZ2vLdnR3u7lJCWsJ2uVa1Sr1oeYRzuiEn6YYYkLVFQcIUM/30ADcAe1S52CQB
+ jVMbB7csqGZy2OHWYyWHxwE1cb9Ss2Wqwsuv3+3Y2hI8Qmnbw/M+z+u4eH1ofqibI8HJTBrdgd
+ 1Apw/u8boViRSHkmEgtwlenpqqeOx3lA2BXAt18DudijjmEnvVPjljRzyYDm8DUbACVT9+tnQH
+ eUyqVOEA1HHK4ajBBBa9hqviL8X19T9U+dzDMK8hbpVRkUx6LGZrD+2faFXCHk2jfBXJYuBU4n
+ rN80akut9SgbQKKDBzDqfu94JyUNIka/vM81bTAFikj0zvX0uBrNmt1HceuzMcrfMFIry/OPrC
+ ZrF3UrVvX7ZU7N81GA/vFP/7YfvGLf7bDk1PD/pTXGQ3SKOpU6Ln0golDK+WQpBlRdcoJFTQOG
+ rM0/MsMmB0fHtnh/iHpRPbZcXyxKEMIUMzb9uUt+8v/8y/sq195zRqNinpa7g2a0lXoJWVpnN8
+ D9hn9/YUN2kz1ngC4V88Li0coWi5o0LLWDhonk1mbwnBsHXCu3GuHCqPYrMVipKb1hWD/FGcfj
+ eJkb+CqoYvBn+/xOdjr4DDQw6ZWL8zshc0Ve/Oll217bcsm45mdDgZ2cNqzk/7Q5sWyzYsAsVO
+ m6Gx3lg2aEe+taxrVXS8xRQiwkmJQAM1tPpummmMnV48qnq+vConGaUhgYjZsjmBOq1xQJQazM
+ Q0FwS6h1KgmvjSgJQA6aAijoTno9ax/dGT90y6r8nx+Rv8Scqt0JwQAQeJZI2jid8Gxc6qRYRv
+ Ybs9UtWk/TfUOKnpp16V/xz6jhCxP/GM2l/99vmhjuCcWytZodaxea1Btg93HoHtso/4pbYmxM
+ UH1BysETEeVy1UrVWqWK5QVlUjX0KpVYabF54XJjBtUQRECySimaiE3Petb9+jABr2uzcfKnx3
+ 2YL3QVb+Bg1XO27NRCytmNGuh9VbgNip5UjekdBTRSKXOCGojNVhBSdXKRcpucbzO5mjOu2PjH
+ A6e6q9QKeOKLJxzTELz+1HZMU4STW81YSk2YYNWjXiZl+n7eY236mizuZeG2qOPo76A1DLR0/G
+ NgcA5CeaQQqzdadvNWzftSy/dtOJ0bL/48U/sp//4D4xlHGGwLl7O7wsFnit4MW05z2gBzT4UF
+ xxJQpEZC+qPBYyH1OOeANgfHx6zKAGVh54JqEFOxlaK9uLLt+0v//LP7OVXbluppH5WgD292rn
+ OqH8TMsVsCPeFlf0XAPsU9NOKPrZVWYom20jNgr2IlkxTV6sBv5s0ZUmNnQP7zEhdQshkegbhy
+ Knn1iMyYp6nOPuU3nEG+DnYpxUR6pdywWx7tWOv375t12GRMM9bd3Bmuydd0jj5UtkgOjw97Vq
+ zXrH1dtvq4OL9ZLI5G2DvQSNcRrBVJx3hVYlHxzGPFIEgTqGgMsS/McgDSoFyxjKkfCVq6FEly
+ pt9wDONah0NQwDgeDShdw1eB81bVoKkIM7sDMEd/S6rarhDRriKHDdLNppNuPUG14omc0yd0ir
+ ZdeLSgmPrDbCXFa3sgCgkZOM3R594KIfKVC81l1ess7rKx3UP9q1/cGDTszPG99ETBuoKV4ng2
+ KKCL1SqlitVLAe6BvbI1RoHtQiC7nLIyV2qhaaWmyi2HRm0vaMjgT3DTAbW7x7a6ckRPe4x4Yz
+ BHRq6gb8fqSmNEBpusfMFDhYB7BF4DbDn4wLsOXSExvuMihr4ITWbDWu22/w9PC98d0hbOH2B8
+ w0AJH+fDBLhGtGcACgNkdcpjUOQTJLNfCLKwR6EShJJgvNSwIIaN38hVdpENKVXhGR8XSjQWWn
+ biy/dtpu3XrDiaGz//I8/sZ//5Gf2ZPcJtfPTvNmIO90ALXnnRCpVgA2BN65xMopSjDXqNSvBI
+ oFvHXJc7XAP9vasd9pjITGgoymyFMY2Q2hNs2xf/dob9p/+/Ht24+plypblPeaIGMEczonHNRN
+ RfVLWBHgGijrZk9HIx4EO/j4UOfp+hmtfaOKer5aDVklBV8B6gW2yg75+HEScHhe0jmz11YJd2
+ CX4y8bvaoFLv7nw3v2DZaWc8dBYfJ5X9nFEvLEFueTmSsdevn7NbmzuWBVUxHBkuyfHdjwYWL5
+ UoaRxMDizVqNmK62GNTA8ggxXB/Bo0uHmCjdCau9RFfKmV0g1h3Kw7a1UOKlJ8AQFhKSfSpWDS
+ aju0bALrxnJ2PKs7DmAREfNoiEKDo/HdGxYfrHYGAAAIABJREFUFIDmII0ETxnQFwjv6J+y0me
+ jFcoavA/o0022CWj64j2xcmeS1VCui5Bb8v8CeFajboNFAowe5/gZI7etWK5Zvd2xpeU2F5Nhv
+ 2fDbtdmZ33Ls6+hnQKpDFLVkKdWrAQPHhio1RtWQUgJfHWwo+FuRFOXfG0HQyySzICda9gMap9
+ Rr29TUDnjMzs9PrDe6RGrfshFEebOQazJlMcPFTmAnf0Wmru57JJumJBjoqGLhQG7JnHNHGRy5
+ REot5V204qeETwYgfefKljCvYHwWIA9pJjMoHVgYo+A71+EvfTz0tGT43dwCAO9uGnDAgMAgYE
+ 1gKU02qLRpKGXI6c2C+gBwaumbpe2t+yll27ZjWvbNux37Rc//pn94qe/sL39fTZJ6e2fx04Hp
+ KNz5lnjtwBFVxgB3Emv0ZJbOvtmo8ldJ943qCCdM7P9vT16K9GqoydfJuyc56W5NTsNe/f9d+z
+ 73/6m7Wys81hxJxyeUlhYMlQK5xJ4HFMnyGxl/9TXWaolAfIA2XjiWBwCuD+jsg9VTTRMowJ/S
+ mfvy3imiRtVf6LO8VFqnEGqdRbL9QSzs2BP7v8pyuhZv+s00fPK3qsBQg9olZytdZbs9uXLdn1
+ zy5aqsBI2Ol2CygGoMQBjNLSlZp1gTzWOSypZCUeOaHjXO8DTU5DhHxhy0p4cFzWnbzkUpJueW
+ 37QRX5jBdjjpmLDllOrOXHoHiABWobPg9+RwTk5fzbBqJOXx43kk4obRCUOC+DpeGSzwanAyBu
+ LogNmXNSwE5CVsOgbaoVoiaMBMrwXNGbBcZNlLcHBs8GJU0jvxoOBTYZDKzg/jdfl4uHHABc4P
+ m+AfRGVIcC+2WIIhjh67G7kf89qGLpv2BFjQEd+znyfI/D0vb4as1jYuoc2OjvVjgbe93C95Dw
+ EwtU1gTtx2gqgH743eGsD2h3DjXEgK4G5dmfQ/6c1GtsINEzDABnODbl+0PAcHUaDFucMiVWiq
+ 0ADSXqomQctnN6AzLg5Eswi8o/nMMYatEAK7MWlA+yp+aEiSpJaKqsoFiizEXv12lX7Mmy8N1b
+ s+MkD+18/+wf72S9+ZftHXWXBwrHU9fSRLpssMD69zBPgGn99raY56EEpvPLMHdCAoIbTZNU9t
+ 729fTa68T24sGKx5QakZNbZ6NgH33vf3nvna7a+ssxFkcD3/wvYZ8A7wd60Cn+ay198fCwcUXW
+ nlXaGqjnH5Z+nUFS5Ly4eAvs4wr6L+T1g72/fU8JCppnSRAuCy2Qb4Bj3HOxTsMe5AMi1GzW7u
+ bNjty9dtpXWEnXqh7AoODyy/mhifVQnk7E16zVbadat05B/B8GPVZxAE3ckp2R9mhI3FMeiC55
+ qRHmelBq46sPLhDep00EM/3CbgQBxAApTqHBzcsBJNyAardTDe/ACqh69V4BjLvG+iWZvpHUZJ
+ lHPumxeyqM8zLtyrIRhH0yAhQ4daVdjyS85mepTr6CbQHdgh1Gp1a3RbFqj3hTX7JbJ4GBxjAC
+ yYTQtekALRBGA2WwawJ4KJPiqgFrigI7ke3i9iH8cYhHBc3kgDSirKSIJobfvn9qwe8IAmumwT
+ 7DHVC30/djlyFZXSVxT2uyCsplwMQdAY9YBuyfYH2NxT0NpEDGpnot2OLIlAE0HSgy0miIaobh
+ C1gA9tOVjw+peFgeS36q5Dako9zmk3aMJmio1lMqlxVnHS6jPqhYzFN66jRxh2RWD4kE0YM1WV
+ jpMnLpz87p1amW7/7u79k8//YndvXuX0+CwQlCoCXpE8h2SnDNd1BKL5ShAoiMctg2YC/BCp7P
+ cUeHBpr5bdkxnBHsUDxhmG6PHwVyBueXLBdu4vGF//p+/w0SqTrOpJDm3YODBYWWvFjV3PX9AZ
+ e9vOZ1u9So/kTFeUHWL5lms+M8rdhYWgQyQZ583yvKs7DN+nskdcsVQVm2TftaktBdLH4xRRuK
+ ZcIALOvtYGJJ+w3OwTw8lb7j5zJrVst3Y3LA7V6/ZemeZXO7xcEiwhwQTOm3cgNVKyToIuGg2r
+ crJVt2EuNAhI6S2dgbjLzWuAsypyEG1mtjgihqIaVXV/OJ6QdEAPHhh4OJHf4DumPCZKXCry5u
+ bo/vYqlcUIM7mYMGOT054c6FZLG23Hh/XMYaGOLQ0FfCFFFMX+oxOm4ACACxazTBi65/CZEwDV
+ gD/2K5jkak06tZcahPoy3A2nOUYLq1LV57pMBxLWGpI8HDzYpGoV5nCBR98/BuNaYA9QJSTlnC
+ xRL6uJ4zBxoC9Dq9kMYQ1Ax/f79mod8rmbL93YpMBQshPmVoVlhZYnAYIMkGVD7D3RYjzzwDrf
+ JGqHQSeKOFLYENAL4rqYi+Dbf0Ibhd40ROoUWfzNnGZZF9CizSH48iEqUFLKwvncFWc66bm94h
+ zDvYeQO7eY+6PI7CHagarj3oEKCKq1oIj6+amXbuybZurbZsOe3b3w9/YL//XL+3Roz3rw4sI7
+ wO8vyuEOL8QW4jAED930SAO2WfS+HW5aAr2bS3OeVlkU3aMyn53TyE6oG9GbgWOtbBatOsvXrM
+ //4vv2su3bjCohPRjFuxpJcE7VANGvmv+PDROArgJr++0RlZHn3DuaTmcWl1EfzWtztMq/7Obu
+ AnQnuPudXf5ayXeOLKeuJjGicUn3vsFzVnf2aWfN4F7afKfg70fmkTsMONk7JW1Vbtz5ZptLK8
+ QjLrjse2fnNph95SmTbhRq1BkNJsE+wYkkA7ouJkB9uK+0c7ViYoVHRWeFgPF4bGgo1Wwe5az2
+ aXFQPxsbGkL5O/B54PygK6NXtUsnqes6mGMBoUKqZv5zHr9ng/4yMaAlr+YYERxSb28JnIxVAW
+ aQ57rcLqEedjYRpRjIrVKIeZ43MnxEZuhmARFxUl+n4HTBeusrpC+qZWVTiRLWmW80jud3QEoS
+ gRoAhljE7rRajKBi2olNqM1fcr6EosEPerF3eIzJIsjy1137QSAA+z7XRv1QeecUm+P98udiDd
+ dMawFvl4ab1n/Un2jcVANoY21i6OHEKkW2FAXuVCr6Ri0ihw/xxP55WMBqtQq1lpqWQ3XBQsBL
+ OY4nwBmLp9SW6Eapn+NKzUIcItcsugemRWwgCDaq8JVgxIXjKSdpWrJGo06TeS2N1bp3VTN5+1
+ w74n95sMP7be/+50dHp/abO47D0wb06vBKSBmHbg5XgL2Edbi9wpOgPvnU0SV0DgKoscwHXave
+ H90OUVRMpnabgbsZxO5cuJPrVWzV9582b7/Zx/YlZ1Nq/v0LXU3SQ5EgL1r/J25+EywTyibDC0
+ TVXzCfDxNrzicJhYICf0Sjd5zdMx5eua8xj3AO1vdx0IedBwPKbyHPovGSfWnaXWfbh3U4M10c
+ FPq/zlnf66k1wWdy02tXi7apZVVu71zyVbby1QonGK0fziyo9OedXuoToasQNaXO9aGJQBUMSV
+ ZuVJrDBUJzdEEFKR0WG17TCFoHuc02fRUb845JVgc+0RqVOKMeoM6BdUhBovKasq6OgZAomSsE
+ heN/lmPFgQcnGlCJ68qCwofDji56ZnCTIwVcWx3yYWPBvKkofQyT88ZTJDaZMIQ9LPuKS0i8HM
+ 0MVEdQza5urZurVbbKqUqNfPQXuPzcUIVnwVXIMAvoaGUQQrHz0ajQcke+wXon1B659w2VDFDv
+ Eftnuixj+fAbgUNaPQToMAZjm0Iyub0xMbDPqt6gD1sFLB4AezRcAWFAD4eihwGbuNz4HN7Zu8
+ 8X9QAGqgdLp4CVxxjLH6iwrhcWc4bm9gkDcdz7QQwe1AqWrNRY28HzW8AId0l4VsEW18ck4nsE
+ GIqNlFo6GxoR0RfItkJZ6s+7hKcIqwwLATTsDXrtJu0m6gW8zY87dmTh4/t/v2HtntwbH18dko
+ A0VDH59Y0dZx8LSN6nbDIpnxYPI52JOTSdb9opgS9JA0OomGNQb7g9QH2AGwM4+3v7zMdjbsx9
+ DTcdru93rGvvfs1e/+Dd229s0RKlAuE2zDozV0E9l7tZpQzQbVkj1MUWb+Pm09/N7M4nG/sPoO
+ b58LrN9BTYH/u+9mfs05hD0Rgr+mpc2R7AguJN4afLj9viWrKd4QLNFUKc88r+8yBRGUK10RIK
+ S+vrtoLOzvWqtbplAiwB8mB4JKDw2Pa57ZbTdtY7thStWIt8M1VhZTgQkWeLPTusBMGdOGmUEK
+ VPOpFvmbj5rQFxx1EmSZHoyNEUdt+VPMYuMHKgHzUaqNBiSQkf6V8mVVeEY3AIuibIzs56XKaF
+ SAKnh8VG/jY3tkZYQTAB699VvGjAW9aGIhFI1btPRwShHuDK0eTdmyHe3ukd+j3A2jA+66UOQz
+ VWVqxZr1p9WqD3jgIfsECg2OA/5O/RjQi827T6pbTw+S0tYugPHSC0R4MnSlgnL0H7oJEceEm4
+ W4F79mbr6CbxoMz60HLD4DH1O8QGbTYFYy5+Mo7feJpVXMmL3HHg+8z7xVLSoGLE/X3VC2p+Sh
+ Vjc9J8CaD4kk6fWrzsR5iIMsdRfH4pUbV2o26NRo1WVxghsJyHG6jGMeDpRgK4okLLBC8qYdbn
+ DuaoDBwrDiwhRkEDKJVrFotWLUMelAePODEjw6PbO/Jvh0d9zg1TS4eIJ+HzTEWX2nyC9h1eBc
+ lQD+oJIGYm6t5X4ENYa103L1hAQQ/j+tHsuGKdifY6dI6O29DDCYeHnKR5sLlk8kYLty+umPf/
+ OA9e/tP3yKFSjU/g3M8WzezoWDQixuo8S34+0g49izwZ3/vAg7+8y4CC4tERp75rMVD2P60zj4
+ L8okiJwP2aLLzWGeatmml7tuU5HMI6DOFvM8e+NLoMZnZijZ3zS2O1WtKV4rFsveP/1+cAESFM
+ p1YLZe3S2ur9sKlHauWKtYfDqwL7hYAQItjpVShml9balqzUralWsWa1aqsbUtFq7GqzSlNiYl
+ TADF44IieYEWa4c9x0ccNRoUGcLHoQSRebeZhGVAq8KYt1+vWWmrLbthD0zlq4sofVPYEcbgqw
+ omwWmHTC/TH6dkZaQjF0YE2RmbujA3eXv+UX6vfID/3Qe/UinOofwasBg8PD8jVI5yCswO4QSt
+ Vq1UqtlRtWqveYGZttQZPnoJNAY7lCt8nKBI2mFHxu3Ml6BUoXWgdAcXQ4IwySkz/Ii4RsX/4u
+ QDXTeFwDH1hRFN2NlUTmX0EKG7QoEUVicEpNG3h64PGIEI4QNdg8abN8dRGBHVl2kJ6CZBRSAe
+ OLfohsn+mEodZsqjsRaloywYabULLBSxo6IPQPpkDaDMr5ws0gYPfTx1FAfyKMHGMBY9meRhAk
+ l8K+xoMAnew94lcUmjevwWOQucvozU1fIEt2LnguIG6Oz3tW/e0Z2dnY2bmspAh4ycwQZ9Blg1
+ RrmtaQlpxVe5ZZUd4tWi4ymV/sRi53TLfjy8+siTWlCzOM+Y/uiddnWNmBeAYTSxXmdmrr75i3
+ /7gW/bKK3esgmKG5nzOkWX6FwJ0HzKLr8Pu2AHyfGX/rCaqMzGJdDPZR52TUOrlU45/8XEpf76
+ gsc+A/QLAn9PxB1TLqkhOtwHecXUlnH8chwz6M10rFjSn4eKc6Ryl08V839defjX5jSQt/o8f2
+ 5/6hKBecOPBgbBWKNrWyrJd3dmyeqVCXheGV30AQbHC8GkAabWUt1atQqBfbdS5I8D2Htt12Ab
+ jBEpa5xpkV9igFOTwFb3sddOxOpzCV9MV6A7guBsx0k+tNG7AErxloDMvsBHK7S52fu6tw8oRU
+ kiamsGXZpQ0FvF6MDaD8RS85gFE3ZMTVmONapX/Pu11ffKzQL5+0D/jkBKqP0T+4WKErQLeM1K
+ IMMwFJQ36CFXQFpWqtTichaqzIrteNFnrNerYoXIhx8twc/fBB9hTAgoKBlr5U1IxczTC0cD0t
+ Ce8tjziNVzFpjVoLPw+pZyIhUTw+Mig1MH7RSM5Ak5wzphuhenNmJDFYjZHZS4jOCyknF/yAG5
+ JVwU+okzkZqrcWP0BFRIOmgBpWiNTQSN7BlhSYGeHSeFKEYUAdn6wfYYjJACyKrUOm/FSZxEQv
+ Tqkpj/qa3LqPltA3l/9GuwgzzAwNpDZGw3dYDgXvSg/XuwNRBIXdwxpti37QxlgO1+54rOGgiT
+ cUePxXADp45TGcOLxUj5NrdtFs7yX7mbRTEczu5W3d7/+dfvGN961KztbtAXn3IFz/jrC2tNo/
+ fB8g/guz4VDcAB+FmxD8pilY86pcT6T3slw/As0T0qIZySVmV5LVsv/DI4/PhuPK83lxNln/wT
+ 8q3G7CN7aQaSP1vKsb2S5/3jM88o+szaSKsnnrF6u2Eq7ZZc2N6wNqoQ+6EoymkKlAeMs6M1zM
+ 6sU87bcbtpms2k1v4kotZTrlXO7Pijjcj2cOGzBKU9zfhqVGkBKE6ryUccuQWCioSN6lWALTkC
+ aW73RYkNWXveeSer0iC7g0Dmrkcrncn96cKt4DlRcSNJCf4HTpaimwSZh8KXfZ6UI0EfVjGqe1
+ SgWL9+uc6GhpW/JqvmC1UtFq8OSAUQIqYay5dBnwGQuQztQZYL3LkvXzWpcYSigHsYjX2BgpzA
+ aevAS1Bcalw8ZKwCflSMueOwKWC0CWCAHhcnZQBp6AD+elyP6CAyX7DIBewzIcbBMVbxsK0K67
+ 3F/bunMngur6AAYp+LgqOkmaoxLpGcQFm8df/ZfccxQCDBy0hgFWSoXeNyx08NnYUA9lFbc+eh
+ NaIQgXNBEezGe0oPl8Xrw4EeVrMB5HBB3BcV1EvRMUhnq2tF/opDCME0kvQPWBfSIawVc9pltn
+ sZsiBRU2EWK85eJHI79yckJqb9IzqL5QiFvqztt+84HCBZ/3drNhhVLsFiAk+ZiszEFeze/jHl
+ e30EsND/PVdZP0TV/CNif5+xjSvYcKF/E2aeN26cXgQX+xRfRGKq6COz5vXOV+u8De/a7ksX7u
+ Ronc1zdkS6Xs2qpRA39zvq6ba1h1B8qizE92jHGD619l+6KsOXNsUm7vdS0MgdvkN6EGxgVtv6
+ w4gGdEdOnWARY/asJqyxOaNBVRZLnZ2MLVIrkoFCnsNqcy7AL3HKlUrOl9hLjCJPxegcrBpi7j
+ 3j43uC9IRACqAn9vOSEPrrvUkq8Nqqn8XBsgzOYiA2lfGESlRQ/DEb3aVtN8qm/UJ7nrJIzK+f
+ A7Y+YzYoeApUbBCAsAURsGqOR04UKxn3k2ReABHI4YqwgQBuLLydJueApm5fVPaY1RdrCa5MAg
+ WPGRYPNXIE9FlD62jh9A7DHzoj+9ax+p3bmPQWBmddGkdXqipFM7zzh0slkezYBFn+cG1EwAFT
+ 57CDRCoCPd4lhLDadGU/o3jletTNXgNJODI+5vyFltZJdejeYi0hYF6DQo3Gq21Qj0vIck7vAx
+ fNnfG9S9khbr0o//mQDNURdxXEW3UOnRgfAkJDi36QnXRYMEzS9lK5nXD9Hx0c0xcs2mWE18dK
+ XX7D3v/lNu/nCdfWb+Nl5shcI6UWwVyHD0584I4QEku9OFe/5adZzfHssAkHpnB98empnk9Xjn
+ wf7C6r3CymkAN8MN693nC7SC83lLGnvC3b60osNezX0kxOUJXwktnouvUwv9AgewtBMtViy1Xb
+ brmxtWL1SZdXEIZ963U4HQ46Xo2os53PWaTZsZ2nJas4fhscMLnSASsTTkb90fg43B6o4Nmud2
+ 0TICW5sTIXKplaVITNj6X5Z5k4BLS9wzLCUbdQbTIICnSKZJ3zu8Thkvsofp0dP+oo3ZOFLP+a
+ 2WlGBMD4rEMyhwEE1Hx77oEJoY0z1CuSa4dtTYXVeRsXOm9Lj+LDQTSdWmA4Y9pGfT8hts+rGQ
+ sVpSgA1GnrasuLvCIvX2NOw5pKGwsESr8lmqDfi0HtARczKl9/3Ktug7sBUMJQz8vWRLQJkk2N
+ FEEJ5Q/sDB3uAEL8/tSHhUm3I8KWXLNQVMA6S6qnHOJhuLfwbro2MRoQqhwuy8/7+ufAadO/kw
+ h5UkBfRIigkaw2TPN6vUmjh8Amc9QfgHANVQWUhMYrKGtfHJ9myDrjhbx/1vNRhXtmHAsSffyE
+ 9yQuRJG2LairX/vtCEO+Mth0B9mUovHwH4dX94dEhp2bZFaD5X87WN9ft3fe+aq+//pqtra1ys
+ jgWOvq9E6/DWMOzfBV+6DLUNFnRIV6glgHeRUB/OqjkPAW0qOJJB6ue2h0k/deLdfbxfrIN5MV
+ FJfXGCdUVGrRZo7QszkcvJVZAvZ9sj/W8Dn+RDuLa9xzsdZWH5Aw3Em7IUi7HsOOt1RVbbXf0v
+ XrdKs2mnQ4H9vDxYzYCq8Wi1csl22jUKcEEqNZgn1CtyI9mOCTFEVUOK3BW/0WrV2syQXNZIqg
+ VTOaiUcjxdczcQ6aHwSWbS4MOVY03Y1Dtl0sV8r1jNBhd5sbsWgAi1RBYFOS/w6EpNI2HQyojs
+ AixkVqpUM3CkG546wDEwDdDATOfWr8Pjt44jYnPogVKlEMw18D8AnJsQUVBtjk+g54T3gYKMwc
+ IjtHolP8+Z7l0N3OwB+oYzQfA9hY2BtCry/uHA0ygOErlZIGEu2iydUYjeaohr9D945iRykGDF
+ pw2F1H51cv/xlOp8FouN2SFGg1RWhD42AvuFO66Uo5bYgbX52P6lmCvFqc84PVYNYOh8JGMlIN
+ yTHTybHBvunKO2HNsIwUqPp+kkCHgcqsEB0K+nkcOpqCeVnd8nwHsvhPR7EZIKdE3SoHhoso+G
+ n1ybk2LI1b2Ph8QqVyQHwdFiJ2sLBOmBrDHueCiB16/VKJ9w/e//65duXKZRQdynuk7RA7bGWg
+ G+XhTmAlNkfQg/p5umAm4Z6p5L5cTeue8XUHw3Rd+P228pqqkpP5eHHo6t7g8vdjEufC623ccW
+ slSmgX/TKSX/nmzO4PswqczkO5uMmdk4b0twD0+55U7L//HleCkR0mHD/cA3fbkYlkvl61dq9p
+ au21NAl3ZCqjsMU27p+BkqitKReuUyxywqiMhCt+r1Sgx4wQqbYuh245pWoFEGXw2KA6oTHCTl
+ LBACOwBfJgW1byQtvFQvzAI3CVpNEyr1iiBHAEo8ZzeoBW3LhqATpagotzGGNUuNfj5PGkd/A7
+ klaMzlyhCRYJADoPfzcS6p6ecZK03JaXEjgENVlAOaELicxbQsETjEMZpozObwUN+AhO1ERUx5
+ OYhR3Q/GKpcSAuoqgZYKjxF4/oASShw0OTFooK/kLNCZkg7iiSPEwdoQv0/vX+cyiHYu8MlKDh
+ 60rs6aQI+3TNmWfGD0vBKTZw6dN5udubVMW8tjR1IAQU+3ikR7RYU/CLppECfFBXjHvW5dMOiW
+ Yvn118UsCK4wtgK5031clAo0SAm6HG3oVcImZ7/Nn9HUYZuhqZ5PAdLDbQR5/k+tFipPxEPepq
+ z1zxBChvRA+BCRKdQ7TSil4JFGOdHx2jKn+N6A41D0zcO/tVtaallt1+8Zd/+9p8ww1myWx13D
+ uFRbo5el/yQfG7Wc5tVmvFdeYTXH8TZn2uePouuubiaz+wOAj985ykqxqmUhUXg3PeTjuki2KO
+ yjyqfnzmkpf46v69Bq7jCxYo+8xafg32C99hG4xpyb25cfJASlnM5W0Ly0pKnJxWL1j07s26/z
+ 4sRVTGGZ9oIym42VNVXAEyiUNgYnM6od5dCBgNXZTXUaAQlC9gSQBdpT6xsoWlXhRtqBuZ5kgt
+ VWIUatjNy56jaS9UKf0Y1Awe4nNZxSwboyCEFJCDBYhacPWSB8KTBkBB488GQsj0AJW44LFKgR
+ MD5Q9cPuScbsdWa6A76AXkDcTqiLn6O6eLRmevbz2w6hgyyT7dNcPHYVbDaZsC3pnylOkHurfP
+ u3oDGQlivN6zdblmrAe0+FqayeHtW3q7TwBSoLxbg7BlQwteBYR3evxKpYNOA4zCZ5SSTjEYtG
+ 70CDyzy0MbLklj0StJU9OQsNniZVav0K/L0UAN5HKB/pUVrjCQmSEuTO1ZmeIyo1OegQZw7TCa
+ WvhlduHhu37TnBPbQtQu0cRzS6heFA8eknF8hjvuuAN/3TYDoDv9+UAOC/yh5vU2QaQpGU1fXl
+ 0CN4SWoxqn9lxkf6DZdb/Atwk5yxEB5LlrFgi2vdOzS5R17+eUX7fXXXmSIin43tQfn+fBrzM9
+ ywkiHKkdgH12Kc0lNfk2pkI5KPQVCLVaqkC8G+xSgE/xc4PyjutZBW9xBZJ73nJQzoXL8i0X1j
+ cA+NDchd00K+fhJullImZ7zn2WRA9JTPK/sveIKLpbbePnOc8ydtgglRtG1Gg1WtKccvZ9aGVW
+ m0xorVQf7So3DVbJOkAkUaANMvsqJUkNBqKlgygWQQrWKrS3N01wxIy8Yr4TRvAONAStkn25Eq
+ hOAhJVisUgwhmwPpSKHXODvPhiQ5wc4YdEh/QJwigqXSpgpexI55Ot2TzlTIO9H8DIx1j8V2Fc
+ wvIPPDM/79CZjE2440NAVGr34eoLBqAEHnE5Pju0QXvbUgPe5UMJfCNU76CpZI2jQTFp5LQI4B
+ /VG3ZZXlm19bdXaUDxVSgT7BPDp26N5AAIurIlR1cM2wX3rkUpFTT2tnjUoBeCnMgfHg5m+WOz
+ nrOqpToIfD9VTqiJVVUulxAUDux+qbsRNq8oXXHIil/bUUw4zwXLBf6Sb2X2LCPg4Xx6/R3mlg
+ 5BCbxReosrWJZJIOHOQ06spyCP+ULHFBcqRPHzTnTZT/kCgenydAcEUjQQQHOw7xw5ngEVe+no
+ cg3fQL+I1r8wDHF94EGGQj0E4pbxtba3Za69/yV56+ZZd2t7gjo2NaU/00sIWx0KgHLrzAFVfw
+ /x4OZhn8TfTzIympX8r5fUzC0FCv1zwvc8C+wWOP/oMCz2DhNxf6CX4qrogneQez3cGImnSP8k
+ uKkO5SXOVFhGLVf2i3TGP13OwzxxR8qYCEpxgVp1IXioWOIWKCgRAR6dEnz7lxZ3L22q9auutl
+ gZnajWaV4Ezhu6dShpU49jf14bkAAAgAElEQVTeun4bIhX6ncAt0UE6fEQ4fo5GbHiyOKCjsqf
+ sjpWeFg5UfVgEYKHArFNPRsKJB9hXoG9HhemySShsIHGEKyUqTEgvS9glDMccegnnRfqQ04xMz
+ VAuKFVEBlalcQ8u2LfraFbTjdLDz1lpw3K4f2oHBwe2t7tL6ujk9JSL5QB+/ABMMz4v7BXAoAG
+ MMBELFQ2ljiXE3FVsfW3F1paXrd1qaMflbqGkuHzCF+Aq62Jo6+V7M4SqCIocH6BCRa9s2QzYu
+ 0EbwYhTsuiFlNzobHE4hTQUr4uI65MihrsSV6uERl+TumPq34Pjp3qIZpUKlsdOogpahzswt6x
+ 1TXUAfHjICHSdSEkkh4sj9EEfiZZ3GWnsCrRqaLAq5XNSSUtS1KfgH3CyCDxqnBKAONCtxxPss
+ aOFnQV2WD7E1u+d0VQO91SxlLNLlzbsvW9+3V566aZ1llp+LLAzkHlaSFvPDyR5WSYVjlfZ2Vx
+ WX4OSRSoWq1gKSF35KpEdGHum6sYXj1ThkqngY0eQadSeVwBlG8Xnufxs4Z1+zkWwX0Amvm/cI
+ HFuYrhMj3pah78I9vwoz8HeD6lrj9mg8yASVmoxJl9W4xVGWKy25jNyjwBgNFLXG3XbRGpVtcz
+ GK24nLAqYSgVFQ4kigsTBuWM7600yaor9D8A+kqNYHaFRmyRFqYJnUhUqWYq3VdEBLBXwoSosa
+ c5Cq+98PapogLtULhPrdDr898nxCamjYX+QeOmwIQifE/fiwfPjMsd7Q2UPkJIGXAZd6F2wqod
+ EkNw83Cmhdx9yQOpgf48NYSiAjrtd6/XPbIjKH6P6GA6rN61Ra3CqtFQo0S4B0kn58cubpl6vW
+ gtN8KWmddptWVPgPbhjJ22aXdNNhQ1So1jdj/VaVOPIhhmvSwrHjcgGpIBQ5UPCqZ0XZJJZFZA
+ yBiQxJeT6vIKqZC1+8v4RoOK5UdVLDSSjuKAMyNO75QL4e4A9FhmGljidEyZ40XfhDe2TtZwmJ
+ nXCRydlegBy6m6q9xNXmN7aBaZm2WEdpyRSkPevUo7HwdZVMg72rOz9+hTY63jis/e6fYI9DQK
+ rBbt0ecO+970P7M5LN6mt10KoOYSYF+FCkvECSgExFkRB8O8F+4Uq+wuA/TMr/afpn2eD/cVcf
+ lKUB6VDI7N0NCo55AFRERhPvF8EewF+FuCf7i08B/vM8kk9O5tCsinQjAkMvkTX+P5PAGNmS50
+ lq9WQMjWxjWbTNmHPW8iz8oTagNJB3ABuggU7Bt6IVH3I2x0VN28bP1GhH2dSj+uUmeMJ8ElcL
+ T3IgvF10p2HXSrtEpgZ60068vtaYFCxgu6AnBKeOHhtxMThIkW1D3BkCAUqTYReMywlb416PUl
+ PImeOnQiBVrQJ7Y7pXTN2H52JTWEGBr6/37Pjw0M77Z7YyWnPjo6P7fikSw4dzV40nVs1WCw0r
+ dFuWbWOxrYSpeBpA6sGSEDR4AY/i/eFhQo9lBIqVgwtQf0DySamYd2/H8/PFKqYJiXwoEkLZY6
+ qegHSzLoI0gDguwcOjp0apzov2HmhYkUvhlQZZh2ioQypKxfxGHpR1cwkLDRnncoIGieq0mi+g
+ rOHwxBtNlxOSprHd08BeKo7tKuIFrAW95it9QrPB7FC+rjgecOHLE5hpgCRVvP4zGklH5YK56Z
+ rvTELx0weLzcv47XBaXCBPT5/r9sjbYd7od4o286ldfvBD75LsIeZoKxDJH7F9RvHJgv2UaWf/
+ z/ym1WAR4mdas9V/Wf4eo/BXODp/f5f4O+juZrBhvP8fjSo/cWfomi0S3s2hZPdtcRnUrimL2a
+ ++8ruQETxKK5RYJ+YKkibswD2maUkehbPK/s4KM5BB0fJ6h2NtJI1my1Wz9yacjRdA0mdTttaS
+ 0022VbqVVuGVwwkkuUK6RzG5bmXC7TvAHtpzbGAQHI5oa6dUjOPLQwLACp/HKgpn9TpJGWDPFo
+ 2dkA5lGGhW+TNBECA5p5N34KGuJQkpGAJUDA0ChuPCfDQPeNvVCQc6uLTIlYQ8krdKFAWRT+Bi
+ xdoLjYBpUPnAgJ+fDRkNT8HPQMbZcQADqDdh/3BGSmc4+NjOzpGMpKGwqrw0anWrFmrW3Nl2Wq
+ ths1z3iw+PLTj3T3uErCDGs8mNpqNrVKt2kqnbQ30DkCVIIKQPDoAXglI2P2gqlQTdkZp6nA0t
+ TOojvA7BH01wOkE6UHq7jbMjx7JT1iY6WKKOQeMcGGhgPkahqUYCA+bAy28OE847vLgEeBFAzS
+ aoloXVMUBM8HZ09TMm8JhRRBUmWw1tHNQnS5eXkAu2iOYfXwRnjdKrMoSMILFbEPWT75ugoySg
+ 1VlRu4ZoKbKUwIAXgMMUXc1jk81h8yYfkOTqZ12e7T8QKHQaFbtytUt+8EPvmO3bl+nxUa8KwK
+ dN3tVrMQsWdoXWAB7Ql9MM0cj9pwzaBD13pAOFVLQKAnbnzRZfdFb+N+zm7lPNX8zCppksblAm
+ ZP9zLFOzTivkjaUM2uNf6lJ63h8dknW4pGe2/RUpovd88o+wXppDaIh5Vc/b8p6rUk9OgATtAA
+ nXGcza7fbtry8TLlYPTezZdgcQ8VTKrMa5sSk+7iIT1elLJoF/jXY5nv4BzTyuJHc+lcXes6dJ
+ SFXh4XvmNt9RP4RQjD8VYO3fVkUgt+EuEuSxpovXngd0FBogCKnFoABUAwbBahs8IfOlOCRKfP
+ U7iC7PaeeGheWgwiBD/ppvD9U92MlWUFmiclWzCKQgx8N2R/Aa8NiGYCIYwKqCBPLsACuLjXpo
+ QNvf4Dp8ZN9O3z0BNNYtEAG2A+h2c/nGGrdQDg54gNh+saIxinTpXB+wvCM9gK0FJjS0wj8OTN
+ lAcbuApqAsR/DaJ5pRo4CR5/c1cgqFmzsPkgBk4DXNCwWVjRGGcjOuQdP/XI+Pik+WWGm2/Ui7
+ TOK3EEoo1g0EmHaDdGC8wdYkT6jCkiy1eT9x1LgEtCggIImScA8Q9tIg7QIcNnnUw2ZqfN9gcL
+ uj78JBQ0mm9lc9ShMWkTguGsgr9frSxZcQUB7zW7fvmbf+e637fr1HfoF4Q8XuMxqmPDd55Q02
+ e8HjZNy4ykQxmcNbl8VMzg63wUsVO3Z6v/cz8/r6DNgHJV7osTJUEb6KMm+Im0KZ57vPOAnDdr
+ MorsI+FgMMjsYn79IH5MufLFDSH/2XGefHAvVzWl4SKKXtTkHl2hJACnlRH71qPpbrSWCPRtow
+ 66tNevkIDF928CAFas1hZCw4YmJWWrUodTRaD+pD1RtSJrCTVOCZ4wUI3hHzIUF6JKumMhQCw6
+ S3gNgDB5Tq1QhAQipBKLGe0xlCCdk8fqQwQ0GpDpkZ4zqWzcah1h8J4H/M3jc1UF4f2oo5glIU
+ dVD2UOuGzsFhmsYG7Ss7gHKsBSGEoWfU7JL9AsiLYr9CDwnFCmYEEY/APbHngnbOz6xk8NjLia
+ onLEbCK08dkBQcWABwzEEBYUFjQNZCdiLpiF/Pp5Ybzi0/mDIhi14+0jL4tRm0Gg+JJTd/tORM
+ 3g9dxaTckaNypnL/9SodS28m7Rx4x2WA0nxLO6YIEEFUFoZyxO+4MdZU7zhpS8DNswnKEOYDWk
+ PthEoe90fyqDMFGtSuScqFQGcOPyUwgm6Isrt80ocv8QSt0jaTaOAoaumFGRYdKlUogRW3kQ41
+ qDBVlbb9trrr9i7775tW9trVvLPxGPkQMZdrtsJxEBXVNDx/YsatKqKvUy7YJH4vQ3aAPJzNM7
+ 5Bu55Sic5l3ztlL5Jqn5/Y+cbtgE+TzVonwn20Z/BCynEZ/GhF3H2Gbh/TuNk1850FJ50hp988
+ LQAVFzQuAwRN1cqanCq0WgKULoHtlwr20qnw4hCUBMVWgrI8AwXfrlWJXVBqR6Nq8bkzVlBeyO
+ Y9ItLLbWNB50gmic3g1GbHB+l8AHwowEMkNQkLn4WBmuQOsIwjNU4smlBc/QRvDKh1C2GvPAi/
+ Jr2DVICyd8n8nFTXTVAOewUgGxsVEJPTUsA2hyykueswMSrfA6I6XsEAq+MwwYAR1WuOapasJa
+ yyYoJWIAzp3rl849KEV46aPjBcgHBLJDDopKP5KkBdfUjNwybs0EL+eUpFjosBJzslGWxwEuZu
+ 6rU3LQuUbCoAMDnZK8GXuqgGpzn4Clyzbtn1OgmTGIL8XwZEDhXjeFY0i8njOwyenUu8tQCiJZ
+ LwJ7h4+7SmZHoBdhLpeVTvHoCAXui8kkQUYNZGbAPkE/6U+k0mBOJahvyveBEuYWNLEEguQTVq
+ RSx2GnhXIJ2RFN9+9Kmvf32m/bG66/aykrbSgwqP09dpJr582DveBosvYwG/U34RizzETJ2B1H
+ Zn1sMzr92Wq3rnC38PJrXST/HPSmzOvtnVffnfXqSCj3bb0hdL5/i9J2Td7mclmnuELP49Rzss
+ 0fjc3ztDalklRa3Cu4eTTpU2vVmy5aW2pRh8meojHsnVs3PrdNesipsCKoVq3A4p2iVCmyJJwo
+ 1qTbIoePf4JQBvLpgNcSEiw1VP6ZjFTCNoau8BlTonCg/fNAulGDm8xpyqpa5bcZNhmYyIxEZH
+ CHKBYsEfXegkDkbstFJztibtwA7hptjtsAzc0kpuG0zB2KCa2b4tUJFJCqaqTHrtsp4/2j4ouK
+ Wk5sa0aBmQINwEXMnR+6KOP0rNY8CSgSs2A0AOEA3gb7C4oBdCSV9Lt0kdcK/BRuMptbtn1n/D
+ FQNdhFI5gJXPyS904f7JYFeFTmN5ibYXcS0Zviw6DIhiPtQFYEUC3LYVDtAsbmYALg8X1LgXgS
+ L5MZMWJO0T0QbY7dqAI2l2EM3gHPpra4TxQZqAUIASWpOJqklprMXwV5BKHHpp/4raaMyixi+4
+ 4k36z762s1634D3A/KRMRuhoTq8Z2bNYn6ESizNiDD9C0qwQt5aSw278cIV+/o7b9mLt68zTQt
+ U3lNrDcFUxoQLjdALKncck/gconX8T/QWMuUvF4NYeFMsd9buHLCfW4DS6l5MudSm3r9IdhH+p
+ FnqJ6icC8A+2SEk7yWklRcoaXg9Zit7vHxKr2UXwfQQLO7YnnP2GfjHwWcF53x5gLzUOTIWQ5O
+ pWmtYZ3mZssoR9cM5K46GVpyNySUjlQgqA0zWwpqXdgQ0yZrR8hcDUfIRljWA/ujGRmUtEzR8R
+ zd0+OegIka/D4/BewP1A76e0suqeHvsFhKDNR/QAvACAbjDmE3ttNeX/p9pQGriaZJWSUMx0cp
+ BF3fOVKCKWy46X5FE5eEW99zbaDzKGkH0B2MDCdpS6ACs8QcAzqEm7ALoWZ8jBQQ6Bj43iBkE1
+ w95arVc5vOB8yc/72Ej1NQzdDxvZ+OpnQ5AG0wkt3RbYywyOM7k8zns5EUbQ0fQrE4HePC+wtM
+ lWhOpIkbzDeqH6P3qWvGdgbuXMtAl3DoJQBlOPO4/tyuI0RkpgNwJk1rz9C8M8mKXJX28rg+Cv
+ Vf2oZsP/3otUnJJ1fLmC9i5Sp4LykJjNugtvyq9Sxpgh+dL4jUd7Ll4czej3RcWZ86IwGvfbaW
+ h8treXrc7L92yr775Zbt2dduadYTfONgn70HVahbss9U1dmGpjYCrUxyYs2YD5xunQfssfj9Iq
+ sz0LY/HIm+fpXF4V9J7PtXJpM/pC0CWQgqK/Rk2CknnhteFSy/j9/XBU4RiA3ehlF8sXtlEOk/
+ JZX79OY2T3gY8kb5t5cVB7xiYmiHEW1Wdgkkq9MBRha7GZHE6NrDt9VrVttfXbLnVFP0CG2TPL
+ ZWPuHxxUNHIYwYVt6yGSeUweFzpUeL2J5L2AYRQJQ3H4ti5MM1JKQHsUd3SZM0nGbODT3KAHFG
+ +iNfDDQj6BzAQvDnAHt/DwgRCJVkAsLCwKSmeGAAPCgOUEuP6XG8PT5/oMfDi90AW6vBBvbBKB
+ 60zoQIIiyqasrHLARijWqQX/eCMvjrw0cfXoHgwxQzQB/ij+ofDJI+HN3+xQ0LmAACf8XsI9cZ
+ 7I2WkgaczUEJU48xsiLAULDQGtY4DfMaWIKtikVJKXZ24sXmuvNHp9i2SIDpoQzMe92riacIb3
+ 9udSSnmdgwO7qrw3eHRvxeNW2XBYhBPIMXdz+8D+wWoz+jskwL4nBTTrY9TiAiXybQnwJ2HD4W
+ B1vrf7L0JcyN5cuXpAHFfvJl3Zp3d1Yc01162Zvv9P8FqVqOWTC2pqztvHiBAHCTXfs/dI/4Ay
+ cqsVq9mNJu0rs5MEgQCgYjn7s+fP9cglJa238TnQY+GSs30b8CRBvv3v/jGfvPbX9i3X72yZ0+
+ PZCPO9rVsZlbh6CfAPnn3yKmLhR4pl92upup/O81T/vzng70oLw3/FXPLWzTO3UCTjf4ikEQQu
+ Bfs48LxS2Qb7KsyILVYdSCPxn9+Y5v6/5LZV1gfJXV4gahUk+pjZHv7BwJJmoBQI8jkoE4AaT5
+ 7shhbL9Ro3B2P7dWTxxoAur6Ca75VVirvDzUfk15pWgdbBXa0rq/DshcTrPSvcc0/lIY3r3zid
+ nFFlooxGmU0FJF76mgXq1E5uISvGqS5JXtd2nx2FQNRXmpLwRM+7um/T9Bwvt8BiIAkiko7Q52
+ fl9KnomI8owOAfLWgOzemblrqDKgjfOVF89C8u7Wr2aVdr67l4kmQIQufKQDEYxcLbzKzZk/bj
+ daamIXb13PwXFAZOiaqhpmCAE1tgjLw1MINtEkl41PQDvZXmtylWYsthJwqzWwhaVNIJqPJ6ze
+ Zg4Hom9A26zMJiwSfek3uwG9KWSyEJDBp2c3MsITRfIVkzR3oWd4tHj8DR3r4q5Jw6aX/L/oJ8
+ XeXXcZz5uKT6Cc8lNlnZVGmPPWYX9Bb0TPSHZJVRzSdcxUmnztJBEmFejNsydKegpW1ui0bjgf
+ 2w69/Yb/96x/s6cmxHe6PbULCFJVj3eT0ayoz+1JV4ucxeWo/z1IFBR5WxhEFoG83SXN2osbRu
+ O+rD2szO6+B02kll7r6Y6pxtm3OPoLKRmP3HmWOFxERfLI02gL6EvCdxonjC9O78vehHaufljE
+ iC8svmX1982XkFz+rzBTqpmdHR8fS2s/nM8nIAP5+nzV+Dm5kOUgMAaXDvX17fHxkfQaoFnPpi
+ Fn5BzAxQIT8TEsuFCiQrEFPYGwV1rcxGQnlkTEcQJM0jUCxvLarJdp4phF7UvuI5pFT5Eo3IwC
+ aMkwyTIEtDp2DgcCP5qZM10LRA50DPUXgEJjlztu4kVze7ZpyATkZfgx66fYj49ZqQOevc/OUN
+ OfySAEAfD2dBp/Q9t/eaoCKY58vr6TBpxHL3AHnCukq52clw7lY9iFHy5VsGNwszhVMNzesH7z
+ SDYhdAi1t9t92un2ntqSr98XsrrenYbvQOZP7JeUxvvqrhaaIc9rYt0F5HyQdYtRdiWomh9c2r
+ 6BQ2WwkZSWN439P2kWnNkv9pCNi9WHq7XlNvVZ47JcAnZr6h2icm0+AfSXLrFCt2FyVwSsDiLj
+ FANfw8Gk1WxqE4nlonvNf2lbI1O9mbcPx0E4eH9mzF4/t17/9wb5++cyG3Y4N+lR2m5m9Nx3d8
+ Wfb292bp3Uj38907fQYep4qe/fYVNNU21SOQ3fNjGRztqRKKl49Hqhl6/E79exy0QjOH1YBIMC
+ 88s2pL4wwyKhfruHzK5VsdOvv6k1VwWEzTNdC3vggNws2Fw18AfuIlSVvWQyicMPt7u3bwf6Bw
+ GqqTNMCaJ0WkcWwfNOXdrA7kfwS1Xr79sbG/Z7zzauVLyIf9CXj5OZwG9+Y2A3OHhoIrhyJJBS
+ R5I0sZmYDFENEtw0NDnGFyBMn7B2oGES1aPCqZ7rJGeBqsO7QqSSydpqVAB3HgcyR/gEVAkFD8
+ rm0IIjMMm4nn3BMCWjqx5O/JrOXn7zTQgBxDhw5J+9e71RAHDvul5Jc0jdoNvU9dt+iw59eTHU
+ 597tYTDRURaDiQNlEX2Axn9piBs3jFg1SDxmNZ/bjAvzOVQ/He9YfjmTphgnXfDF3Yzm7cTkg2
+ 7egenDAZFcvtgrLuY4NekrZZeyapWoQZcLnEIZpNMdaDdRKrpQqMKOmCuK+q+WLVe0eGvqgRra
+ ysJyglaVCcP5unpeZveODnjcnZiPb/7mZfap2qoC17bEex1YpdkRveoObL/pDXHt+TrkHUhTgK
+ qLV9UJLSr767pX1Bx1x9n/9mx+sg8Q0/PClgKoAPpbKF2DvmJcBMyYDAsRLl1D3ZCtpmkLrXjZ
+ ss7WqTLhukvuH+NOcvV49XqduBkeAD6C/l8bZ8NLJ58jEIKN92iVEfu7ZZ/XRKLPf5mbqTMOlv
+ PH+yfKrc5b1wBew3wT7krPPv8sEbTIJczKybFbzeUYtGSHbeMh0d5oCevztmywkbzZsfzy2Pnx
+ zLHLQYMnuRDtYMeoC8JmsJUMCcFDx4D/jWTDAE1w5zV159pCBsquWRi1N1vD7DvkeGSBafmnVt
+ XbPVRHYAwP8l1dzAf5kb0/7Qrl2cMbEk0YUTRnoctF1yP8ELNHArstuZ6HJnAl4aVPMWZWNg0C
+ eSsD14jRYr+Dhgy6TnzwBYLWw2eVM/jkcd3rvAPqj8dC6UDJIKKdTu7le+hpCZhPCE55JXXnXI
+ I9F5jeeWLc30lYv9p+u1wtRQJw7ARPViUF30Xe51YKWxfLKeXhVKiu3qu51NAUsVdBqIZmp+xk
+ hmUQ95UNOG2BfAYJfW6GgrdqkWW6nT00QM37+xUwEjRPyRno/2vAV4M5rqzn8OWAf1mkJEhthq
+ TBLewjsVcUk2AbAimoKGkeLb25uVfFy/WrBOuclBr+scW1Pnj+2r777WlYJ8Pa/+sV3mkVhB0J
+ FN8ZQFedGPYufAvuqUCKrr6mNDbAP4M5sXgGl5OyrD+wesH9QieO/5NbDm413wexGVl8HxLo6q
+ Eu4fPx9nH1AvT/fBti7THYbxAu8D68gV5J9AfvyzNz5e+ZLnhGofNcUa9v6/UHslWXohWy4rSg
+ KwFG6CmAGPdsdT6yHdni5ENgf77P4pFcZXVHiklXzfKlUkI5/Z0d8PNWeL4Jwfx5RCcGVu1dOG
+ pDdqqEKlcNxrFn0IZtYlD4+Qs+X9sfeYMfsGZi489tbG47H0uoDtkzLtpstn3qNcfXMkQQqwdc
+ n0KtZmYolpm5jUxNBxKkcV2W4Phw99k5lq8wyFzzz1dC78YXnc6ql8FO5nLoXjmYGsJ4A7OF2y
+ b7ncwFvp4MzaVPafbcAdn8eKhTOLV5EvdHYdjo9W1zf2Pn5mZq8bOO6nE1NOnxNuWIL0WUMVHw
+ +Q1/Zr9BUcOPWF3GoWc6U646OAZWQtljppaHjWAcZ6plKkCeYr7j1GvT9/EjxxEBULE7xma1Qt
+ XANRNWkuYkcdst5iwoDeFdeyVQSUQ1Z+b/lwhnbs+pLPfYAR+gpaZwEl3RH1u+IYoztUQH2/pk
+ 6fcK9AMBfXs6jUvXg5jbV19brd+zxs2N78eqFPXpyaN98/dKePXkkW23JaUPp7xJm6DS/5zKYb
+ dIofk43M+ca/LIKygw9KZEK8O80aOMzKtU3G1LJpHgKtU6qYQqVTU33V9+MAqHO4L1ULBQ81fV
+ SeujUe3erHP1ngL2/z5omLMFeP/uS2ReIXyvUNhRODvgoVfCpR0bproUAKmUrwERGCNDsTnat3
+ 9mx5nppvabZyf7ERsNB+N473QNwMZDFFQGXTSNUGmVl8+7eKPfNXGfIou8AZudofWQfGoemKhn
+ +Yu3UiLJb0Rk1veA2D0tl8C4d7UtLz+sDBnD82A5QpfAcatJK/eN++WSzsn24ky25QojBJ6ZUt
+ ZkoFnnwegCwhsraHfU7eO+AKrw4wAzYf/zwwWbzubWhbezWpnLFvPTl2yifmsws9BQ0qAiQceL
+ xgyJIYC+PG6+AoKEYshqNJjYY77EF2+bra7ucXYhaoq+Cjw5rCucLaJtr9WQ6A+f2BfhhhUGAo
+ zmsvb4Npj/btr+/p/cxm17YDsEMygJOv7GqfIwScOpRpeiaVq42sUlKlIyDjTL7dMysKIZCehn
+ Xmt/3bn6VzT1+lzaFArHn4D4VWzlkpvCypgNu2HVa5dQBeFX27pEkg0cCJ+9L115aIwRtKCvu9
+ Y2q1FzOo+UuohSvpa1/8uzEvvrmhb18+cRePn9iB5Nd2VYTSNlLpZhy64G1EdPI9TBV5LnV4Zd
+ gX9dTohsfoHH8st3K7ANE85wF+VNl0/W1nvhQc/VlFRTlgp+muD+yeatvyvgqDr4C+zwWD5iZK
+ OiTiMCaMaVKqrayfAfy/K8I5UVwqENLvP8vYB8nqgT6+hqqziIXH1x4vzes1s3Bd2cTUvtc+wP
+ 53vfYQHS7tp7d2MnBrrT3UDDpPwNgpV+6FpmLLujrBoNOgCOWVhs6SHr6tUA6pzjRlvO9nIwlS
+ 8YzRnJOpmA3biJfrA2Q0SD2lYkDASEBS2JQSSldtZKj/byWgg6PYaMVlQHWvcXrSmoHgM/cjlh
+ TumqGOneblRF/n+JeqUwcGaTf2FQCr1+/Fui3kbfekCFe2uXlVDcuTVrmFDQ30NoRrQXQ8298+
+ WUfwY7ea1f7aOBtMLR+f2ijvX3b6XTtarW28+m5nZ2dqirI5eeatr3yTUoDaKJeT4tVsulO4Pv
+ 4/r3eHxk9N9fu3lhgyzKWhrz7eT+A2tL9bKqpWW8M1gRDTLAqk/eLKy83r97c1ExtG/0geQoHf
+ G8E+w3rk75xmytYx3rHAuz1/AH2d43QANaHwV5Omt6IqHoRrgpzdlv7EkKxpbmJW+yMfTmON7L
+ 93zLh2zEb7w7txcsn9tU3z+3Vy2f21YunWvHpS07oSfjkrYYH6VE49rtR3J1MPCmXgsOPO/TPB
+ 3t/gg2uP4NGnPMaSrdet9oW5r+f+vsKsBNaSi5/IyDVNFQGKokmquw8ejP5mZdA/hlg7/bPRSD
+ 4AvafB/YqWdsd29s9EH2SqhxAGkDgi6UluCMC9q3GjXXs2sEewy7p4H36lVtNNE0ALFktGaZPi
+ Hoz0Ad5bvX9tXIAACAASURBVPWafhNFNXHrWToKG75y4Orq2gMC1Ydkii2f7tXYejR5c0JWTpl
+ tX0qufBBlUNoYpK1yTHRqyCoklRpmIjhpixMZnStcmGrlIqWkJ3iQ6WvvqtYyrkXVzFiasnTnQ
+ 1UWdmvnFxf29u3buLsxiUtq50LzBVhOUFVgGc15g3PHyx4a6ez0o10vVtbBe2jH5aWaJu72RU/
+ 1h2PrDkeia3gdqBzAXrMCcVwO+EsbjQc6b2Sn5Z7P8/MLgRjcPQG2P+gqcJ9+eO/mb0hg9VkF/
+ VYNVcWe2QqsSkd5L9XLta8algv49xm7OhjIuiIXlcS0drApXnXFMLLbH3tm741cpl3ze3X80H7
+ aap6kbFAGkMkbSM+QLFS1glB0RTqxIjCgR0OwEdhDN1LZ+b9V/bYbNpwM7MnTY3v11VP77ruv7
+ dXzJ+4bRfLAEJn5wFguaGl1ItsNYNukIjzkVDRO8idVjltn72UVmnCXQaukgTaL+aBrtl77Dvd
+ dNIu9nxEBOpbveH+grqQyu9+kpJxqrd9LcPwVrZ+1Rl4rpfNmBJetZnQZHMsAln//QuPklfCJz
+ J6HYZkwHk3khwMw8Cuj4djljPO5snUu4n63Y302LDVv7XB37DQEZmSxzae9g395vZwBXbjTH87
+ Dk/W6oZRzwcmrAtapnnAbAbf+5caCxtEuWm1Y4qpz8zPfMuUWx+J/Ud6Iv3ffG5WxZJYoZch84
+ 8srGd9v6zYLUDBz9QR4nxwXDU2CE8eZxmsKMHI99GlkggEbsKBmuEShunhOfg+7Y5aZi3dvNm3
+ EIotmUx74PsCFhh6wj2qlaQJ7zsHphw+yZCCbTrtoPHMADuYXsE7u9ofyrZfSZrFQdeNBkr2o2
+ EbMJANVkxE5rPx9HIx9raCfM2Sql/OZburDvV17//aNGsru/kmgZMYACsyXlWOs5msTo5G3paA
+ ok/fM7Cue3YdjHb4CRHygKzL7kOaq/K8siOuhsAT7tDdOEC13nTrgO3By/PlcAih6KXENZEO4q
+ igy688kggSB5e1aYOOKK4kV2OgOrdJuWn/c0xrCr755al+9emEvnz22CeslRc2xGwCpcFSt2qn
+ sjrAbGam/+TgdEdCKh+gnkRFvN2EFpnH+t8G+eo0E97LJWoJ4VWjVL6pzo6AdnvLxeVQgy8vKN
+ 2nT+qAKDDpkDPgi+y6riY2+QL749kBYXCMJ+HGONs7bFqXzBew/E+xdV84e2JhYpepmy9JwLEC
+ dXVwoGwIIeyhuel0bdVt2MGFvqq9qI8ulBO62aPK6vwgfJU6a6XMvkzVUPqGbd6+YKKNDGSGPn
+ lbbLQhkU+ADWOLrQ/ePIkJfYanMjZgqIl924s6bqiAYvFqylvDcm7UxBs+fADPZLZl50jpQHlA
+ ms9mlsna+4GhRvaB20WBXu+u+NFdXdn7u1sYEK+YT+DlZ88XFuTe35fGzY6PxSINh6muYKRNPe
+ 2F3EG7oMVQ856dnClCcGVwjVb3EINhoMrEOfkFN9yCCKluvGbhiw5eP9U/Pz2W0htqEyV7+TtB
+ BlaSAM79S0GD4jODI+yQYPjo61OYtKoncMyywj+qLLJxJZQzjNBilVZdZTgfEFoqnenOU90/Cb
+ sjBOEGoWkEYQ1c5ZVvJL9Pmw4HQezGFgdsWECi5ENCH52VFDXlzN90k9GzRS9D0OBPa4a3P5+3
+ UjVt+uFW2v74G23juVtMme0N79vyxPX12ZE+enCiz35vQxxoGRehNXu8R3A/2ZcaqMLVFr2QO/
+ BBnX2X59zRo44zV5/oB4K+ZtTLKhHVDBOdscJd5vVs71MBc/90z+e0mav1e6x5BRTEVQfA+zr7
+ uGfitf6ci+ULjBNp/RmYPgCKZhIqAr6f0Z9iKkwp4kImSJXKzD7tt2x10bTIcSGevmyCkgh3AP
+ rJ7z7x8WALJpEzPuHEUTJgi9clVV1fAa/K6bVEhXATTKZRH+u8490tl4C6d7ozJhapNVfjfyHv
+ HL750oNSgE72C9Up9B5rFHifgUJsuqby+VjOYTD3tjmeXl5o74P2TKeOyKaCW+RsTxu5lA+UFD
+ ULgyDH7BE8tGJE/jS8Xdx38woe+CHThrSOfHd4b577BFO5cfD7nibco2wcsH7o9GdVJDho8sga
+ BqGQ0wONDQSxAXy+wYjCbzQB+6DOzLuZ2zR2bXs51PNodjFKK93d5aY+Pj+309IM7cQr4qEBcC
+ isHS6wBkL/yn4zNQq8fmfT2ZcbnEIKiWGAe+vlMQoI68eQ1wT6y/ILbTerPM/i6wVqrceoM8Va
+ cvVM4GgyqfHi8USpKKeNG/FXaf+1kgE7ztY+cEzh7Xb+qEPy5XDnrW8WOTvbtyfNHNhy27emTE
+ 3v+7JEdHxwK7LkeGxj7NX0oS/2Jtn8+ma1XpyGzc478Hi69spzeojayMXtHeln2IcrAGlVCNsG
+ r/kng9QaAphFZ2CfA2WOjUL1WZP96qeL7VRAvePfN91w34AO1nX3/BNhvA/wXsC/rnPLvnwB7X
+ YhMCzZb3oQNKeZgMHL++eNH/TkejqzVahpsOIA/6LKYI5Q83CioO/B6oYkbVI5knDseQLiRpDp
+ hcIvNSCh+GCCK6VSyYjVMux31B6BWAGWeQ2oYwkb2B1DZhE2x/EtCMifVjcG5u7c8TVHojdFo5
+ Fr42UxBQXbLsbpP9NRg4BOmsSgEMGUgCu8WgF6KF2Xbvr+WqsPpKAdwKgunpLyXALB4kIDiMV/
+ EwkDZYuGrELVNa+bGZ3JOjKw1pmcBV76P3l0XerujgTXUNXJejGYyU8rNFpvC4Im92fnmTz/a5
+ cWZLBkYplLAYVCt0/VZBpriNI1DDsn5B+wfHR7b+flHVTG+wpKP3VVLUuI00N874LPIpvLKz0y
+ rlPnF9ecrjd1R0pu1XElxQUbvJMHeF3LXDdrM+tT4ToQOsM/sfuPSlmrWTfj4jbRlSGAAtKtFO
+ MX9oWne9KxnuE1+S1CZgLPPGyhTlTrIl5l0+107fnxoj/Ct75gdHezaydG+PTo+sfFo6DRhh/6
+ Nf0HreIM2fIfuoWoys4/ctZKh++KTQvteBLEEwZLiSRANDK9os7pRm9TRFqVUZel82FGphTGa6
+ +9rHX5m+Pn9UjNfZuYJ9E6rlnRNHfQ8+CWdl0ddHlt9XZTvrQyWX2icPBt5b0UG5ldOzZW6KgK
+ vmLZM0FB9cGNq2Qg64/NzNdJGg6E3SGnQtpoCe/hJ5JcECWR70C+DGBd3FQHpIM3NaKjGQhMf9
+ ZbTSyyDSJ8cMrxQLKBsAfh7vchkfbiL7JosTIqH2G+a+mllUdAkom4uRGEgBeU5ADSA2+cIvMH
+ qHjwMaiF/vNLPVZE0mOZdCDD4vqSo8N6xsAPveMAJACWAuTmWVypQYoyei4ZS0PEmty83dzkoz
+ 0MgSH90jl97AKBfIisl49+5jcFzLJnbbev0+9J8A3a8Jx/JR8qKph5f/2t7+/pPdnb20RebXy9
+ EaUnuyWPSrE420q5lJ/2G498b7yoAaXG7bKsBuxju4qhimTzflwFe+NArMdNSK0WHeq2feF2v5
+ kSHhG9+UjE+YFV1/sI2IxqRflf7FSuKL/J1XcvuZlowRv64Ch+8X5NgX90GcslMKWdSSdlMdJt
+ pgrxWUIbbKBCbiiXZKfOZd7t2eHRg+4cs+Bnb4eGuTSZD2x0P7PDgwCbjsVfIuZ9BvQ53wEwzu
+ Dv5l6fIBWaWALcJ9hWlscGFl9RG3UK9l0pJ/r6ideJcb+Kr902qAO7PqXMc39ukVmq9fvUJFly
+ /oH2rqtlsRm+CfRkA8nfziiiDRhXsvtA4cZkn2Ofdoayt3t6ksrTp9ISoBC0E98hL5opDIyW71
+ uuhZ9fNDvj35YTZ67bl3w2Ysq8VHn+sYSGnahpaJkKGCHjQ5PJsluGkbqunrN9z4mYsP8cBEm2
+ 9l77d3jCGWm4lseRmEziCyeLtAa7YLJV+8MuVuGjeJ745NQ/rhmeaIbi51s9EZWiKdCUeXoDNs
+ FTsfoWucW8d+gaeaaOfpyqRMRs0DPMI4QuPioOGqhamNwkW3hNAK8+qOoFA2Dg7XeD2EgDtckG
+ jGCrMPdab63AkRaraallvMKi456bet9MTZPgA6+VsYednZz41y3Tskr6DBy8//z40RODgOPDAX
+ 8USFW+Ii6iWdFTNxIY7k5LopfzQPfJ9g5PsiQX2zt9noObGFotHnwCfHiojFn2IEnGqpaYTguZ
+ QT8Y3ayXbUg9G+SBVBfxbaJlDW9kP0DFVyhGndTTkpb/6MFUYwKva4T9fIMPaR682Uz3k09xNy
+ W75czDo2ZOnj2w8JtHp2dOnj21vb6ykZzwe2+6ECeeugqOstbWq03es+mfvn9n9nHaGJk92k7N
+ PgE1wy1hYAm6pxJHkOMNllTXns215ypevk5Ahd1L/RxqkVZVWUcn5I+6qaap3sfXa2UiuY7m/S
+ PX9DCT32Cfcd77ydb5k9lVKkx9J/BlcaSU1UwOuKY06vLlvOuLGBchulW1Swnc7AK2PgmOTsL8
+ 7sS7c+3olfbFfXDdS7UhrHFp3JJbLKwc1wMBX+LnXOiBKduzul26I5k6TaMsB14Y10PzHRbMN9
+ lIBxRBTtQNWfLhPv3KBit8mE+52nd7BLOyKXbVmw9HIrYgjA5exmCZMCTJdBaWL6aXeE6sCFZh
+ uTQoWmrDSUJM17+zoMXzJuA3wl+zOt1K53I8md0vvi4yWAISOH9UDvjVTno/eSK8jQMIUzZbXo
+ nJa3Y7eJ5XXDgvTkYfm6kRN8/JaDfn5o8zBNI33Mpuf1oqiyLCl6uG9aHPYraZ8c51jJJi+1lE
+ zoD7QpVtSXmF+y8vKOiS7am7e+jYvJp19QjocNXe6SiCofrByqJqBMamcTpc8q4btEuxrzKuM1
+ XJAS8C9Afb1v2W8FnQEx6dstFrSkvYIXKi1FwvnTRJLLVKP1YPX3kfiuuP4uS6X64XAfjQa2PH
+ Jvp2cHNje7siOjg5tOOzaeDQS2A+HA1WTXk25L1PSQaKL4pgebmDmfZraJUfjmusvp15rsM2fi
+ 8v/OWDvT7EhqaQfVs4gbGTzHnEKUCnB/n5qKN+rB+AIIgVtVH2/oos2G7wbQa6q+aoo9WWCtrp
+ fihtDgMw5is1R4tLUJPUyH/B1zTpr/NoCDJZec1PS4IRrt+uVjYZ9e3x0pDH92flZlc13OtBBO
+ 6IwWp22MnFMtQB7NzlzHxl05bw25S1grw1UUjtg74tu5UbqEdYdkoVy0Ln/E64e4EsJpox/d1o
+ yGpviJImkMLZdAaJuTobaqKuhJtQouGsC3AAMDVMoFjh7AgR/ckkSCDhXvnGroUXZNK5R4pDZX
+ 0xdqw6owMNjJ+Haf8bpfbkGCh6osC7TthikteC7PRuWkkbmWqamLGDvqqeWvGr43s2S529qPSP
+ niNfnvCrAobZBOULGHFUY741zz2pZZgIu5md2he8Nj9VMAlQEx0Gz1i2pOT9aPhNAhIWvD2hdW
+ +MGm4l6gCXXHaZKCnAXgMl+A+oPcIvBJmaP2q7wghIjGPOZO4g67SBjNmfHfdgud+ZmTlnvf6/
+ sEyqwzyXqAZ78SoK96Lbgur1KqDoFUeWErUMYzKGl935NLDxHdRMeLLmWcHXt+4LH46HtH0zsF
+ 7/42g4Pdu3wYN+aO7dqzB7w92ZTcyk+GEhwj4oqm68xxfu5YJ9At5HlJ9jGTV42a5UpF9Cd2f+
+ 9VUAFvaWyJmSVxQLz9MypoLrg8Ddoluq4qqP1QFJk+CXfX2XmOW2bgSQavA9l8056Fc/7hcbJE
+ Fr33f0EeYbmH1KoG7gxd9wnh+yRJiSyMbjv6RSzLdQsfQ3eQFlMxgPZHS/mVzY9OxW9ox21XZe
+ aIT/caTVtb29PRmjcRLO5j+hroUlkE27529eQUTO81PkZQMlN1uWmCeAGYMgQ4ZmZB9BkrhQSv
+ p/27OzcLgDqnaYCiHxNplNrN31QihdlkhS6CcB2H/8rHSP8N+ofHgdvzxffI4XUFiwAlf22ZNU
+ hx5xhjyD/+rWNBgM7Ojyshq94DAtFoEOYO8AHRw6XcuHETCumSwFgtPKhkyezF820vFKAxCBNN
+ g8Et/i8dF560G2+3YdslKYrQSktLtDfY752Or+wOb4+McykhTSiEHwfcBq8qcoLu2E+I8daX6s
+ YrIq+5WAfpXf4v/MAGrdifaTSySUlmLe5SoljJLCkukhy2dielmBP4HX/It8FzEmqssD0yYkgo
+ Mz+E2CvSqRINKthL8/3wx/fe0l+7qBvChO2aCRnsCMBoZE+HPa1XPzXv/6FHR3t2cEew3CmIEC
+ WT3Ans+e6EeDr2ktO2qFONFZxcCUYVkmaFm/nG7jLeZdAWXHi8fjor26A7J3suHz9quSKTxdcK
+ PxuIvUvQ0NVQeXNnBVVec7vViNbzdYIyNnUTVzw3btlwNiuGOp/K9B9Afv6shG0h9SM83tXoOO
+ 7aOHtB9gY08zTxCD+K6hUFsreNQ6+s2N7k5Ht7+1Jx728mktBszsZK2sFuE4/ngoAdnd33a641
+ RZ1IidIJlDDR13Kjp0dNXo7WnnnoKztTm3Awp0vM5IDToD9eDS23d09ZafsZCWAnF9M7UJGZK6
+ uOL84V7Y/Ho413ALYzK6w+l3oQhKdM5/b4dGhaIHcpZvVTpb0mg1AcxdyScp6GtL4x5+endn0/
+ ELTrrxXAp6CE2sGZ7xfVin6QBrlP+dQee8tHL9PGiuzvvbgpl4Ghm7rlQe2kKqSZatpmLsIcCb
+ VNLEPlUHdyMZBxml8ZjO7nF7ax/mlwF77XG9w8FzbKqyStfYwKC6pnGJqOd1ElRSQhUcvQoYBR
+ RNWwSZWPyIxFMevBrUbq3E93agBnwtjcteru51GRKkcMWXJsBO9hVhdqe1NCjylbDNom22wz+s
+ 6s87I6LXfNid7o8eRg2VJNykYSVcZ7zEqXp/hCHsDKb6gA9vK7H/1q++DyhnbTquh6//46EifE
+ b2g1g4VcjFcFM1rzrTWwn8G2PubD4Dc4r83wX4re96icT6V1W/TOB65C4DdonmqY79DL92lcTb
+ VNiVIF9RRBqkK7T9B42xXNl/Avgb70knC21X1HaTIqBIaAPGR/2rxiKYGffUewNxjsKrdtv2JN
+ 6LYzyY3SxqhobvHyvf09FRNPCx8oQeQNkI3aFdqcOIACRuwWnD3LfT79TYpTToy1Ri7Zl237Fk
+ Y3yOAkN1Dl0DNkOGzdFveOmG78OHjBwWB3d19UQkarpJ237X9PvR0o2pGSosggjlWgpx6C/Lua
+ fuaJIASOWa7owABtTKdXtrZ6ZkaelQTbPOC6tLkbZi8daGodhpqZms1IoNKcpv0Bq7MJRs7op7
+ y98jwWVso2SFUDTYS2Cbw2QkofdgMGgUwJTAI7PW+lqpOqCzORFktBfICek0AN0TpsNmK85VNT
+ f7iy9zddsHHGLzXIOWOeFxXrejnEZw9S/esXO9L09ROYVyHGZh7/NOoZzLVd/hqyY1Az5u1nIu
+ SxklVUl7FG9dtMX1dcviVIkdg7Xy9FGGxjUMUIscfO2TdedWdTNk0poKC9xKWH8nbixbquNoIs
+ B9PBvbtt6/s+Hhf4oRur22TycgenZwosJMMIBxwRVOhjokK6t8a7MusfkMFU0WMhzl7IcWW9FM
+ N/+L7ft7vWhQrYXAex8NWgnnxd/dTi2rxczn7L2Bfg/vG3xjOqaRpdVOjSpdkQoWMEYrBpyoF/
+ OETw3ldrBaSVXb0mKZN+n1d5DwvhT3qEUpXQHW2mCtjJptkPycl5Xg00L9xZNTEKUNR3PTLpWy
+ S+wSRyIC52bSUCXA3GsQL7cvNRplK8Bi952Fk5FwsV8uV6CkWmMDdU0nQi8ApUn2ENXz8tHawv
+ HZ7ZL7kdxMj8T7U5cEQcOCGFSiT/YW3PNQP26jAKwar1hiXwdVC1eDxLztn7x04ge82zY0GE8W
+ rGPhxUMlpQ86J34guh9Qe3JgiRsIJiJPho57RVCzBsu1NdOSCADdUhP8dtdGNTVlQHhQFTWO2V
+ 3HMa3ztr70iIDiqdsqsF5pNNr+cAwd2WHW2NlExaQuWbJrdstrVkMXUaph9ofDCmIz3h1WA004
+ +TerHSSPcASCniEXlhGRPE7rFhZxgn1lqrdQpXFBD3lgfUmSSkdlLZRPB3rN5AiS/z+fiz+NgT
+ 9WSbpueNavSbJponMGwK6Afj9yA7/j4wA4P9yW9pOKkSStDwLBLzupUVWdozksAvg+Mq10CG0B
+ ZZPDZkyi463wej231e0+efJNOKk5uFZDiyo+Zh6wsaqCOd1JZQ5drFusq5P6qY9NOOfF6Oxj4v
+ +tPvqyA8jyWVccXGqdE+5BRemitKFd/RJThcGRqJjE923VbgUF/6Jp0Aahr0LW8G61xu6P1ay0
+ yp5sbGw16aizKrqBJowwdh0MF7o1w2nDps/nCTk/P3WhqvVSgONybCCRb2O1Kn4yOHp8bXxQ+P
+ T2XEgZ1TKPlVAcld3L1PO5y7ha+/F0VxNKbkrPZlbW7Q217cm7WB5K8OeiZpZqXDExpKcpKNyM
+ 9iOHAPU4AAzVNoX9uqAS8kc1/nCe8cODXaaQOeqh2oFe4ZH25yXLOe8VOgkEuqgk3edPEq2gLr
+ 1h8M1cMUaWCBOuCWLTC68+uZjafXkoXLx2+wPdaTdnk7tVkjHtlfoXtBJVM7KuVLxwGve5XRGW
+ yJKgAgmTtyE1j14FcIsPbX4AuV0iXySr7j9VRHLcGyfQePEjL4kJe/+5hRM/BFV6u9ALoqZwUR
+ ophKilynD/yPQnpzxLXc00/Zm0ahstRlSkDDe2+mtHqLKoO0XNJ7w9dE/uVNbOQsU77zkMGGkH
+ MK0t6Ql510Ifa3R0rsx+PSUB29HnjjXNyfGTj8URNWvpJfF9AJFTzIO6qs7q5uEGvbPDXHoASb
+ B0Y76N+Cn17YnsB3JVCpwD/OrOvVTU6wqI/4FE2gH87q+dHuYRlK9BkUNho6FbZ/GbmXx6Hm/S
+ F6uieKuEO4H/J7EuE30qJitaK/7W4bTSI5G7lUhIwRStKhTLUtyPhsMjNiWUCzVQUBh1uAG4EG
+ lKYPMXF3B30SPm0pYqLAgdH+OXBaCRq4Y9//JOmNKEnJjS19nat12mpGQhgArBI2KBTyLwvTi9
+ EBel55ZMfA0yxlJpjPLu4lNpHWbm8epAOml3itNlo23TGFijPRvk/QMVllm4eRhbdamNjvLL1c
+ qGBsfF4FCsP25rAxc8GTCGzI0vvYC/R7njwCc+bHfTmAhfP4OfzhV3N3Q9d/Lp80Wg8E0jaypb
+ 5SlMvjg8+Py2ZbxYLi9asDOlOz07VdPY9vq4wkQIoNorRR0CRkhSYTLxcMK4GuDTlAmYfckKqy
+ VJ0gTUVFT0Eqon4LLVaMszTCCBYM/AlI7UwufPA6Rx8gr1LNNkXQGXkhnUuq8X+ouO9G1UqQeO
+ EG6Wa0ZHZU01l5l8BT4J6bL5yoiktEjaz3tUN1VqVY+q5qiClxvZKSUPM0GVxopeiygJqZboWw
+ Q8qjmsEQcHh0a4dHe8L7Nudpr16/sweP3qsvg2b36DrOu0S3CJw/QRPnxusEtjKyiXB/j7lS1a
+ Dte9YVO94HAQY65IIDNjoFQQ6l2CvnlVBt1SijhJDAuwTSSq6JoOylExJ4ejKuEPzlJSOZlhyQ
+ rcIWiWiPaTM0Vv4wtmX6VCcwSpg323R+id8a70eOuFBKCvIHNEhxAQl6wl3d71ERbvODXm7tlv
+ 5wLi9wHh34hlqlLBQCnzse/v74sn/8OOPGnjixp6MBhrOYlCLOpoLgGbnwf6+mlx8LReuySfLF
+ 7ceAJdTlJp+XSzs7OJCQOKLwAHvtvjq1XVDjVkNuUR2yXN4Ge9BAeD3AamVra7g5Wkadw2RJHT
+ V4SEqC3ePhLPVzayBGdQ5bjgGzUWg0ALyUJ/IsllgwTwA9A48/Y61+13JKN0oK6SBkkay49b9g
+ zg/PGfuiJ1NpzJJg57yx3hVkn0MzgwqI8BeQJWrGOOj9mXivhEkuesZls00hTVdCpfPYBH/Jvt
+ 13jvpNDes8Cye5yEoJyAl2DtHHZOW0EJk9lo848oUfpeeh1xPAV9l1t738WAR3L16ALmTtbjlg
+ 2pKEHOu17l0318QrpeyQY7J22LbVU5HqyEP0MfO4dTiZ+mrwKTKJk9eQ81jGs+d9o4dHu/Zixf
+ PbDDgWmjZk8cn9ujkkagcsntt/9IOnbzvHgb7GpAL3XgodqpQVSVoZSafz8336k1glQImwF7nN
+ arDfK06BDoglMCcYB+el3poTtJWKBLBPKuUbPBWP8/GeiW7TLCvAKhgYrwXVIO9P8sns/kycH4
+ B+88D+/R0IbpqmrbVtuFgVJl7QYmslvi3uGWArx7sCxyHuFQ2fC3eCptgbBVoTu3s2Pxy5qU//
+ vKdrioG+Pq379756+B7I5mkN31vr1fiSfudjgIK2nWtzqO8jolLKAtKvlzEATUEHQJov33/XuU
+ 26hYCAIqixWoZm5sW1aIQsmY4b4BYiyhCpw19QPMU4Pbhp6YoqqO9se2zMCRel0DANDF/6neMW
+ QRf6K2l3muaqm6Hi/N9o9eqtnnRF5Ekr80cQ6cCQAUuuS3GVqTgw7OhyfMyxMV0LFWE1ikyJBV
+ 9Br0Hgb1n+Z6RhvNj3tlOq6sfkOBG01bBKasDdPfLtUDfz4t7wKet8Dq2fqk6Qm0T9I0mpbFMk
+ NGcQwDvCRqHfwsk2YYWPQqazdpXIM8h168HaeDBQRm+s/RlxZ4Tw7U+y0l/f0UH5mqTmWTF9fr
+ LXHYiHyMG32LGIakaGrSiWTLyVCZqsiXVK/A+Wu2mTXaH8rBHenl44P+NJVoYC+x5LFVefez3g
+ L0j2gawbQPc54F9PM3GblYibvZdAuwjRS+powp6qy1SVVqtpKFcNsJJzn9HdNhcRlIqdkI2q5e
+ UCKveXLXxgcYpcO8h1Rep/Sy80YLe2TwZdZ3xRXq5mQ1lGexXVkJD+Zi6KQJnjtIF6gKAkkpjc
+ AAAIABJREFUB8gYIvL1gYyaY+zFkoa2TbBFoGm4mGuaFh05yhqKMsb/lQGih99pefN2NrOz8ws
+ pVpRdsV8V+SUcNmCPhUG3IzMpvMGhjeTo6BgVN7PbJpMpw+PLhnk+s/f4wK/W4usBUrc8uLFFU
+ AaoYsiAyHKY5pxf+famzI7JfDEP8+UiPQH57XphR7sjGw2hdHwIC5CHvgHX1quFmteAMZTJagU
+ 3HkUzTW98+MdsL+qKGoC2QZLnu3gZDPOKSRbF4cAp/3jUKXD8SC6hgy5nWlLCakNZQwPOgHQsv
+ 5ZuP+YT4KUBc7llZoYVAc2Prcx4/Vw6rcOAlktj3cffNz/JB56mrKyWnUt2CPegm2sDXdVVZ4o
+ EnetVWAJDDw4GCuDeKL/0wKA1lL6ZTLt0Fcw9OND6Dfus6B+7/tItD3yBSSDlRiOKQOumeYC6e
+ w5lReh9Ed+pANWX4Jr+PQ72flwJSsqKWyRCVGcM4bXkhfPDD9/bq1fP7MkTrBMGkg/TrNVEuEz
+ ekFnlPfYA2Afg/xRFUccEP+tl8Nv4PQWXeME0sYnH+3x6HVxKkE/wL6kc97OvVURVu7ekWH7i5
+ 47Z9fG6qZyD+SbdnlRdtpRrsPdqI4600JV4zKp+4sH+S2YfF5pA8mEaxz9sblz/SOHqAXNuTC0
+ LXy40fOQKBed1mTol+1Xj1VCIzGwy6NuYNYVkpRrZvrXl1dIWjabNRZO0xJPS0PTp3OuQE8YHr
+ l2rDbdQHg9tbzxxd8XeQMBNww2w5UYWtRTHwVwAwYhBLkAEhYmyZl6D5mPeb/DRV27vyzcZouL
+ 3RPvEQhHUL8DmUEtZkBIubW/Qc6URW7fCDiD93gmE9DK82QsQMRrvssNOt2/d0dA67OntuC0z3
+ 8/SNzlswBsOnv8S6NMqglYqslfcOpG0Mii1Xi+l+1dmLymjdxhF40TjNLlmmpOq3EKTvmEzkIv
+ AY2CJ55FHfuwKFsDTQqfvgp0AASkN7SQJ9ZuZ566y4fDh8aatTwlL/tjckRyRXgwBX1PKeSJi+
+ bkrYnzwSmAvPogKw+90x3iCi4crnXN50hfXdj5GgOAAkhSTVDbRIAboldnHryY1pew1dhIrZMQ
+ mMzdDQz7KLuC+PXv22L7//lt7+fKpHZ8ciPIjMVEjOprEZPa1OdtdsK8TVT+Ibc6+SMUCIO+Cv
+ eO3n8gNzj6z5H8FZx+jj1U83VgpWHD2Sh3vSDO9qklfnXs5+yrQ8Rcf1otZz3jrpSLn/rmE6hx
+ +AfvPBHuVq3h5+wmVPSsTrWGedX3r049S6zThzV27DGAzMcsH1MYordWUZ043LHCRKgL20+sbu
+ 7xy33jRNzseMGhekkUyyEXzC7UJq0cmw57tDvt2OJno+Xb6Y5+UTfM0Ak23YxcXUz3PeHdXb/T
+ snH2sZxU4EJC0VhH3zDaVxLVdzWYCe256AIfHE3wIQkIR8dHXohBkYrXT0HJ1eFnoKbTyyioBM
+ YLHMkBDA0e+MhGKhv+YwB0wVEaFoiXUKJuch/cBKZEd1QStHDCjSY0MVUGqiamaS0ZpzDKoxRY
+ tpm5lyxCZtZQz6eMfzVed7wrsaxOx6gaJ3kd+zgpWsl9Yu/WywB4fHt/YhHSTrJomsH6ubeD+b
+ L7IHQD2iV/fverLa/iCUsNug2qF8w6ocx2kCorhI23wYnI6VEliThTInM6pwd7/nhWMwEbHET2
+ JKvt0gHbKLyZz5S7qtFUOj/lv6t076Ib0UpUe/RaM/wjyLR/+e/T4yH79mx+kwGGSlmlyGrey/
+ 6Z6i4xXBcJPZPYPgX2C/v+oYB8nqaZ0HgJ7fT8z+Xs4+wB7/yPBvhx7Lmw61JOog/odPv8L2D8
+ E9pvNWdcCu787mb1kY/1BpdrQ5HKbxhm+JxihuZWudqjSaGzvWJNF45JktqynbUZCCPeaWd3aA
+ sohFBvII7nRLi5m0oAj9YTWWaMdXy89sx/17WR/X2v7mp2+1EBUGYA3FQKeOfjAaKE3VEmnrX2
+ s7969t8Gg75419AFoysp9kovpWqv6sonI8eT6QO1dVdPJfXugc+RJg4SSDVUsXR/0bNjH/RLqR
+ cpzAZ6SehqvBIdOVyoi/uzg+b87scF4LApLyiYNPS2qLVZy6yRgLFgr6HtkmSgmk5SEsbkj/Tx
+ LzbFEBuxRHcmZUWDkw0lUXamKkbSSdXoEFCXeDoLuGJmDTMFzq3nsGS/NTa0whNaRrcWtLa2lg
+ AGwioOn30F/JiWLUTFoSUdMBuvmDbCXFY8yYvh6bDNc257r+dSojfOuqWpl9W4xoM+iAHtl8dG
+ UzvWDyr6V4Mc8QOr9xRi42yTnxWWf3lx2bT3vs74mBR4xYu40EvVpUFAkM8wJtHZUmUDf/Of/8
+ h/s0aNDl+Fq3aA31L334JXIZpZ6X2PV788Eriqz9+9uFuMV9bpNg5STs3lfl3JP/3mw4ZX2/j7
+ aaJsaQT5dB6SCUgmQ3nawDJIm3lTd2E7moKZwNikYf7oyGOR72nz9jSMoaRx+/+UPv74t5Utlp
+ Pz/19+j3K7e9Oa/k8fMLVJkwfynYRhuvF7XdvoYoHEjokLp2GJ2ZRenZ9btMlVLtrpQs5bhJy3
+ KFu/s4DID7AExhoqSamC37PLa5kyxxpYidPc32DK0duQN/vTk2Abi7FvuLY9aZn3tGXe74wZlz
+ R0FC/T3WCW8fvPGt04x/BQ3OOsCAQN587MXNpai01RMSkHPy6YrjhhJXvDW9BBugtsl22YegNc
+ esDWKOQR4dR/3Vf8AJROLSlKPDvPlMryOsY4dANPuWPT2SBZvsEdw3/slnjzyqceuONfCeYBDV
+ 09fAu5elhG3mMX5cBP/QVkBVuj6eV9q7CobT622g304utcOikqJC4mglo1wrpzHX2tVjYOu5J3
+ K/D3rzx0FDk3eDFVrWEtK4nljH3DFjYf3jxxRc3UgICytv++6VWCUNNaVSNlM3hiiqtq5vhzFv
+ WbCb0iqLu9B5VCTD4nRe/DgqM+5AHtZIlQGZb5gXrm+eggkFC3r9fGx37X/9X/5z/bb3/7a9vY
+ mSnRS4aUAFm6e0tSnH/xWRvoQXbOd0d9Vo0QQiPu4ftrg1qv+jH+eG9z+PVz/tgSzZOjFg8cTV
+ NOxBWj689egXal1qmZ5fZB1TyB3F0Q8qCJidgS2A2IJ9r6UpgqOWb1FYPwC9hvgvh3e6hNZDvN
+ wQuU3HtI6NMOT42NQ3laLlcvKrGln7z/Yx/fv7fZ2bW24yeuV7U8mtre7J1CjOUtZTDY7W64kg
+ QQouSHIyKXQ6AzEAV/OmLb1Ev5qfm49JJnDnj06OpYyh+EmsjG4cY660+mpgSpJJo/d3Zfq5WI
+ +sz+9ea3XBBQbLGDB777jbooEM7xyKOHFuceycdEdKvXDCkBLNjwDB3DIYGggD+lj9PHwH9jR/
+ r4dHh4JyD3oMBswsi7WC/KDXzr3vaRaWUmmStsaYIAGkRMl2W/4+6NmWkzZanWrYCH6hnPCzAD
+ vnSa5rIjZPMW7dRCkSQkIs09WyzdE73hg496gL5I6eylnCHrhKe90u5fHYn0zMZTCx/99fcM5S
+ bCPnQGihzh2p0Qk1Uu3iTA34xyQ7qI+UqCIPa6uzHAaUH+Gv78nGp7Zpz0w14mOI8A8wT7BQ88
+ Rx6BwG70Fgqf7I/lrVMCVElfx/r5NjD+9gnJGWnLVoKVEbeL8aTfWH3Xt5NGhpmb/+q9/bd9+8
+ 5WyfHpKqNKoAiWVzT6EPHVyY98m/fDng/1mb+L+YFDl4i4g2gLoOlPfGnCKgF2m8mXmXuruda6
+ zOVvZG2TPwRvBKnZzKXlWOqR/hWDAA7I3jbXysPIAyqMoaJytHkTSY3lGvoD9Z4K9ylxK1xjvd
+ urawQ+wP3j8WEsouEgZBcfp8s2fXms3rTxaAIprnB97tr830arCs9NTNRz7g6FB2yK51FBK7Dw
+ lIx+Mdq03HNvrt2+lOCED//D+jTV4rp67aHZbbXGiXBd6DjTq7Y6NhwNrsG/01nw5ScNsOp/ZH
+ 9+8EZ+sgZ3rW92MAAwZOFhGgxOuWzecgK8GngQlKgcMy1LRIonpaGSTwVABi6z+6PDIDg4OdLF
+ CDUFToF6iT7CYu52v+HTDJ31l6/nCdm7MWjJcu7bFGnMy10YjuUTuOTu/kNulAhMAv1rabb8tT
+ 5qr6aWWpovq0SCQB0fltFrL6DYGq2ufmM1BoFTaJLAhxnH1TMmE+4XiMsoAQL4h5Uty8a788b4
+ LSfOOXrcGe9decB2lHNP1+QnW+osmhvP1NUsgS2M3hPMgwNAVG7WQx9KwrsG+zOx8haG/ZtKD9
+ Xtwea5Xaklj1Q0+b/LGOSjAPn+fP7VPWMtqpB52sH98KKnlL77/2r7+6qXcUrWWEz4fI7gYRsp
+ air0PFSgVEsvaCXJTS15+vwTl+/7+0PfKbH8b7KusOH5Z1UwZDEpH+5Sdxs8fAvtszFYbrRL87
+ wH77fdXBauwN/bj25rkjQrOQ3GZzm/RXF9onPwkNzl6/26dJaicjsas88rctO6DQkbbZ/NOvy+
+ PGcpbtN6nHz8K9GWfgN6azOwGh0ez8bAvUF0tr21371CUBkNN2lGL7PHqSouve4ORjSe79ubdO
+ 18IPuzbKdXC9cq67R3bHY6UOfc73HSepcHtYxcsUza2MwUNASDyGq/fv9PAFlk/70WZvbj0jrh
+ +OO+lmrH+npMfTqDjrMC57zEFSb9AK+Xw/elZjz2w+N+3Ora3O7HRiPWNznFjisZyF5rWap6yg
+ B0A6LZtyT5avOlxt4S2kq7d/xOVwAQrklE2Vc1mkmCyUISA1TqYWGswsPnF1N6/e6s/NbjFc+E
+ tpHZB0+0P+E8e+t5QB85892tSITFZK37B+WjklNn8dOrG1T0hs/dl5pHxpvbeZwXa0mHX/QC/p
+ iqwjzIh1Thq4GqtX9BEzvk41RADP9piJaD1pmrVxA6pZUruEugVj5LHr+YlCOB+deP7X1o5VHS
+ CZCLFQFo4iWZg4Ge6dkh+sM7otW2n07TBqGvPnj+y7755pcx+fx+w78rTSZ73EXx0nDJvDbpUS
+ pVsVla56IMSSj+T5YBVoOADWfo2iPv73Ohnbg0obbpolvRRdXSOuoEStelZYocn5Ukf5Z9B58U
+ BVIalZaDbUk7pUGNAq3zu+j0lVVQ69G+qfxRovoD954G9skAUEzJAc7AXVcIULRcwfuzjsR0dn
+ mghxscPH7WndLXwzH3QHQrI16u5rZZTcfikNO1W3/Z2D+y2yQg+lgltKXCkKKGBKh8eBp9oNvr
+ E5hX6a+3EbfiyDxwzuy2pYZjqBeyhcNjixDIRHotCAgfH6ezSPl5cqFGLORnHzvuhjyAefQf/+
+ oWCDbchqqM0QuP9K7gxUDYa2mhEUxUPnI4rgsiCtIPWJ3xx6NSiFnnL9/Q+shkoO2RoKrLDfs9
+ uGAyjMbnCyRIlDb0JmsBrrQC8upzaguMlc2bJx/nULlEVUbU8e2T93X1bzmf28c07uzw/E93De
+ ZJ6R9lyU8ZmS+Ydrq9txsCYhrPQxfvQFKCrHbA+a6MvqWVikXuFI0F1eAyIRduBALyGgl/0dHy
+ 1X/rye4VUgWvuAqZJjy+Qriv3g1cjOagyZz38d/1lvObIfb7AZC4eSRlkCW4Vn1/MDmQgqyrVC
+ HipnVezVktUUA95cKv46ThujteN90xuloB9f9C2b757ZS+ePbZXL55p8I/M3l1NaahHI1y0UJF
+ SVaZhNVVxB6C3QPDBLDg+qG0K5+7z/RTYR9N2S6ue10BFk+UnsiWrrB0u66CUi2IyPMgOO1LKl
+ MaKQSis7ZLSeQjs08/IL4sa7Dfee1yDX8C+vIOLrODezD7ALvlXpHKTyVgZ3HXzRr42Tx4/F9/
+ +5vVb0RRYBg8GI2XayCxRsMwuzzRRS4bb7wzUxFwbtsMsdBiJQwZ8GKzyxiI3Fb7fOza7nMt/h
+ ksI9QMpGklov9XUqj502jwWamY1n4sHh1IhC9ek7HJpl1dXWmDy8exU9ArHr0lcqUF8/aCokPA
+ cB6TdibApYCcAEBi8Sd228WBoEygjGstM5qIW0XSnBxpKeHf7RD7q3vJaziKPn6btsBS827Edr
+ IjFv3vWv5jNbDGdyQdmNb+y848f9JwYy318+84+vH5to/7ADn/5ne0MBra4IACc2+pypuAwv7h
+ QsKU6kDTy9sbmq4Xe/5QVgKtFRUNxvuWICfCq0eoUhhq8OQFbNGp1DVRSxtRL47IQEsbI7OE36
+ sw5JIvw1lR5oTWnyuC1faaAnaxZpQUlFJNyTiU62Iuj17YyX1ienH0JaH6MObDlYSInZFWJ0Oy
+ NZfSZxWdTolLmyL4buwYH+3xOHudBjcrANC3bHXRE4Xz1zQs72B3byxfPZHimFZzi7APsI4NPt
+ VEeswe1pCju19xn9l+B7nYA+Jn//jmc/X3Bp5qWzUqhAP9kBtwGOydc62pAYJ8mdFVQicZx9Tz
+ OODh3nxl82VgOb5+qn5A0zkYr2auYL5n952X2ymorbbRryGlOAWKAH7aVveHAnjx+qvL+3buPc
+ q+kAcm0IIDXbNxY8xaKZs5cpUseAUP82xsOLnjkt9pkwX3Xty/mcod0P5SmLXBoZA0ihmssFmf
+ qFHC4mkvnDJVCJi4mGO8blpgMBjYejnzBxE7DrtZre//x1D6enUnPTTWhgaYWC0R8i5YoDgayy
+ MpCEkmDk6EY+GJVAezfJSBg3TAc6/V9WbZnbQ1pzG81CoJHzqDbF23gFgamqoXAJEkkGT6bpsJ
+ vhSABcNPkRmbJJc10LH8yo3D69o394e//0Q73D+zlf/orWzYbNv14ZmsC7GxmV9MLOV8yl4DVg
+ M5vsyln0jMkmgJ79r36Iu2cjhXoF4DvNI+bmvnKxzAcUHYfOXf4ywhMQ+vu5wBnUl+iLcKicFY
+ Vj8+egp2WsTFLLqPBsbvDpd+0FdAW2Wrlnx/nkXOWNFQJgjxH+upr0C5oGU2uEnRl3+C0kmqFo
+ HnqJuBmZq+JZgJVUF5J5XC83X7LhuOBffX1c3vy7MQmw7599+3Xdnx0qOpOi1sctfSfgK5w2tx
+ QvThvtcVNb1YBD4H9p6qB7Z/f5ew31S53GrwldVR46SgEl7RNBDS+n2C/zd1rmEpWG7FtrKByM
+ rvfaNbG0iKX0yeZ9HCDdjt3/QL2n5nZ60OrpGmuRIF0lHcNYLvjXt67kz3rtPvG4CEqc5QfcLx
+ MvXa77oXPB4xVAtOpV/MLzC/9vxZa5I4NBhMbjSf24fSjLRYzcZsMOgGSAMj1LWqduU12cQ7si
+ PJo4aOvZdvuySOOe7mwnVufdN0d+cIQAJXs+V9+/NHOp5fKDFFJcLNzMxN4fCy/4TtyNeVKNt/
+ z4KIJyLaydC44cebWUIbt27acFtJOUfEXOH7eyMKZC9i96eGKu9ZhKK3VdHdduOzQzfP6AND5u
+ w/K5qEBeM5LVjZyTPQ0zi/sD3//D3awe2Cv/uNvbcmSds7RcmXn79/bxccPqgbkK7Nc2po9sZ2
+ 2Jp1PL861uSuXkrB0hNcHyHnP6YWziua08+5kxc51S1IZpDffJ/NPq11ndpzrlpeJwN6Z21q94
+ zp2t5Voa45Cy8s1mRp6/uBoJa0M2iT7RrI0DrsHvPflyxP2zOl9o1eMxTaOrw72Lt30Hk1ux4p
+ awd+T55HKsZX959BU1btxu2gFF7J9ggXXfqth+/sT+/b7r+zk0YEd7u/aD7/8XtJLaMakK/wvX
+ G/xGjHSupn9+lHwvqR2SVwr0CvB7l6qZguQ74Bekf3/VIM2K5mHf79eOF49JrP4DGwR35Lzr4g
+ 4vf00Ngs7tUzKS5lvfhzB28fIbSw6r85qfGpVh6BKMMpT9wXstz/JT/y7KmOL08u1tYPPjEAOQ
+ HcgAyDFQS+unGphsbicDZmq7Um2eHr2weazqe00bqzLBOWQVYIHGoJ68+6tXWE7fHutRq+rI5r
+ WbHcFFFA2/JxKYYxvfrOpJecYTaHqWKiBubDOTlsySIKDSueG2bv3H+x8OtVxw9EDfmT4gDHHR
+ uBBzihJJasWhyNvwMaClOTxUcgQMXrysgGwvVLxoBGLOGQgA+67pJHjZPoYXp9j0UQlvx/0hXK
+ V6xs7+/DRlpf0NzrKl+V/wyIRFDirlb35l3+x/cm+Pfr+O1vDcxOy1it7/Yc/2Nm7t4ymujZ/c
+ SUKh6qCxvPp2bkt1jh9ur2Byy49u13cuMJHE7LhZCojOb0FH05yAHRglHQzpp4T5L2R7SmdFDl
+ SXbglri/zco6en/PeVyEDVTNWNjsBcGFQplfZ0A8A9v4tVRwx2FReus4wYQ9Qr67LoKFmb1AyG
+ Zwys9+YuUmdaTFwtUENaQQPKojeTltTs0+fPLIj3C6fPbZf/fBLDe/pkqv6sN5xzs25iBwSF2u
+ qo445GiSqQNA/pXri1oNj+XUnE/8krZO/HRRJVGHVMVW/f3dQyx9TAGxFQTnC1wEpmrLhPVrF0
+ 2LleWb3SdRkNagEIOWYcWx149fPnX8lTZT/rJ6pBv4vNM7PRPsHHo4FwA4ZaINMvyOAJIsXSGn
+ 4yFe1uc75WvRLr9+xxerKzj6eauCq2+rY8fEj29s7Evi+/+iBAN046pP0ze+zL7bTVcZK5tvAD
+ mHQVfOSHa7HUkB01Oj0sfsbO9jbt6PDfQ24AAIADtuqkHq///hR+ns4eCBkOGCFHBr9hRCF90K
+ Dl8ydBrDcPDFMS0kfdJQu3BtleJigAfT8SbYO3cXQlzf7oLIAeB+forznOXH9zCUlsmNYX9vs4
+ sJucX7EsVPe+zfuxIkV8/W1fXj9Rk6bu0+f2lryvx2aBvbmxx/t4sN7a4Sfjh7P0E+7LR96/IG
+ uaMiiAKqM1Vz3vLxZ2xV6fq0FjOqNoBW2wRqGKmgbNXCD1qh2tspkzDNrVQzCXV+gzWN4vHP2A
+ EJsHKty3XAYjT212RdILl0BRWIBN8KRpcFmJPArNOimjew01FX8WHLaHJKKazqfu+L5A0BSRVN
+ d+qHL5/urmxWD0TYZj+z5s8eSAO8fjO0X339jP/zyl+Lrw1A58Kj0b3ELkoCqqIBCJhT1he/z9
+ S/NjxZSTX3vLwb2AZkb2582ZZ9JLd2FgBrYE5hFw9yj4d/4VuViGR8ZBoTFk7vJWs3h323WlmC
+ /dawbBmzx3r6A/V8G7Llx4b07ZOeDsbV7PXH3UAV8j0aryvZOS5JB7IZl/9tiecVKAEb2c3z02
+ EajPZtdXTkYr5c2u7yw2fRCFRwLPXqDofxlWOJ9NWWi9MZ2h109D/zo0f6eeHr6CjSIAZnd8cQ
+ G/a588QFaByGWa9/Yh9NT+3Bx4dkmG7OGKG2QTGK/4GP5/I5XJJhZuXzSy/yYFIpJT9nbkuHLB
+ TEoHdY4QvHE66qZGNQEQY8KCBdR6ADROfyh7VUz39cKvaJhMd8zSxCDniHzZ86gt7+vQR1M2cj
+ 6P759Y7OzU9FYNGixUEBd4xu61to8hTXFAt8eNWV9loDmKsGA7y3Jl4X19WYsLZ2K/ay+gMrfB
+ zYVG8FAeB0AEHx42g67M6a7Zzr6QcFUVHZ1PlP95QrI+nypue3+xAH2bM+q0HqrAHBgVeM36KX
+ MNquGa/zM40NUK/F6+ixUnbjDZdJIrviBumk5PdZu2PHhgT15fGz7e2Mbjnr2q19+b99/9636O
+ NVB1fNBVTbqFgCZfEZ1Vmby25l9eNLnXbtNtXxagbPJCW2rHPN4dEwbevb8TO/Di7tgnzRa+Wh
+ X2hRfaX+QH19w99XHmVVD0eDVUUQSARm88VUCfEkn5dn+AvZ/IbAX59327HfIBqmRcpHL+dz29
+ vbFczPdqcx+vQpZJtQM1gZI7tDg39re7qEqAzLDVrejTPZyem6XF+dqNJJla9l5uyNlThuL4hX
+ DWh15xzCNujse2cHuxA1bg6Nl0QnwDtiTpWscXslhU5uqLtlJO506l6x5Avbs0rBFSUHT2Jtsz
+ AxA0ch6uNpL6gtOUm8PR+s0gUsE3SF0UAN+ABycBfYJ9Ax4qFwsAW1+F+8a+GooEpmfrVS5APR
+ MynLepudTSf46w6Gtpfv2c3v2/p0tLs4lPYVmml5eaOgKCg0feuwfsKZYXAfYY/ErTxuasywcv
+ 7ZQ51dTpgJzUTgOtrJ9TrBHRhpMSw7bOY7XVgS+KMUlmKJe4L1jAjebvjWuhZd+pe+PHoFM2BL
+ s09rBqwgBdTaPi0s67TAya68mgQuFBz2Lar2fBspig1LIcqvfDU08oKWl5wyp2dp6g64WkwD0J
+ 8cHNtkb2ndff22vXj739Zk1MjtlL4RNlcmnwL6mSbwBWlsifwroN5q+id4l1t6heKqw4wGooGL
+ qX6/pnoLlqoJ75OLedN36fZ5j0ysnGtF16KuatTpDxfGVz1XRQ+r8+fFU1FYpFS3es17pC9j/Z
+ cCeMp3mKfQK2XcX/5deX1LHw8MDTdm+fv1aS7exPMbHhWlPtPhc91A63KwjBYmGYUk8nkxsenk
+ ZHu1Tuzg/U0OWEXSZmp1f2GQ4iu1QSzdJu1nb3nhkRwcsEmFJCX7kZkeHB8oDyO4Be/HKuqDZN
+ sRN2/CsebGQNw5cv3xsuj0bj3xJC5O62Ncq248BGSgqAhKPZyBLthKhHAKoKW/g4hmm4rwQwOD
+ naVxC7civJrZFcdurIuCA2QkQ9gDYJDRuPcOnUQ3Y8zvIRtH5N+R+Cb1Cxntt07OPdjWd2vUVG
+ 7FWauxqIxh7XRer0NivlN2rGYtZmxaE+45aedL72XGaILxcSrD3HbUO+stVrUHnrgsSp+LLlVk
+ rQ6/VLfLPkX7dbdcSCHLXrJuXubuoO1c6Sa8msaJEDj05/eSPzdGvOlNOsK9sDxLEIlt3dyYPH
+ C7vy9mBmJAN/x33/ffnJyj7LoSGrRtr290b2zdfv7LDg1073J/Y4fGBPTo+Em2oBfHZnk2zsQr
+ EvIldQax+vknj3B1a8hWN+fVzaBynvLcz+/rfBV0fYF8vNsnGQf3rd58IIw8WAAAgAElEQVSnG
+ grLUaqCydkA7jiOur3q/ZxSX695hKIHcCfw6DUis8+J3OKQNiqcDABfwP4vA/Z4TTM16dbHvbD
+ wbYkT39vdtdF4ZG/evtPS7cl4LM4WOocpWoAXegfKoYsembWG+3s2mkzURDxHP35+YdOLc182L
+ tWKaXgLfp0U8XLKwo6lHDB3RwM7gLdvu30xQIHNMpJF7IcFyjtw691KAumqu4a88wlCUEeYbQ0
+ GQ9udTKosH4tabhgfffdVhVBQ2h8bqgvP6lm3h3EXqhPX2mvFYA/pZs/a3Z41o6nt4/4O7hoWg
+ 8fmd6FwuOihPPBwZxk6A1/K7N1XXmZgsWt1vlxI0sreAHT21wu8d5bqf0jWaab9vleLpYbUWNi
+ ygkO/9QEmgT2DXaGz5w6UoiZuIj5Ld7V0o7A1RmuyX8gskNv1LtgHkusPDxiuXXfVj/vfa/l4V
+ DZC9SjpeS36K5l5y74iGsocXwaL+8A+m7ObooLwbMkF6LGiMIexXOvvgKstZMh9F4tqobhXar5
+ kByrvdufaDo527T/+h7+y50/ZLzu2wbCrrWosK2kpsw9Yi6GwTZnlp8G+AvaUN/6ZYJ/ZennH3
+ 23o1j8tP3v/7v1N2vy+N5j9YknOfpuDL1+78s6p5gu2J2A3+xv++JRqFmBfnaANtN8UMX3R2f+
+ lgN59UGjQsl8WEAXYdLOxkIKGZ6erLB0KwTPdrqZD+Tc3EPw6zdg+1sQYm41G1u73tIFKDpBM1
+ GKnsFrYfDGTXTDDWAAn2fXFxZmoDjZYjfpdbbCiguCGJOWjgsBWmeEnn3xsaa0imnKy7PUaUzP
+ sBbwJCv3RbXfUjKUqobmsLC2Sv2rCMhwe3UUSszMsj/1xZNU366XcPTXtyzQwtJCsjcnyCYptN
+ KcKOjhn0rzlP94LdII2+CErRIdOJrz0/kZa73K8gCZXNtp5bXJaYZWAagf/n4X3S7ptZcgzzuN
+ iJW4esL++aRitX7lVAsA4bIassALV4GjkYKml5WH9GyZr67U3Y12a65SUK2oKI63Y1epyRx9o0
+ jJ3VRKxg1ay1KA4mAuQVJLVgPRdonGbk67B/rq3UK2zr+SZRQafHjeZ2bqnTQa4lIlmsHHfHk3
+ yhgw0/x0aWV0HyHDxOWoPGjbZHdivfviFPXv2xB6dHHn1R6OfpfMovOT/ILK5ajjW4Pn5YO/Hv
+ 7mCcTuz3wb0n8vhl8n/fWB/T3FQBIHS5TKuiQJithfIbIN9SnRramZzGUm9g9almtucvfyf8vX
+ +3TdokyR7CKO3uy1/OSz/9DPBCdOE1DQj3jEj+dqTBQ2HY+t1BwIRLde4mksiSUmOBzu363I+t
+ /nlVBYDWDAMGILqtKzTG4hGOD891c8X80sNWiFdPDw4sSWNzOXKphenzv83bpTdA/RsBkIyyYI
+ JpJu+uNy95mmcYXdA5aB5gBUqlfPQiN9ULoVw9IdUCdAlrs/zQSkChkDDh2zwj2dgifK+1+345
+ Kn8bBby8SHflYxTu2U5L1BCDvrGMBa0gkTsyAp5jdwBpLRfAaChhvNCE8Tp+S6ARmJ56xOwgD2
+ btngOzscSSwQcPCW7dLtmzhcAS6C7ZuELrV9l9kzPOti7jtzVMzJTi1kA/qA/kKsOBf6x0EbBH
+ WjThqIA7tDoe8bnwCrmOWwIBPg53KSAERxsrFKs7YadxqkUM8HTe0UWTqQRHKrR+qTEYmFJ0ku
+ amkUSeuv2zjyHtPIxE0BVlcvOc5JW4p9w3ORxZPb43owPEAy0Rds8eXJiL1++sOEAabDJskN0j
+ 8o8gl/62CcoeeXkOXPIFStRZtxyues21TlB81SUS8lRb93/9y84KWibezj1TTDfcr3c5nkqVij
+ ezz0c+4YrZkHLZFAqqRwJS4sm7qZxWgbLOC/VQFf9fnxcZPP9bVQx0Dj5jQ2N7afh7d/0EZ9zb
+ PdF8X+zg5T6BIWKqysAMdQK/dHQDg8f2XA40WJpFDSnpx8MJobr+GrJchFWFl6poYiTJEDPzTS
+ ajG20u2cfPry313/6k4CKrFlt1Z0dm0z2pCwhs6eJy1IUaAxoE7xI0LNr85E8aBpy28SR0tcKm
+ o3Gu9bp9HUbzRasLGRi1zNhTNYAdyqBR0dHunFdPO6UC1QTgcDB/tYu5zNVBPKEiSlZLW8hQ9f
+ OWrfnZWGL1umh8MGdswN/H7auYi98VWOlIgngl+8Oi0LI1Fds9PL9qPpv6QDt6eONdt0KoHNvb
+ WxSAtx5f7JEELC7de/ypuEALsrE1/7JLiFW+3lm62NGAttc+RjJR2ruuc+UfYW1RGbpmX2Ho3y
+ AvmfQ2mkbC2uksgjVS3L2/DxzHK8IPPhwzn2SVZ2FepCLcxcDbZVsM+BUskX1lmoQU0i9xfYAZ
+ VjTqawltY5TOXo/6hx78OV3tV+425Gj5dHjPTs82pP08uT40F68eG7DPmDvCYWUX7LndUiv+gK
+ Rm9bNxgSqVOBH7h/SymiLhmA/GXSfS9gAtBLsqjbwtoSy/ndZGdzNFe8D+41XK7B1m+IJyeR2A
+ NIBe2aeG6rqNq3EpRvPqfstfHcq/5wiMOaDox58EOxVaP57Afv7lAbbQP7fE+z5ILRgRLrh8Hx
+ p79jJs8f21TffW7cztPPzmZaZvH/3zq7XC+dEd0y7azH5AgihTRTdm01lSVgK/O53v7PT01Np9
+ aFUqBaoDpSdeddOQQSNPZw3jo+oUshshqhv2AzV79qYPbE9tPTXtrxayaHz4OBInDBeMbPZVPt
+ m19dLUSAoYvZGQ3t8eKQ+Aze+uy0yyevqlrzI0KsDpK4jrDNN3aRsmsKygGZt9Dv5PmdK07LgO
+ Lp2ghBKkAAVuHs1bwMHAEdx3IBxsTbPl3/7Ug8PPteiSHLSU0YU0Baob6BNGKaKRiw33VUB9tB
+ pal7H9KxOb2TGCnUC+1oOWV2DgchSqkQz2CcPsjrwJKCesM1J2xvDKdObcWFVTJSJTV8Kkv6y7
+ nJJJRaTvN50dURLDb8asuK+wp7aN49XvQdPmvxTU3NeQO6fJYCvILqkqZ8DWeG7I1oHi++W9Xt
+ tTWrv7+/b19++ENhT+SECeHxy7OsJw6o7D77C5MzUKw15fRcn6JU+7Ns6+ztUCCVf8fUQBtzl5
+ oNbv6PT3wbzgge/09+9D+DL9+MDYaVCp9bh12obl2TmHXE3MFXPqGMtM/fN9143s7NKKk/Mvye
+ w/4wU/afBPnZvfsbz/DkPuZH+l2wVczJ4Wyx9W/bs5TN79d231mh2bD5f29VsZe/fvRcdA9ijs
+ 8cO2Z0sWTIy0Ug/3PbTp880Mfr7f/5n3z6lhdzuuCl/HSyQe13x/bPppfTxVAY0V6cXFwIdDND
+ I5judHeuzDnHA4JfphmZj1PHxic3nSzvTOr9Lb7rdru3i9NTVO62W7Y/Gtr+3p5/hW6+MT86MD
+ li6gA3PHywI2BCVsrrgsVFt5E5ZZc8O7Fy6akoiR71hiclSQYpKQNSNvENcZZNqFF4vPWo43wJ
+ 2ATjNXActZd6xxFte9QK22D+rgSbfhAVwcjIXUDOhfc9GZ1IuIWd3zCoyxbLSFHTme45hKs+I0
+ 2bBM2q3Uk5L5ZBXFhbL2ZTNZmk95FRLNt3SmCnStD/gOfPosm8QgFAtUI8BKh2nq4xyupbykgq
+ Ha0WeSmq6Y9GAIRu9lzBtC6Cj33SwP7GTR8f25MkjOVx2ur6c5PjoyPYmY5fPMmsRzV7uhYqHT
+ l45wN4VLCVA1vYICvqxfMXrAgfOjQnaLd191A8lJurJHwL7uyqXEk+jnrij4MnjzZ/X/y6ZHkm
+ f473ldPDGWsWkoDLwVeqagnvfCGQE5/JkbVZBG2BfZf/Fuf13k9l/BgL/9wT7W5HKO9Zusm4Qu
+ 96VNXdubf94zx6/eGat7sCGo32zm5a9ffNBQ1LIBG9uVnZ2+lEUDvRDb9CX4ubo+FhUzenpmRt
+ 2kQUzhcrmqMXSqRZ6BGSmcxqTJupmPBrYfDYTJYND5OG+uw4C4CyKaHdZ/+dcO80zBsBoWp5NL
+ 3STjycj0UvYBPd2Wlp/OGi31fDlecj+VBIqQ3N5mKt9UdBcy6a5ahJG1piAITfL2HxFYOBL8j0
+ Wiitrp/nKeZjrWEiFlc0zGCZ1CL4saOOXkptyoD7M6Xa/el1RFLeiDZwWid5gTKvKsiAokKRfy
+ J0FxOEvs32pbWeTVckcD/TqxZMJZbA6htjqlHx5gD2fm3bVqrHqQOdbs6TEr4AJUKVnkEtHPKv
+ 3AOXDXFld+JOIQsrML9Q0LmT0Jm9myJWePkoJzhGVBb/vcspmmK/5aksSigw+7AaA6oGb//bbb
+ +zrr7+yx0/27aaxtn6HpTx7MtLLSizPfxBgDtQV2DssB3HhqvsSGCO4+D1d/aAGfP3cVUsV1Bb
+ 8fSlb/BTY3/fzwMqtTLrMqjcBf/vxLmRw6+nEJTWqK5nkXaWN77Gq32sVuKqX8mtKEO8XXXlq/
+ FFlD2Pr3/9+aJz/0cFeuzQb1mp0rNVgzRyl+dq6w7YdPn5kB8eP7dmLb+z2esfevflg56dntry
+ Cb7+y6fRcGS26cW6s3b09e/rsmTL+s/NzgTrNR9kstDshp/SbGGAnI2ahM3w+w1BIEynHuQEBa
+ c/+8XVfhzdPQ7r3Ic1ba9mbt2/tguDTcEsHcfF2axA+I/zqkWx2WDmIeqb2MsmGXxvaIhdxZ5N
+ QAWFH54TjlJwxqBcoFgKM68xvbHGzknySY766YpfslS2WZLBuMkbmSlUBEF3OLu3ycqqgp74f6
+ waRg4ZO3rNB/t+Bs93CZtmrh8QN5909EDgv7RdXAmP8w58n+O2tYn5Dri3XyGIqWPdYmJCJfhE
+ 4eyNbYK9qI2R6ZNKZrQdf70Zlta1FGpYp8EVQ43krzj4mfX0BeFRb1QpBbzQnV85z5GMSHHziI
+ ik1z+QT4N2G2uXAgOtoPJAHznfffWO/+P572z9gnwOigL6mqzNTFikB5RczFM4713sC8nxmUOS
+ jcNlqja+CtvhwcrTKm5b+0fnPc0ohsW6bdnkoO//p79cBZBPUH5Zq3pPlbzdsA6S3B6/qYOMWI
+ vd9qTKt9hlkhVPUmtvqm61pWv3G/yyZvZ/X+09UnrzPafJ+Rky59yG5PLrVxKERKoShm7U1Ok2
+ B/bff/cq+/fZXtlxe2+mHczv78N6mF2fi7nG/xI4YDxxAnZuL5SAsMHEuta3pVv6N+Rk3L2DAr
+ lgCgsC+jZyTDN75cUB/MtpVeU2Tto0tw61nawzqIA/d3d23prXsxz/+UcfAHidAiucRt79aSq4
+ 5aLe0KJ1mbbvN0FRkGEgqRV3lJF9QE5Uvi2cyDnK+BzYz1Ols7pp31DVw8FqyjgkZyp6V9O+YI
+ 3Mrt9rutEnWz/av+ZXvoQUayEqpUnxFpC9NgSJarfD1cW94TeZGr4APT8tMCrVMO+YCHAT9/xQ
+ 0mCXgLWQ2Gknkpk48jKoE1NEwrYKHA72DddOrl1jgDUklbp0GMftyQ31U6t05npSY5rFpSXq4a
+ qIC8n0GYU8Rg24abKv2BnBmoyEYHvY5bavnJGDGIvJU3mRjN//twQeKcWwvXjwRR//y5XN79dU
+ LeTKpaQvlE06dOj+6RpHUhrXDxsCUZ6QCvdKLZoumUU6/5Y2zPZzU+ARnXzWA71AxPxfsHV9+S
+ pe/2Vz19+eQtGmn4HSdLxZX4NqicR5kKArO3p/2Z3D2PPzfE9h/qgH7kz+PG+TPBfNP/V6Wa80
+ GHuYtjT3fNG+t0W3bs1ff2H/+L/+7ffftD9q9+vHde/vIasDzUylsPnx4p2YoII2EEV4WGgPdO
+ 0NNaPIZbLk4v5BGH7CVlzz6+6uFhq2YVEXyyDUNP+6umE69yIyKHkKnab1+NxZIcwMjjeva2Sm
+ TvG6lzDWI2oKG7u1yoUXgcP27w4FN8NWhSRce6GloJlMzcdQxth9JsyZg42KW8iTMxMjo53jWs
+ CB8PrclMkhkgChr0JXjW8NSXilbWvqTBjCbtVDjeAPVd8zmflwPUswCtNUPwemSe4NjU1Yq6ad
+ PDGtpRzTBOT6UIxXQS4jqxbTAXvRIVTx7MrzRJIsp5JBKRn4pgK2asWrSOjfu/QWsKmqpo6SjC
+ lo1z53ZXi21DGVqTN3K3gHJJnsBgjZKZYkspnEtjWrQnyubvE53pcTHlZte9aS9gydFLq/l3PF
+ 2+/2OnZwc2nfffW1Pnh7b8+eP7eDowLpYVKP4zrV5WEToxRyoaxuGOIc61W554AF3U05TyiW3w
+ V7/3ubst8E+32zcsD8X7OssOz7JjfzxboDYjCGbDdufAvsMDPeB/R3uPcEnpKv1zt5/Bdh7cZT
+ d/U/B27/tzz2S/XTm/qkjeiizF9cbt3H1HPe8lD6Ye/xH9KFGL0Y8HX+H7xz1bXx4YL/85W/s/
+ /w//i/75tW3omr++Id/ttd/+qO9ffva3vzxtb3HoZEdra22KApuAPhrVgciq8SuACkmihwCATQ
+ LgE7GzhfN2OXVTDe4rIcJAoula+mHI2W3gDkr49Dei8GAQ17fynFyfsnA1pXkdDsdpmtJGgCSG
+ 2vcXFuHtYadlszVuLnlXaOf3WpSEq8crdsLFY4oDPhm9tgCplQTaccby8QBdaSas/mVza9v7XK
+ FVQHDQ2S6K5tdAeq31my1ZMGMeydgCaClRl39AoBNIOl+LYAmNJbvEPBjzYleZaqSxtKQJGsPj
+ XexJxaqQqojIUvDh7w4IRWfHs3nynXYh2fUlA7e3h9BI9a9fTXleoM7ZixGYUCMABl8OO+XACS
+ bCRIFDW35e80AmrbKSbEA0gL7oK1yWpbzTsWgrFoSU2S2dTN9e9LWA2o2kx3gFWBIDjQPAaF3b
+ ZPJ0I6PD+wX339r33z7wh4/ObHhoKe+UFI3qvcq4zUP/HnLOoi7Ksp9QD24bFRJEUbvU+Mk0Ov
+ MFvcmE9Z57/r3t7zhSw57C7jT3qDC0gey//rnHuoffvzdn5evofNUcPKbHH69jjClltt45hr8+
+ vVzdWb1qlFF3Hd8On9lZp/vdXt26f9L+qN8Q5/K3D8F5p/183vAulRS/CTY55q3sJgV5Rue4bf
+ yPPcbnpunv7tre0+f2NOvXtlvfvUr+9/+6j/Zy5Pnxkj/7//ln+wf/vHv7R//4R/sn3//e7ucY
+ tl7o0lb6BpuMpp4Hz5gcQyVs2NHR4cCdZqxcPjINaVVVwa3tNViHivmWlrwPJtdicrJhdBQRO5
+ Z0/BhKPxw1nCtXS1CWa7QyLesP+jrMVfzmQ9fMRmMdr9htjca+cAMQEPT+NqrCbhaZKMCL2nXT
+ RbFgLe7X7qCyP3h/TTx/vg5QWm6urXpkkx9rRv5cjbXf1AWfJHVw/NrIxMukyvGoPDZ71mn3bH
+ GzVIGcA5kPgnLtSz6Joa5fPdtLZnUEnU04PrMIttNbx5VLpn1QCaFB4myyITyVOA4wOkaEnfvf
+ QT36vGAwSkRlaXmsgOwvPsjqPM+qzGjtJpgelg8uVci6XaZ26RqHt+Xl1T3TjSmqwEpKoE4j+X
+ 9kQLMdPVMlVNaNjjYM9TXFi+Oz83R4Z48cL779is7OTmygdZVZjZbTI96meDnNv1wkq6oxTYeG
+ AK7qmSrAOQys78vq09sq3+XY3iYwy99dxwgfdCrAkePQBswshmMtpniT9M61VBVHOxGszZprKi
+ EE8jvw8H6e8UJy0PN594E0w1SW4FmE+z9t7fB/QvY+znJE+6VQJVPuLYbIIruOLtoHz17bi++/
+ sZevvrKfvj2G/v1N9/bwWTfZuuV/dOf/mB/+//8V/uH//bf7I9/+NGm8ytNhrolFXbCbV1Vpx9
+ P5dIIUGN+xo0u/fxOUw1VqXCqRoyvECSLHI8Z4LqWOyZfNxpGQjtPlicvAneuVKYbUjs8ZVZr6
+ 9CgbbUk7dQcgIqWGw3K7I3Hcs0kq0fLL0tlfFLwA8oBmuCukYqSXTsv6403qAdvWIY/DMqc1cr
+ m8PRrzybRwVPRkPFjHey+8N7cJJOF4kG54lUMwzu4fs4V7OShk+270LtrGKvyPQ/VTvi7+Ko+5
+ 5RzkMt7sny2PrGqzDn+n8EiaWiiMVyjBM/j8KGeRAQPAppkpddO6QD+GugqVhCmjFT5buj7/bj
+ oVSB9bOjx9RYpH2pzOSqDVeTd/lUBQvDhqoLSOz9+XoJGKnj4Xj7OA4zz+DS3u0M2lHXs+OhAr
+ pZPHx/bN19/ZU8fP7JBv+87COpUMijqzcGgjZ+X1rtFczyf4qdonDuZ+BY4uxJqk9rYBM5NqwW
+ veDZppLuc/Db4byDqZ3L4dwOi33lhqRHU1E+BffY48tVVNG4zHRtN2bvyzS9gv6WRfrg6CNPxa
+ HJl6ZoZwi38JpTGuG9HJyf29auv7cWzV/bk0RP77uVLe/X8ubZXXa5X9sd3b+x3f/N/2z/97h/
+ s7du39v78wmaX53J0xFpBma8sftcCe5qn3Ixkqmz+AZTZT0tGnossclqRiVwGr6A1cuMUzwfYk
+ zYDZBdnZ1LooL+nRyDFhZwfkQCufSfuzdo19bINWNnueGB97aD1ZebIRpn6pQpgmXoPNVB457j
+ NwLUAVMlDKkBiNZ+8ZcIXXr0JzNRWbrl8tVzZxWxms8VKu1lvGkx2tkN2GJRE0DdpH2A3S00FC
+ wRzj2rQN+7dnxlZLOoOuwfXgdMq1VGGn03QMTncpY/d80poiMzrq79F9g4txpc3or1RW/0ZHD1
+ ZJD8nk3frAb/h058+J2qTXsJSQj2Y2DOQDVAFiKyggmBKiWb+bvrVb8wDZAWwBbgaFIvNWz4/4
+ cEO9qrT79q+3Cz3bG9vbEdH+/by+VN79uSxjUfDyv00os3PBvt0ZE9qZhvsf0pnv53tezAITX9
+ ZIVSgGBRcHX0iu384m98IonkZFb+//fPN+FOCfHURVtutRPkGNaM0LymnB+jqEtzTGmHjyO9T4
+ BSA9gXsS7DfPsl3+x86dbqRct1bVKs7nZ7t7u/bybMTe/LkqT17/NwO947sYP/Avnrx3J48fmS
+ Ndscu10t78+6N/f3f/I396ff/JB39m4+ndnr6UZl6vz/UzX01x54Xmd61T9ji98LyEnbOBievJ
+ m1kvLhlokyB88eamMfC6eN/z/PxH5bLZI0fP37UJisamnDhyvKtqUZwBjHAWlI6tjct59brMEP
+ Am72WTl+vt1qoCTpkexXNUfbxBriq4SgMJZt2r36nO6BzsCpwx0j3uPGJV7Lf2RIL4pWtb5t2t
+ SQIXCtz5deVdQLQyFrjd3mdNo1wcu7wendrBl+w4Xy7V+dQxgnSzDBUNxdCyGrS0T9fD6K18jm
+ B3sU6eWEEVSO7Ar+r9L4iAKTvvSSmEXG83+DzA+ohsO0p9sxWr5vXVAaqOI9JMeTj0lNe9BDBH
+ KkpxnLqj3gFVJt5ZRO2hocqYIXVBOdKPZCYpaA33ul17eBwz3bHQ/355MmxPT4+1rISwN4Hr5J
+ aiGyyUDdttLPj+2XOqWo2QCpllcmLOxvkkJ4BO+tp/rzv8drXmHX3JobH6XMCKymg2H9V/LvID
+ T6L0qlf5L6qoDo1nppvKXOYgN8MCDWn73/LK227eir967cBvw4rNS2ls/iFximmHz8B9mBdjsy
+ 7o2XT/Z2YNB3t2bOXL8RpPn302PYnh9btD2yyu2vPnz+Vvwy688vV0l6/J7P/G3v344+yOP7jm
+ 7f24RRfmqVWGQJ8fNQXF5fitAGv9dpVKBqVv/aFJ76MYyU7YMzPCAwoVpQJNna0ihBAmc4u1Wg
+ 7PDzUxqd3H96JtwfcBRDo1GMZtZaVa7LU1MyFpiHLhz5iCnd9daXs35UveO7wPC3r0BSMKUxJM
+ 8mY1cgNX/Y49gR7VCn+flxVIn/39bVdYj+8vLFWt6/VgZfzhd1qT0BH5mluDOaWCYv5zFZrFp6
+ 717ry0VDFJCCX2ZJuikBlQLa+gTwweTO03kaVyjb+ZEK6gvhiMtW/V1MB2keruw29uvco3AbZ5
+ wySy+d1nB9visJzDr7uHbiaJaAtbCPqQON0mNwrd5qaVwDsOX7OkwajOEc4mhaqo5KGdKGBVzE
+ eFMM2IcCeQMJSkv6wb3v7ExsNe3Z8cqjVg+wzZkEOfkk00LeBSOC9kT1HevkQ2Ac63VHa8MyVz
+ n5TibOd1VchoaBxHFu3s/Yyg9uWfpYNhQD9z2rabmbtmUz7r+ZnWP+ZKh1vt6ZIIH++AdX5VPE
+ 26qAXcqbq53cUPPfp7r+AfZGpbZ+g4rrIzy2o2Vj64YNDNEGPHj2xb7/73r579dIeHRxZt923R
+ rttuwf79vTpYzve3VcqMl1e2esPb+3v/va/2oc3r+1mdW1v3r63D+dTu7iYCjiQU05GE7ucseD
+ kQhOj2Amo53XjmT58Oje8vMavb2yAmyXe7qJ9yNa79ujkkZqgp2enctl89OhYOvW3b994gOoPR
+ QsxoXp97YtLUP3wM8k1teqPHkIztmzhh+X8NiCPKgheGY1/W66JLCvvSHONQkbN0VA6EaB4D5m
+ Ru3eNG7tgPubLPG6cvlmx8KQjn3dr0jR2ukDSxRjlBxjZBzCfX9r16spfqzLN8snRDGBZrXiml
+ KV8dMU8D46FGHEziWLycsKfh48uplYlqawnc72X490WvsSvKyUNWkyJZFP++U6/xHo/2Qd71SG
+ /Hxme1esHFUKiIZ1jwDqkIgtXMG02tNPWd/R6Zp/TtxlMZb8c3HS+RiYO/JvEQZ+7uHuftcACG
+ 109YH94dKANVI8fH9nh4Z5NhkMpcRACuCw1QejPzOxryPKAW2Dap3T2JZj/23D228GjOPgHdfj
+ 3ZO+KBGH5HFl/HQQ2X6MOHvX3v3D25Xl/6O8/oca5Ex0LsM/mUJXfcZHvsFavb5O9XXv54mv79
+ ptvxWXuDsdoL83aO3Z4cmxPHz2yg8FISo3z+aX9+O61/e7v/tYu3r2z7k7LZrOFvTufasE4E6R
+ k82yTWq+uBfYXF+e2XC/dOhiHSWyRYzCIYyaD0zISskdfuUwAACAASURBVLpGU1bFUDBYLiyXV
+ /bx46n4+cFwYPPpVDw9/Da0C28DrT+2DfK+ASywL0a6p4UkWDSsbXp+rsfyfRaR5PISXgdp6Gj
+ UV/XQiZ210q4DYOuV5I8pxdSQ0Jq9rw78ZOJk9NnEnONXr+1aO9bp9Ww43pMkFD2+HDllYMbSE
+ rLiWzWv2QMgL/xiACcBXlVOjMrKHWfrPvJE3wePIpfzaoSlHoW9sTL7eAjzAhEDInjUihwpawT
+ Cbu0g2WWjGVPP9VYpFt1wvK7S4Zx41p/N/wToDALqq6uqS/rHPeL9d8KSOZaI5xYxr1Tq3bHlv
+ 2trCBxMdzSsx2ePCR5fUDkM9vWHPXvy9JF9RRLz6NCGw54Ne26oR48m5aH6pUyW/pzMvkyHHwD
+ 7yH1LVmcjc/d7dJOXv9Og3Uic72nQxvu4D2Dv+97m9eSVwfYcRkpfN/90sOd7LlmtK5efmqLNY
+ /izwP5VaXH8EwD5OTj6r37MA42Jf/XzbjxBJZDWdzdL87B/vUeRlKqSKNKZLbd2v2sHB4f25Ol
+ T+/b51/bsyRNt6oEnv7Eda/W7dnJyYk9wjeziX2/2cTq1f379o/3+d39r07fvrKOm3bWdzuf29
+ sNHO8eyOCYtF/OlNPA0brk5yaS5eQFnQIBjIrNW1nx7qw1ZnMKz01OB/u4eE7RtqVuasRIPn3d
+ xvVrOvZLUE9lls+kaa/h7ADyzUHz4cdhEp4+mvtfruOolpHmDbs/Yb7s7wRXTjbagiMj6mNRlu
+ le2QaJrVg7WmKKFw6WrbPz4AVjUOTQxacpyHpme5T0ToNLul0oH5NXuXvOfLRcLl16SKQP8Wp3
+ X1vd5fq/M0iM/bq683sLhUkrB8FPPuSMHy5iEDUthwF6PjYUkNJuzZqBWcV2/AzmcLJ8vTehUX
+ +gxmjoOX5yoWJyedjtgX4aCPDYqOql1nIeXbBNtP8qkivpJYzW/hn36183SnPsO47R4f7nwHOp
+ NzfwevvTQMqGeur22bg+PpkN79uKJPX/2xPZ3Jxq2G7F2U3uIUWsRdGt0rrLT6l7eUusU1Iaql
+ wpB797ledwl/pa/7q9VhYAYuNpstG0sCIlAsCG23BTjRMwqM4L67yU15cd01454G+zzcfV7KKq
+ fYods9XtbzfNtTc1GkzYm2f2g/f/uBJ8qhTFrvPrVb6qz828lsfzLgvdf5tlcG81KvBhAyRI+G
+ 3cpH+TlAMXh0PYPj+Rh8+LZc3t18tj2x2NNqOpGbLZtMBnZydGJnUx2bdhu2+qmYe9nl/b7H//
+ Z/uXv/s6u3r+zxnoho7PZ8trenZ7b2XRqKHuwHD4/d4uEDovMO23RCpigzeaAfTpB5qCRe5twE
+ 7PYnKwUCSZOmPQC2C9LI1RmYtdrNVVBLLzy15i2NW9tsbxS1k6mLgkhOvzeIHxqblxm2Ws7dbJ
+ eWa/TlhLn+PjI9scjvQZrF/uxxUhqnFCmaHG4+HYCAJTG2jPTWAQiAzV5uDglAjXmNJVz0Xw6b
+ gPQlAqJ19KCE/hppKCySPBGtGYKWBHZ6Qjsq2wbQaHScySRMR2aSpQqBxCrXmfuuTeWXbBa9JH
+ KFQdVNVxlP+zTvL7AxEFIQQtqTTMHuVoQM7AaiDkeqC5fvOI3rVYUigv3vcC8jrNKsciF1yVQq
+ DqKuQbdxVF3RrBJsFel1nZlT/L+0Huc11aDpnrTF9mzTnDIVDVdkGsF9q9ePdcGKtwsqQ6pLun
+ lYJqGs6U3gOtcdKNZmwBUZO0bhdUmckfj1Jvt+bVdremzq2JLrZN3MmnTEbMC5ALw6qZu4GPhr
+ eOHcz+F4k+xNSG7MRQWQeeB30/+XgEsM/lbaLz8vfjwM+BnG7l8ryXUqTCoF47XVdVDeNj4AvZ
+ 5aqL4+3/Ze7Md2dIjS8/cw+fZY444U5I1sVjdDfQDCNCFHkWAAL2GbqQXkK6EkoC+Ero1tPQAg
+ oTuVhWbxZyYyZwzz5BnPjF7+OzCt8z+vbd7xDmZSRalQqGCOMwY3H3Py+xftmyZywUTnRsZmx4
+ a+EyYiQD6nb09u3Pnnt07vmtHe/u22+1bq+EPAEt+1Dm9wdD2tndtu922RnnLpkuzV9cj++bRd
+ /b4889tdvpaYH89HdtksrKL0bWNALFa1V6fnWtmLYZh/V5f6hlAEvUM2TiFOYLK9dVITpezmXu
+ 902AFwJH1DgbD4GGhfly+SQGPz+h3u5pH+/LlK3c9tJWNrq/kLcPMWR7+69FYYw85Jagy4Muls
+ Z9NJcdE7gig3j0+sr3hUNk//6CToKEILG7hUHbjsgAkuoJTdy3nmsA1mTuosxIhU2b7gCv7mpq
+ LvLOUmsSlc9RBk2Ck5jn5SnJVah0ANg1XUFAAtEA4CsIO9u4fE+in28DvAd7ptQQHRncuhHZya
+ WphaEjQNbw+ySHVfRrKI/ZfAXjhjpXOhzO1Jrp3Nac3l6imzyCw09xGDYWO6xTsuA4CfllFIF+
+ NHgFPgbNjKOa2fM85JQBzTvmspM7SqiVmB0PZUJCtN2vW6Tl9g+rq+PhAowa7TdxYSTpqatbTu
+ EFl9snG13PQd4J98e+FbHQNwwSGt4N9ErSkgeMOmkm8GUZqG1hdzIQTyGaBhG/KqUvhtsx4Ezj
+ fBfbp/W/j2/PPz/ZDLbDiDLICbJGeudFzkJfCXYlUBPsIVO+KVf+U2RfQXg93murOfZCa8SiUk
+ U3RZNLp2Pb+vt2/c8/uH921w5092+kPDDqjtAVQLBQYGEu4vbNnu8Nt6zJ3Fb/3pdnr8bV99fB
+ be/i7T2x+fmrVJRw1D/VcUkM8YhZbJTu9HNkzuVGOrNPtiqMmywdosU0ASKFwyKQB5/EYisRny
+ CbOvt/n9/Dzpo5YuG6W99gUUGQDAGjO8sIfdMhErpp8Hl262CRrHGEJZQYBZWaLmXdk0kGL9LJ
+ WKdvhwZ7tD7eVXaMuwuoBVU+z4XbN/sCtvCbAvNls7J1TBgQbFCXK4lgJlPDmaQpgoaxSNqTzp
+ BmyjCXEO4gpVVObypaBTl2mZ9VsNLqSh07KxJQERfFR9dIopOWGbs7LpIKxVDHJojEKhvLtEa/
+ uQ1DI2kWVaKXkdgepQSrNrlVvBGqmCABq4ILeCSUNdIwa3ZB5ch40XnEpqqReb6g245LK8J9HW
+ bOFyieKvcr0w7un0EVbHK7COeW8a/Zv3dVXfFEHYVVAcOZDqOfUAft6zXb2tm1/f1v316DfseG
+ gb11RNw70Avuo7eRSwRzsUoa8loGnDW9owfP2xMidM/uD+HmDqlivu7B1n68g3Nb/b/jdF352W
+ M3tFvR6pJoZPhez7Ey4taYsCkz1LaWRioX3b8ov1wPZevbvFshh8+wcXnxuvh/rxm85qeNBoUC
+ hbYJ9svbOguo/0Tj5tUhyuqx12WzLz6jmlFq9YvV+13YPDu3unbt2//DYjrb3bNDpWafZktf8b
+ IWR11RytH53aPt7Bzbs9qzOA1cymyxLdjIe2xcPv7GvP/3Y5mcn1iCTnM/sejSx8WRuY6yOl3O
+ brUp2PhrZq5MzAQRZKNa/rDywUYBrxXFStsd1l2uyquCGgZphf1qtlmeZNFXNpnqwXd+9paEm3
+ JgEA3V5rhg0PhVVg20DdsjQNbWaWxQwL5alO66eM1wpoRcAiUZVRbt+FPgYisLKgoHUALZ3g7r
+ 8kmyVACCVjJqKAuzDiwXqhb/RnMa2GNziYLfSsBVUSmniFcdygXdQeORrriqDsLUCmapngb9lv
+ DVZP3p/0TieSUHZ8UXwSbNe0+AV/CGUtYqqsWxco8BelgjRQyBmxblzgkDqtHb5o9M4PlEqp9s
+ YdJ7MykTDxAol2TzUam5VnIzMlNEXGqJkAhful95Z612vTvc42CW6ynXzdEvTnEZgctnu9s6Oe
+ iQ4j2yHAn+tWVOhdv9gR8otFF50TDNysB0UDjUdAgc0k2yYi+XIYoH2Rqa5zt0nDFIOXwgARUs
+ D3c3ForvOCjROyvx9ZbEecPI9SpSN68KCIouAkECbJqw8DuVZuacn61+36+gDmDNQvZnZp/cVq
+ RztJavGzE4iz+6LvH96j448xhOm7wH7jKvPD2IN07KaBlD2T5x94dzoOkln50UyFvAAUK1irUH
+ f9u/esXt379vdgyPb7w81walFo1L4iMODTlcTFUp3h/t2sLNnnUbLeHSRsk+XZTudjO2rJw/ts
+ 99+aJNXL6wNnjAtiWai0dgmeLpjBIbWvlqz04srafCl7gBMlks7OzkV2AMSCgAKUO5lAl1wocH
+ hyCfr6qblBbPJVF7wHB5ZI4Mm4P/Pz8nsISIAezJ1lvxNTbtKwyrI2MliUB51231x6zzmxBY4/
+ K3ySgqNTrulugDAR6CRb00MwwBQKQK2Wy03RgsJouahak4pBVdXAwHcAOQVxeo5iiCknyNx9fN
+ wBYXKuZIHvz9cSTfPa6GRoHkwd0uzaAXmGK0FNZeapuSlo5GJadxfgKeK2JjaQXswu3aqoeipl
+ oG3ePKc8frJQuopPk/OpEhF567ycZ4/z/IF9lITZaOsQlHjNs0KQHHOvUBMH4HTdi69TP0JbC9
+ 1vDrkQTUmOorvOAaB8xbg7NOyAOn9vX0FZK45ii2oR6wyOpqZPLTh9kD0YLfV1HWFr/fMHnM09
+ xWSf36s2lKdwrP6Qgh4mzJnExzXsvOA2Wxgepb0Zkm4BtQEjbMG0UWpfKF4m6/o8udd7/sBXX4
+ R7jdpoJSsZDibHVMxlG1m63nWDtg7WudqnCQ99d/mB5PqEelY9XOqD8UOvGtVoY/6J7D3MxWK6
+ miu8WyUR7XKzb49tMPjY7v/4IEd7x9JM99ttJyX5oaHfVisbDKb2Kw8V2H0YPvQ9gc71uDBiMa
+ VuW3Z2Wxqj14+s08+/o2dP35kbRYNUcQbjVGcrOQPc3Z1bTMyytnCnr9+rbF5nqWvZJMMF41cD
+ 37aB3m4bws3A41VgAIPJxSQQID5qzOKfWWBPXQNfDBUAUXdxdKpCLT3Dq5e5AWgeaDJsgGxneG
+ OF+jY59lYdA4JXl/yy44CEoA92B4qaHDLkjmStQIQWDYDPhmIkZGrE3geQE8TF/s6k18/72cfo
+ G+wjQDgAXGyfKgpqAZkjAAuHvbip9lnVDrjsZrIcEYEMFklJOsGH+ay1HWjQJ2seL3/ckugmTI
+ oWRwQ4CpVAelslgqurqphjCTBRZ27MUidB4/zDd5Q6E5j/gTkQdu4vUOe9bsCyAOGgFouoqma5
+ Hy4VFhBk7tFQhjQhXwoQkE8/t4wpYa0KsNtGPBe0/kcDAdaESLpZQUE7lCcR1ePooyaThfX1nb
+ TOk0GkzRUsFW3tWy2nUb0gJVn52ucvTBvg+cu8M5ZRbqAqE7ZJbD3sYTZ10bPk2iZNb/72wu0O
+ Rivf4A30hWSvYJlQRHkizuwxomLN1/P5G/nzIsmcR4IPEbmnH0KkDqdKYFZo6DWd9YDVaFAm87
+ 17Tuu3/4T2KfHIiRs3KzSP6OIaNRtsLdrx3fv2f179+3OwZHtdAfWaTT00EA3SE7Oe2cLG8+nt
+ qiabW9v2/HOsW23e4amAoNfjd8rVexyMbfn5yf28Ud/Z8+/+sLay6W1KUpCo2hwNt2Qc7u8ntj
+ 59bVdTmb2+vRUPu9uX1IScPOA8jpkkcrIyw4u0AZw2YAPDznZPjdGkjhyfEgrpVJBJQIASqtNB
+ k3XbNOnR9Epu1zJq4fRhdQJALPhsG/1ekWrkdl0bKOrS3nldNst6/f7smTQ4JTBQM1QGm0n4EP
+ 9Ufa/N+r6nWOEO3ESsER9MNqQrBzVkfh6xidWJBOVpv5qpP/CbwPuftOnLtKJagLqLKXwS6MW3
+ j/KrgkG0wxIoc4SvQTnn5Roznd7BuyQ6yEUBQ3LZu90Tdk+dhFbGr7CeWR7smGIcYiorKDaJDP
+ NPHZy07XkSZPoHmeGwrohOWlGdq6xi/G8JxrD/W98vzxY+T3ir/QURoNFNOULmS7BtqVkZDgcW
+ LVW1YAdaiOsrORyWWdUZcOGPcC+LbBvN5qix7hvZGfNqocVlLx7AuwSHZET9ZkqaRMAN3n+Ij5
+ 5xp7DszdtxVcWCBJ1sg72m3SO4986NVNU7CR9+9r2C9n0bbi5lt1jh70J9v6IbnzdBHt/WeLsi
+ 5l9XLnkmZNOb3xsCgS3g/0aqXZj99fAPt0gmfVHBoSRXbyr1PuOiFK4VreciB/xxh/zkgBCf0C
+ zlZGrazLyzd0H9RWrJz2E2XGSGZRsueUZ/fbent29h4nZfbuzf2jbPS/Eyg8ES2NuerYXc1IB+
+ 3KjaocHB3a0c2j9WkvbL5ewCEYut2XXcO7XV/bJZx/b1598bNXrkQ14aKLtHe6dCU74uY8XSzs
+ fje3FyYlN1TgFIUSX7EwpCSCDFTCZJZOkLs7ONdRDxb4pPKwDLaDg7fM+EYnMTgNOgmpwh0r02
+ wzs9uETACpAS6bf6w+k3adA3GpR5EOv7qAFgAL2KJH29/akuQc4AZhGsyVwwR0xyR+pZyQawB8
+ c77oFPAA0aCkfS4iHPSsUbzhif+HGFQzUFOYZcGYKBsUSoCrpoj5vLrB3/x3/J4dLNXlRDPUbg
+ 8CXpJM+39NtEyjHJkkuNJo86cmmo5jG/gH2NH8RYC+j2S1NHiIJIDMVDZbNg3UFjfz/IyhwTNq
+ VbCaCBxrVV0Lx4vRNAq+QkPoTn806FZ8dd3OMTnH/HR9QoOsNdcNqb39/zwYE7iYNa1c634C/G
+ uNqFUkte522dVoNUZVYYavIS+OeaCp3Z80S66KkMD1RwekUYSPjrwvURfY8xlkqonsGpllS7oS
+ MP8Lh9xM/5WZqhYc8e59n/emxTyCQ79vtf8+7edeDjl8L/uXMfn5s+RGkhCbbo0QtZQVZb6ryf
+ dtQNK1JPf2+LAYrFYiz3SrURGLnNmPOLWD/dovjd3JCPwKQ/drfCHs/4p0/4iVq3HGNdhpi4Dd
+ EnuU47tPRGMUaTp7a/1UJksMivvRb7bo6X6FtUN0c73ihFV6awid3uEuf4gRjITybKLNvtNuSY
+ x4Od6255ROiGJ0GlcBobgqvUDkUaT/58O9s8vK57aogWdFwCygC+GFZ/MJbT6b2+uzMZnN8y3l
+ wKza6ulb2Bt1Dxk+WTxPTi+fPdXp97J632zMbloYegH42R6ECN1uTaVhFUrqmFDYaFj5zeogHG
+ cULah0oka1yxa4uL5Xty9I2GnXSFCfAul3HBnfH9nZ2skJoU0DfENh7XcOvRpbRk4VX3Q8/FQ+
+ xemBYCZQLBVh4evh6eeCEMRgUDHYM6T3suxcu3MbBgd6DglRIdOzGgBBRXuLeXVmjVdyUgACV5
+ S1zTg140VphTfeNu1NC53gh1a0pqDe0Oz0FhVcvXyqwLKOYmoa4cB+qcBsrmDCY9Gvk63YPCLq
+ nfN/Zl9yD34uvrmfPPXS0Ggmde2qeUtE5fG/S0BbuV84VQI3qhn/0Rhzs71u317Szy1M7Oz/Ta
+ 5hSJquETks9Gu0Wmb1bI1CDqdLoVoHKQTTAyragi98s0OYIl3PtWSacgLqQuhaLvfHeTGefAod
+ foQwQiiuBTV47omO+7QxUM6jPPiqFj3Udfk4L3ab02QT7PBjfxCuHvdsy/MCQLHC7M20x4GXBM
+ IrR2XGuRdEU/uKdxbpJ+lWRs08b2GyuSj//wwZ7QKh4kgBHPWa6oIL8NPw5Yo4eBqkhzBYoGap
+ 1a3Y7Avp79+/Z/Tt3bX93zwbtrjVrdQeSNAcyHj5fikPBjG22YqLPQGC/1x1YA8sEgT3PJIC/Z
+ XP8cZZLe/jyqX300fv24qsvbEhmXIXsIVP2KVMUbCnUXk0mdg6403JPsbLK4JKpjSZj8fU8cGS
+ GXHfGG0qREZ2kFGib9Za06tgNcKA00FDEh6/nQRoOtl02OF8KXAEr+e6YSdvPPk1GY2Xmkioqm
+ lCEZY6tnw+87BlZOBj0bHswiDm3TMFqKCMko0TKmThvFUNZSVRryooBIcCWffTs21VCUEdnZ+d
+ 2cnqi8wj4aIYufvxhBCeVzgQKhf12QAMQUeNMJt75K0th6dIZHEJgwIrYO5A1mQmTOTVMuWxWP
+ Cz8uCjx8KOPTtp5NHo5haILK0oPAHzz+rXz8RXX0curiMCEUiu8a6TDl71BKrzS1ORyVDVYVXw
+ loIIuVhXhDpq7Swa5FPedd8b6zS2rah9tGhSOq3Q4Hs4zMxF6/Z51ux0V7nf3dq3Tadjp+Rt7+
+ eqlpLnw8i0sEQB5/WuJtgTsUUiRKFQqqHG8CO0GdIljT8C1kWUW6JSc494A+1sCA78qNlUlXrs
+ IpcXMuqhL38ykMygsQMSmjj0Fh/Temzr3nE9PeWtx+z8V7LNVQ0QYyUhJQG7wXhGKwhL7Rl3kL
+ eduM7H+x5PZc5qWsQwKYJdmHmyKfwn7XcrmQV3Aw+spYPX6dnx8bO/Bzx8d2972jvXoimVIRnT
+ LqRW9MIqN7I/MEaMrq5RsZ3tXap1hq2c1Lh60Coob/Ssb/orXK7NnFyf2u88+sc8++sCqs6kNM
+ RTD7hZt+fVYgM73fO71ZGbLrYqUOuVaQ9n86dm5jNMoiFKsBAyvr5324Lg4RiiUTrsn73ufgOX
+ DxrlBU6ZL1q1mpPlSAUCgHAoaqVEWFDrhpn3urfjzKOrV6v6w45q5OxhIydEOyaVOKQW/MNgSE
+ EOB0UksSmZpZVY0BNFKVZk7x6NReFtVBTL2m3+vX70S/UQggVLgGFLZjmMH7DkGggugDQWFkyS
+ rE74X6y77Ai9qcz+oQYwFNDTJ0jt6BbZw8+nmCE6VAOzj+yjGzhWImZ4FGKsTlzmspbKCpc6/O
+ o3pRK26VQMzBC4vtO2s+Ulg7QEHp1PdiyFHpWDMSoHzHeugDN/cGdNlqw6GPgVMa9ZY0XGvana
+ s/l7SCo6mOEZUorIZcK26XRv0+9bpNuxqdGFPnn1vp2cnUttQl+n1mErW1KrsJtiT2ee1kR8D9
+ qkY69lwAq+cjskOcMOMMHn4Z9lt0DcZvhV4WF+wv11nr/OxUfRco0U23r/5eTd/DhnoBjjflhQ
+ XWY3s7+kmlqKKvQspa7ZCWgtrsbJLdE6iqov00s0gu/YJ/2gye45dVE7h8ALkMx5fPHM0MUhbv
+ rIFXCY0RK9vO0d37Gd379u9w2PbGQys1+qE/YGPtONUOtj7cozfyVuertX53KoNd5q8s3dgvRq
+ SS5dHRgIYEsmyTcolOx2P7dsnD+03v/mVnb16Yft42TTq2qer62u3KlZGGu34OCdSmC3Ribu0i
+ +uxnbw5VTbMuEIKjl649dF+AAUPNyoKQAiZJTp8snwv2HkTVfJ+BzDSkHJoHYq9tYpnrPzMF58
+ vM7bV3BVAZQALGeFKHbSAgxQdUD9q91/pe1Q5yUIXcIfnB6QqVbh6n0/L9tgfH3Lt/DhZOfTRq
+ 1evxKsTSDgeDRKPKUyjyyude41GrFW1IkBJRIMZPDr7R8aasjbkkWTfqFCkZIF7NtQ/BEx4bQC
+ corFTZgSK6RQXzqqavQjCNEXR2cyJRpEC2BMoOLfK4LfKypzRrMvREu+js1OvtYgmcvpI6iCsp
+ HVM7vOj+kKhoKsCts6H8/RueubXQ5l1knAy61bW1Cs3o9MoSJd7cl7KGiLflHiAf91eVwX1Xh+
+ 57NSePHtqT54+VpDEt/5gb1d+RwRQ6kHrmT2fXwT7eOZuoXHS45jArgj2WTkxo2jyz9F5inOVc
+ /S3/F1EmwNeUVf/tp8THfP2v8dnxQ686/WbgPM25kNYkQ6oQOUU+CWXe6sbOF8d6S0ZlicaL9F
+ eG5+ZRb8UQIswH7f/PyawV/EtTxFy+iYVbcncxOmspJ+mK3ar1bD+7rYdHB7b3Tvv2Z39g1DcI
+ DdDn+z8vNvW4iXiczNV5JM9L52vU3W+ttsdu3t4JL6+VWVWq4O9F96IRN5BOS+VbbRY2ouzN/b
+ BJx/Zl199boPrke0CisgeJ5OgNBzsOSqNtGMwN5kl2etyaS9evxbojK4nNhldKgtHwcKDKGOre
+ lPFNoqbcOEU35AzJkClaQrlC7YH3GjeHVm36ZjAsGW9LhOvtqR3R2Uyn8ykyiCTJxMHJKBUSqW
+ Fsj/AHnkf2+ZzeFDaodIBdES3aCW1Ev8roGe1s1hI2qkaJZYJqIgqbuuAyoWOYAIarf0yG93aU
+ rMPX/yeAIns0xujUOD4gHJXABFIJgJKgJnMXFJQBntIhlpToHRZJh3GPqcgUXbyvpFnDxbLLru
+ s1hp2LWtphnujp/cgRaDhH2APZQK4UvjkwLQ/AncfCE7ApR7B9YVuU28BVBINU+FimdgRXutA7
+ 5QhKxsHAgeA5EevO4VxhtSdQhXESmwLRdZWWdcCoB9gXNfvKiCRwdNQ9fLNK3v46KG9evVCvRM
+ M2zk82LdOp/UWsE80TiGbvBXsQ9wRhe8i6Dv+vSMzLersA/kK2OfnIOTG6bOKnLsAoKC2XFPSF
+ LL4Imas0+BFqabv6drnbxRo0zW5CbNxrQro7Xr66OnJgp3TgoUzUpCuKtXMG9A2OfmNFdGtlNc
+ /KrDPZTeF813wLde3Lk8r4+7Ybttgb8eO7921u7I+OLR+tyfFTYMiFDSG5LAxkhCwp7NSEhw3p
+ gI4yEiRavb7A7t/eGR7vaHVy24DzOu4qGhx3Aldzus2K5XtbDK2z588tA8//ciWT76zbTlONpT
+ 5AozQOJ4ZOuAD9lgqjGY+5OOSLHY8EdidnZy5SVpo7CnCkVmiv2eL2BwDZNAe3DQUT8mAr66vN
+ Bwc0CCLk5JnNhVQ9bo0UC1FpeCPT3at7L/TUgAZ814Gh9gcBktLfvh7gN6HjWxZu9nUfmjQCiM
+ II7sF3FRqC5tc1QQwL4tWfI4Z9Q8AB2AySpFpXTJLs5VkgWTBaioTkLX1+bpmy4X4fqlsJBF1v
+ TvZOZ8F6HIPsLLgmAFwDURRNy7KGR/27bVEtwnm9VN6FaYLSUoTrQM1c3Fx6rYISyikiZUqzuO
+ nxrEUCJKiBtsCzo83v3ng41wgJyUQSfAhcgAAIABJREFUcNMUvXbcxMz3w7X1TuM4e+ONU1qNs
+ CpiJSBbBZqe/B/nh/0B7KFw0NhD1XDOoN6QU15eX9mzF8/s0ePvZBk9GPTt3p0jFXJ7rZavnMJ
+ 2wQu0PwHsM9lpQQoZK+MbxMMGaP1/xdnniXcOtZs1gM2ff1qBNlFXEZbChiMrN+u4E5gXA2AeE
+ IOTyCm8W7j9zWBYDDz/iDj7In+TUvnsEmYqnBXSwmrVOnTE7h/YXYzMDg9tf7hn/XZfzUZkgOp
+ QjexdTVORSbFkdzOqNADbjcdQ8jCC8MHhsW13ulZldmrZB4cL5NWWvQwpZsUWpS27nM/t+8tT+
+ +iLz+zVp+9bfTZVp2IZ6gbbARmb8V8fa7dYlDTQ43I0liTTqlWbaJrTtV2eU5B0d8vzy0upYHr
+ 9jh7cydQLlejDmTaLuRmySALD2fmpjpk6BEZXC2bLQrPQ7CXlRUXKHR46ggNfvV5XfgGXl+fSu
+ i8XM6uW3IIZsIerp5jnPiYlNXdR2KOZSysb+cyg7cZtsWHtTseb07BfaLYk7UNxpIldpZJUQFc
+ cB1mzgAM6ya2PCV7Vek2SQm8w86lVzACga9iHuxBIPLgA9N7lu4o+hJpZ2XXo1F6uxyNbYsqmp
+ qZYwTFKEXsCOpHnnuUTOABQwJ1znEzKJKOUbYHTKrIjtpK2m4CLQMu5lRV1KvgD5NEtDDgT5L0
+ IzErGm5c8s3f3zKJoItEHADuZfFVDw91HR4GAoTLcD2Ty/Z7hmYTOvtX24TUEntliaucXZ/boy
+ UN7/uyZzvHR/p49eO++7Q4HopoAe64ldRe3p/DRjjnA3CzQSkRaAHuXDwbfHFLCtUx4I2PdBPu
+ NtcCa9PF2xcxa3p5l52zzBnhvcPY3wT6Cf86krG0/yJJbBYd5naKASc4Nh5I3wD5bjaSNFMA+d
+ dwmSvkG2G+enfU1xq1gv7kMydQ5mwL8zRcWzJhuW8r84O828Dopad75vszwJ9cYq+SqE6GFl96
+ ufBANcbttvZ1tOzg6sjuHd+xod19GZt1mxz26NRfUfWf0GfoPWnUHLtQS/ElNSrKuBeynZltV2
+ 9vftwf7xzZAoolXdRIh67qxH+jtKcNokJ6Nlwt7MxvbN0+f2Je/fd+uXjyzTqlkra2SrWgsms+
+ VTVKQRd+NYVh5hcXvxC4nC1tS7FwBbCP31aFYiV3y9Vh0C8t9zMzG45EoCnjXeq3hXHM0xJyen
+ Cqb5oGnyAuXDb1DFs2DDCVD1ysrzqtLdOU0RtUl0aSw58XWhcYS8npJ8UpQCa7U4dABcrc97go
+ o5T3PFZH1bs26YbwGuPE67CYmY5deusZ+Kk4fPlkUCK6QjYZWHdBWFB/ZBseilYv6BEb6HrBnP
+ 1JPgYaAR5cyBVY5alboKm1KEaPawQjOHwrIl9kckIqnWumVbTqZuR02YxI1YculrVLgIMlEQjt
+ lP1zyKEsGhrhAWzF3AJoKgJ1Ord5siFpLPLz090hHQ1fvYOcUTvpKXbb+Gs/oOc80p3GsZOqSq
+ apzGPqqaq1uRwDfo/ja7emcU8BnCAl1Fp4ZKKhnL57b4ycP7ezs1Hrdlr33s3t29wj77p7bJSS
+ wL0MNeYE+H1aVstcc0LXfRbCPgxD4ZcXaTY14AeASoG3SFEGJZH9Ogsx3FEvdRSep9DaLuZte8
+ IEehVS5UE/VUfBpm3h7gz7JMLsA7PHujGYSVG2CfYqIxczeZbppG2uU07uAP+3DbTTO28A13YB
+ v+7tziGuM0w/ie34H3/bSQsdTgd/zaTSxnWgTd77XZWgCkiSfg+Pk/LCM7fdse2/f7hzfsUPol
+ uGuDds9awe3XS57BqslsToT/Xseei3JI1vyZTZt9u6hjp671GjY0eEde7B3aF2WuWn/UsxJDR0
+ ss1U3MGPE9sV8bi/OT+3Tb76yR599ZvXRhfUZILGaa0wftdHJcqEsH7fD8tK7N69nK5sIWEo2l
+ kzTRGmcX17IiA0+nMwRRYqoJw0Kr0pqCrhAHbCkJ1u+uLqUYkP1B8kVg54h85b9QFNAhVpHBUj
+ RV/jEz60eWR48MQAznVzLa4XvWQF0251M4uiOm2j9YzC2AimUEl21Pg4R8CcoQEW8ePFCrp5kr
+ ARU6ldw8z5QvWQnp29U2+AxUecx83oDdMmkZZymFYSDIJ2y7lJJLwOBIwaELEuiNPiCeoNfJ9i
+ lKVHqUA3A5RigWxDWcK+kGQJQSIA0KxM+n5UF75dkUvsHxeQduJwbDVepYSLXFk+eZshynrUKU
+ bE4jS10Kwsv1jrApGHtKqzzHnVchsyy6r41GH4QD+rNuvVR4WxvW3+A501fWT0Bs173kZIcF66
+ eJ2en9ujJY/v+6RMrlZd2cLBtP7t/1+7uH1iv3dGKREPNy9xj3Mk8azkn7dp/xwE19MXzeQMXE
+ o2TlDlr3P1tYJ/lzQWg2OygvdnGU9yuyp8bCo7lmhtDkaO/kck6RK8xLEXKJ9X18vdt1gjSPN3
+ 0CsQinpMmFaEbm22eq2xVUJh1nAQi2dZucPiFLP/3AftUJPrjgf2N1D7D9HSps6CYpTkO7ulKZ
+ NeSZW9YE5eZrjMY2KGy+WM73vNu2F6LG94dK9WcUc4HNCtLk5tggL3zONl0otRcI5plOrVqu23
+ HR3fs3s6+dbaqMj8TQKwt0/wz5L3EkrxkKrSejUf29cuX9sVvf2tX3z+0zmpm9bDppihIkxVg7
+ 8t3XBppqFpKs4+m5BoOf4wdwEKFQzIuHlwyYxXrymbnJ290r5ElX19eWacFX9sR+CpAUARElQJ
+ ITq5DseNWBZxeAulWqaKiIgEEC2HoGsDKs1UvCr95/cpGV+cKLIeH+8oiJY2UUMQL2rxPBUlpy
+ JcCPZ+yVbLqVlVZZ6fVslevXqs4rKHnthTQsA1oNsDz+6fPRLsAvi6bpCCO0Zfr1ml64kvbMXe
+ uBODVFxEDusnWJ5O5ahiscAg+yDUJgqlmktw43fffs3enbWIaFjUWVh3lso6Xz4AeozCs7C/qC
+ A7e/KxFlIN9q2VvTl6HlUPMkI0VQAJ95+/9WFy/7+dSASDuS61SwvmS1Yoay9hIuWQtbLl3hwL
+ 7bm9gHfpGWEHJI8lrUDTOcUxQgk9fPLOHj76zy6sza7dqdv/usf0JYwl3d1WXQRqrWovAPtRma
+ SUtsHegYV//3sA+S3SLyeRPA3tw3ZsbM7i1Itg7mL8rWX27z0+saYqz5/1aFz9u01ohk1ymnoM
+ CZ39LUEmWymk/b64qipTaTUrnR9E4Ga7+EI3zgyfrbWGC398C9Fn2HpgfrAwOfcWX51rcUCzwP
+ rhSuNxO14a7O3ZweCRvm/3hjg0ZNJKWpNAXMU5vSxSE36QO9usXS4ocN8ORmgMwSCZfjW7X7h7
+ dsWOUODx4uiIehvIo7IXWJBGlW5fs/mo2tWeXI/vq6y/tu09/a8uzN9arAHyhwoHjnnh7P+U5C
+ rWLmWd6M2bIzuZ2eo7FwNzKlao6ZhltiHwTQGHl8fLFc2XIzGql4aiJLXN5y05OT5X5tTrtGOZ
+ NUxfqEgCNYScUZrE0nsn6mMybrB6+Pzkg4o8jPXanba9fv7TLizONJkT5AaUAJaP3ygDNAQ0AZ
+ jto3znX7CcnixUH3DKKkauLS3v1+o1oBgrO0Da8h8yeM0nmT/CBFuJ48GphtUEGzTHDmWtgR7m
+ kAHV2fiEAdAsD17wDqOjmNamp3hANBuXioxOpX0C/eEctn8fjBOXlq1znrVnJpBVLp9tRpo0Fg
+ dN8IbcUELsenwNNbp4EsNQfQbbuSinn6NM/NWdRG5FNsmefmeWxmgLdaTP9ztU5XsBlxdYdDGx
+ nd0fZPasmVh9sFxVQVWMIY98YoThf2JvTE/vu0Xf2/MX3tlrN7GB/x/78Zz+zB/fuSo4J2KsjW
+ sdDbaOg/c5w1DP7G5x94e/+tgSTtxclb9Ii65SPN7ZlUBuqt3WevrjIlhtNIgXkEroOkBlEhiH
+ pGvTrh0LNQXHNX5E1SBV5pchMs+1tZO1ZZp8+Ndk1pyz9BuDn59nPm+PVW7P7DTz+BwX2BXIm9
+ j9X0vhqJz8wQEOZizIlP+WSrPEwkClCIQwGtreLhOzQjnf3bbc/tA6mTvK38bb6lQAVyRoDuGP
+ 5GdzrRiD2zqhoxCqCPbxtdzi0O0d37KA3tKY8aVIAiwCCDjpGH2pvY4QdKp7xYmZnk4V9/+alf
+ fzxB/bsy8+tNZvYdpPuU+eL5xMGhuANszQYbwEDjUJ0uC5m9urkys4urqy0VRWtQ1CQLS/dqJr
+ RytQpH9NHsRYTN3YR1UrqqsRVkt9Rh6AAqt1UIdaBK1EsKFb4HECXjBOlTIOhJ4O+KCBZD0Mbq
+ ZvVqZo9LHXrTdU6yLyTj7o3BeL77lQMkktWC3v7e6LKsICAT+davXlzImki3DOvPTk5URY+vkY
+ 9hDVDW+AK0MHZAzau0vGGLVkkh+onOU8ChrM5IO4+I9greDGXngVX6QBY4tIV/MviwDl+gq4ya
+ IKDBptj+dxQxquxf+jxx9fePCVA9eAgVQ31l+D0qX+ke9unW/nQ97Ra4V4lIPCzJyNeB/Gmt7n
+ UVO6hk5uDKFmhcxm55e6O7TAHedCNFQzWB07HiOITYCe73ZUK3U++/94ePvnWrq7OZHL38wf37
+ U9+9sD2d3c8uxdVWKAdbkj//v7APpbthUJw/pvNbBcA3CzUJqbGywSOMoLK6MNJ9IHDSx44UrX
+ vtr+nV+r+LgJ+8TMyuIrtpVNcEJwyG9rf7vfW5mogxzytEXPpZaKUEkVTjGiFfco+m9/9FM6+W
+ CS6LUf3fX7XMujdmT20RsFeOg+XCfqzmoBnvHKbTIZS3Pi1slXqdWv3urazu68Gp4Ndpijt2BC
+ eUstWL15R2GZXnVFxk6mUESW+8caRqEBHhgdn60OkVaBcrWxnd1fyzR0KWeyT31lulxDfJjdCB
+ 3s/V2QW0yWOmWans4l9+vXn9skHv7Grx0/soFWzQauuBigoidmYAuLCDboi+LFtirevL8fyvYf
+ SISBc0+UpNYsDDyAEb80UJ7T57JNz12MVYDknqcsW8O12ujonF+cX8sGnRZ6bEXomKWf4EGihq
+ 4sLAx6ZcAUYk+m614B3KPP64ZAsv+l0TgwJl0kcxejV3MqAa6hmWE3cv/9A73v6/VNRPPDoNFd
+ RVATsydQpVGPRPBp5Zo+dAscFAMsZU5RRVQZlyDhZWWh1FKsyrCY4L5JVynaBwDGyBpa+DWwp3
+ ESNFVyyiki0DNuHKqO2g4LJ7XI9ECRlD9vS6oXOZ9V/KNg6DSSr4pS9xzKV4/WVBJ/hRVqnfmj
+ s8l4BdcSG7JH3A/YW3vfJikA8vuyM69ZRnWrPtne2s3qH3FAjoPg9z3yCoCxDseS6++/s5avnC
+ i5H+7v2swf37A7F2r4Xd6HjXLKUv7coJfx7yeyLkHFLFl60QE44W8zrfWWQgsMGjROjRvO/bsK
+ XZ8+3/z1fEWThwZE/e30B9/13aw6eaWZGYaUQU7ducPZxEF6VSYFhY1/fxdkHLv8ksH8XVP/hf
+ wvKY43Nya+Ufq1h1DGJhyi4VZK/ChlmhRmb3bb4yN3B0A52du1gsGNDugTpJMXyQEVE9bVmI+j
+ 0wKVlWASAbPkctgOuhgi+Hj006osE9rIINq0e8MQZtDuG4DKpKGSEpotFJ6wfXNqmVGks0TUwu
+ 2TXq5I9fvPKPvr0Y/vyw/etPR3ZAT4lVfdYQYI5nbmkz+ec+sQkQPJsMrer67Fdorufmzpsyew
+ BjlQoZNuAIFkuLfmahCQFTEv7CLgxEk8ces3BDn0+gEMPgebXXo6sP+gJDFNmTZa7GF+LbgG8l
+ T3pmHzuLCBU2apbiwIfn4uVwXTi1wK6YUFh1QeIAHiA+5//+V/IiuD16zd6P7NyaaCCGoJbPw+
+ HTy8aL60/GKq+wDVeLHwqF0odzhE20IA5BWm2SV1jPKFgWrF21wvIBD2AmeDB5+PgSderg7q7Z
+ fpwbzdGkwc8NMlWRSsAaCvvBvaZA7KjCG96Vn5ZoiQKxlcRfh94hiPKTBSZj44UkEcm70odv3c
+ 4X+oBiAlgZVYImZdrMppzW+N6qyWeHrBnvgAyS9kYo6iJc6/NE4TCk4VngXv7/PLcHj95ZI+/f
+ 2KT6cj6nZbdOdwX2O/tbdv2oOcGd1VosuCalT3FPR62In8wjbMG9ps8tFOuG/Fg4+cc+v+YnL2
+ ujayw883/6AJt0MVpxOIPg32RGi4c7kYwXCe8fmJm/4cD+rs+YZ2Hz6Jpyr5RNHCjw8WHdK3aa
+ Vqn27ceN16rY+12T8DOBKlBq239dse6dQYteyesLghVrJVTN5mcUne8c+yJR/THUOSQK3zcYFC
+ PHBmdqAwpcSjMlezo6I7dP75rPZa4ycxIUcSLenqIePijYJeCABpo2u+n85WR355MpvbN86f2/
+ gf/0d58/ZltL2Z21GrYorS0SWmlouxqOrflxLXW0FbTBQXaiaibqRqwKnY5mvrvlnSQupeLN/d
+ cS32TjLl4Lzp7QFhqn6CyXOe9EOCT+XN+eT+Kn1bHm2wYf4j2Xos5OG41NXkXsyiyWE6S7S7R+
+ LMy0HZQF7k6he3JdXI6yZqxKCi+9957djUaa98B02fPn0vOyKhHiq8vX75wuqlGEOmKi/Yseib
+ pKNeHVYwcL7GSWK5sOBxavd6084tLUTDsT7PdzuoHAKE6dVdLGeE9f/40m4HLPmjIeQGE1WQkg
+ F6EXUPNJtfXCiYEBorjvJ5AgkqIYJb8b9JkLa0it3yVKkM0rW5IF3xVpNUrRWxsGSLLd8APmkd
+ gP3fzOwz9nDjTVDF6SQY7Ozbc2bHeoB+NUW6fwEougZHopFCzETh4NsbTsfxy4O7pp6gzynDQt
+ cO9bdvb5d9Q/H27O1BzWlLgJLCXECFlsrdIL/31P4Kz3wD7yGuzDHcT7DUevsAuFIHT2xmLGTF
+ d9JuYdIucsoinhaXCDTBV0pi//wbYO6eZfVrO2acCbT5Pt/je9H2e2f+eYH//L/8qy6XXCtW/D
+ 7IXOENXtudfKUtRxsKvyZSK6xwy4EIvsiwKPO2wEu6KWKxisoVkjKlI7Y6acXqdnm46DLLg4ls
+ 1H7IAh8w/sk1pqWOJlbhZqd41a7goFcuXFelCZgs5IZevPpBEuvkZ+u+ZAhBZ/b2DY9UEKmlmq
+ G6kyHR0yLHI1XEHFMrTHItb35/r5dKeXY/sk4ff2We/+Y82efy1HW4tfPoSBUgGaNABKt+Xpc1
+ WK3nl+Ag870gtlat2cXVtl9czBSSM1K5kR1C2q+uRXQYvLRpptRK9BT0jPft0IoBHjUKbvYaE0
+ +EZTUisAlgVsLq5PD8TAEtKyH4ooLjUJHV9opQhU0f54X4yqPa2QsIJKNVtgd3EZCKaodfrS40
+ DT8+sXXTxcNjsE/UBJIN0np68OdE1HQ6Gom/QrMsD//Jc4IojJ/UDBXRRH3Tn4tmDDYPPp4XOo
+ dmIgEiggL7h0kBbQXu8fPlcck4K0QQYPtezcudXNYdVyhmKvhi9obtf2fX4yvXtyDyRycLbh9s
+ mK6kE1mmVJ4/4cMjU+QkKRyqiWB3x2RRVlX5I0unOmivslle+QpMcU/r/inV6fRvu7Cqj7/b6k
+ rdShFbDW1gUK50JAFIHAXSMshpfmbx688q+ffitvX79SgxEp1VXRr+z3beh7BagS3dVF/HRh8E
+ 563PXOI2sJpGRIlkBeiMTTj8Wiq5+Y2f/t4YrRU/3tMWEK0U6J1Gp6XlPmyka5RY5+9vgL8fpj
+ Li5qd4pgrlf4OIB3pRVZufKacB1zj6neHxNkxNSWaCMX+V7lMXBwusjOPz9gv36KUpRVRloFCQ
+ TT64nTFJGT5fJdKV4SD40VUC+ZlX80LsdAcBgMJRGGCdKZru2ag1r1ZvWipuYGZvSe6t45U09D
+ vRR2Ejdimu7Gcvjgg+HD7XYOH2KCX4c0r8D9vOJJJFkrvje39s/sg7F3wgiMi2M4BYLB205Lwz
+ 5jlCnWC2co0W7cTKf2+OLK/vktx/b1x/+ylYvHtlRn2NuY1Ks5iq6VueziZqukGe6DpzCXVVLk
+ IurkV2NoEsWNgo/nXKlZmeXl3ZxdWUVdX8yjITBFA1TociWmgSFPQJqEoBVWnwGc9CV28JbZSg
+ NOsA3ujiXL6RLA10J5Q+8Dy/htLMSQF3CdryI6WZjgCuXvN7w5iLeu7u7b8PhjnU7PXnlvHz5U
+ ueZAEDDG5k8P19cnodiaMu6nY6y0imS0OlUqw3nulHgIA/FgqIp/hrAVgfvfKYiMoVbJIjlSlN
+ BkWPY29+1s/NzHRMZLZQZA9U5uRRJFaPDJE2eOTQxVejydVknAYWglGo56sNA5ikQdkMz3Rcq5
+ /j76Wb2ojU9A/6lebkxfYozCtAzDpLPwZMoKZjg7JcoiGThjJy3pGRoZ3dPPSUomzhGAraGxsP
+ 3EyiCCvJENT0Djk1aqM1n8iR69PiRPX361JbLma5Br9u2Qc/dTdvtpu3uDG17eyBTNY7dh7K4M
+ shXzjlllVKcYvfvzUlPxRRxXZTh+/quv2/8deP1jgObGFXkjd+d1W8WT3/o5xuZ/aYUM/t5PbP
+ /sTTO24u5gSvFg+XQ/xhgX7yYHpWdf5TvRzQ9xYy9wpkv2ZyMQ3asDWt0W/KWB+Thi5mJ2e/0B
+ PJkz81qXe5+ZCmaQ6pWPnLX8AopSNdSBiX1wFu+JFsjMITnTRowkV5OAZKL55bGFFRnNo0MF+C
+ Ewrm3f2idejMDe1wh/dhdq5+Kz7eBvailBY1WJbvGJXE2s6+fPbWPPnzfvv7gV7a9mtlRp22tR
+ ktGagSd+RyAcx7fuz19TirbZKwhNAgzbCeiiEpy9nxzcmbPXrzIOiZb0rfjSY9PTD2sCa7UQEX
+ 2SLZ7gRoHpUyVYeQNZX9k95dnp1o5wc2jnHF7BFQgLk8E8ODOyaxpzOLii5ZgBCP9ANgjI31Fc
+ 16p2fbOju3u7CpgIe1kFQI44tEDuNENTEs/XDjXG/Dn+sOXA3rsJ1k/YCbefEogJqNHPVL1fgi
+ ZxNXtxavnKiRrAlOzr1UVq4fjO3fUn/D65I1WOnxOveY2BKwcxKnLn7/luv3yltVrZRWQ0xQwz
+ hNcv9NKvgqEdgFwI7PJvG/4rEajLXsOaCf3pyeAevOaCrIIzGru48PvOA6+OA4CAaurme7FlVR
+ Wg709Ozg4tN4Qnt5llhrmrqKsd+im5sE07MizfMkV9NmsHlDlAPSPHj2y8fhKiVSnjWdS21r1q
+ kz2CIQ7OwNNZ+t0eq50q3pTYQJ7JXgBtFHRia5gd1pdB+B1wP4p0kIlj8Xn+0bR8pYu2Q0646f
+ o7P9YYH8DxG8UaFMQvO148jOwYam2AfZvhcKf9ofUfJVpaAvA6/LDyJzVDbhlZagWikadvpwTe
+ /2+268y9JhpOU1ArmFtqBz08fGAyyYX7jH4eDr+fFWa1jY5GSNb2QD79QWmH1uW/QfdIn1OUDe
+ cfA8TPvA5+cxPlzNlVADWA9E4h9auNW0rAd9PAHtfnqPs8NF3I2bVjkb26bff2Yd/97d2/u3nt
+ lde2NGgb7V601YlMny81bFJmCZOylvnaRRSo9XUrukFWJiGoFQbHbscXdvTZ88FRmS6bJdlP6A
+ AAMA5c4xpwAiZMBkzX3y2Bk7HMO3LM/T0vmpKqqbkw+6yzJhluzK12fOzTMhiIhSfpw7TVUlNX
+ nt7exqeAaUD4HJ8BC8a31i1JDtn1W1KWAq7/z1ADJ8+mbp/vRQ50CJQXTI8g6OuaAVB0IF6ePz
+ 9YylvMHWr17oCJq7rwcGBnZ6f2es3b6S6AsC5vdJ4R3HqBIxmW0VfecSUmCXg25Viid6GMFwje
+ 5cMExM9WVQo5MU0L983WVgYK4frrGuWmgNLHySpBALoF+hJVhp4F/GlayFrBVab0HqmBGnv4NB
+ 29vZEc7qvPoN3GAfpGb13hcc/HVuQBDGYXdQlw+4nE3vx8qU9/O5bed1zHsjo+4B9E0UPoWapH
+ gjGHBKoWQkmaabjqAcRVuw+acu36yHFk4MimxtPYw7aRcCOF/4goCe4uvX1mwAZdHP2ofnfU1v
+ RDfpmYwduo3fSQa0fW7AFN7aVfu/nJ1vBFF8X+JMHgrSfm8dTrBn4+U//v57Z/zRMv/HqrFVa1
+ zH6WoMe0QVems2lhTdl8ChoGoyu61PoYcrRnho++vjXNDvWYchx1flklp90UsL3pps1NT9lJzt
+ sFJIBF7LMtHLzzD0mwaSeW9Vlk/K22KcVzU9ZlTyWjwJiV8XwAM9WLsEks39wxzN7rBcE9qKtf
+ kpmz8OwpaU93blk92ezuT0+ObVPvvzCfvurf2/T59/Z/U7NDvvIGNsaYI7GnsxOnvNq64S335K
+ /JtLLEZLCudklVgqjsX53enouSgXgxU+HAjYul2STo8uRsm9AgqIfNQlJLGXS5fbFZHiiOeTRP
+ tEga16TBoCIMqgC7uxT8JasmDQ0HQBzrxqNglwuFdD3drYF/HSeTjSDlq5bumHJhvGN930ku4Y
+ mWSxnNhpdCjjmU78OgBHePnj1cG8AuGTWaqzCjAzTtFpdA2qePX+mVQdWxKVyzb3gy2WpcJ6/e
+ ul1CzVmIZtEAeU1EVkkkLluVXWOOY7l3G0Z+N4LrDRBEdS84YjfyaNeDprecarXydPfh7W4vNK
+ L6NyRcrdkCLyGxjjYE9TYD44/Ga+5xXGM06xUVJDd1bjBvlZlSWapDltJNoPSTIXEoHEEuuqrc
+ JdU9pXgR9D77tuv7dXrVwo+zXrN+t2mLKaxzGaFB57gQkqwPtw/1IAUAJ9Vn3CgiJoh00wF2sD
+ 8TJ20npgX+e4fkbWvvbnw+ozXLtI4CRTTVOp10CyuQLIA9FaQT0FtY++Lu18o7BY/L6d6fqTOf
+ o0KKu7zxv4XawWhFPp7BfvUnZgkZeJx9YR4oVVdrdy0dGx28dKGoulbbziQ4yTTncgAWVqTvdf
+ l4Af3DsC77My79tw4hLhWAAAgAElEQVRJsgj2BZo9i2TS4Mf1TT43fJDrRJw7d2c7n6uqFUnKB
+ gr+Hr4S8HF/mF4lsJ8yGIOOzWo1wP5Y3t8VTb77aWBPnEpyOvxupgsT4L+5vrZvX7ywX//dr+y
+ LD/7WWpev7U93tm2nt23lelcduHQ5zmfh1TOduUVDqHxQ5KC9n8yWdnp+KVrnXEM2GAM4lka+g
+ iOlioR+jGT2ZMAAMbNJVZSeLQS2KFrmE2oVbrvAQG+4LyCCVRaFVagTsvM0zSkNE5e0Ul46Mah
+ dzpArG/Q64vShQtQ8FB3KslKoeOBAhglFAIUDwEDB8E+qqDFtZj5nQEIAFD4KZlgcw6PXs890W
+ eWunZ6eaPVA4bNcooGsrOlNrHAwA2MfsGKYjhnfmMDeJ1kBoooNcW9SO9EKINQyydKYfUvmZ2k
+ yGCDuZSqnG907J1RJCgBOearpLu5xmv04BwQRKCEPPK5e00q5XFEHdA/74h33q8eGWQVZaM7oy
+ vWirts1RPro9cOol0kXX/CyIYCeX5zbw4ffyQmTLl4AvNuqC9yZ96C5BvL98YEtB/u7dnCwr9o
+ OzVhpjKcCSQR+EaLRqV6UpN4GqmsQf0Nnv8Hhv+vvm1l+tvh/F2evkxMIvpmVx7YL2bfO6mY2/
+ tafU4DIP/fHSy9/OKv3fcmjjTCsyNnnSf/vl+LrQZPbY7i3S1ZWsXLNaZp2v6+bsovFan8o7/h
+ +231QUNG0omFHy3SZkTm36CmHyyK95TweFskYPVNJN4qAzuU++n0+VSpdXc/lFTDS8lVLnchA9
+ DLfpjv6xXJXHbru7UK2C9UC2GuJXtmyB3fu2f19wJ4CrfPXXg/mXLiy812cPcFGmaFWD940Rpf
+ u+XQmOufjLz6393/17+3VZx/aUb1sd/pD6/WG6jPAdkGu8tQR6OyMFn2C2PV4apcTsvulja6nd
+ jWd2WjsfvkEAiZBIcVLpxAw1PBtgRI8snedEiwB+p3tbQ3Whi5ZyMYBeaeP4mN4i0CpzLCOkV2
+ O3NeGz5f1MJQETp0BWEmd1KhDDaVRgfRArNzbXd75bn8sZ1HZG2DZ686ZZN86XgKOHB/9LnCHS
+ 6dK+K8y2rLry+WvU6fR6kJA5vp2pKRNKVcIMGSxTmNcywEz1UNYqdBExT28UN8F9xIDwt3cLaN
+ G6MSVesj7Bvhv8qfXsHLkqXFvunTQs/c0blA0lKOw7r8kOuCe0nwDrZhSLwMmf3Xb3d21vcND6
+ +BmCTVFnaJGPcPVaAQdUZ5h7JcDq9J5p1TcdCfDNxQ+UGdPnjy2J4+hvWiuK0kQ0dVAcoKeTzX
+ j3NSqW9bttm1/d9f2D/Zsd3tbtCwrP1lAaAhPGtjhTWaZr1WCnIJ0MeAwB6MfLNBugv87gkGKd
+ 5uc/Y0M/O3Z+h+Ls98s7L5Nenlz+xsAv5Hd3wD73w/m411aEZXVKLRCTVOvikNEBsZDuxvDu/u
+ drnVbbWujqKGrlQHGeojhQ6X4XWsi8TTcC6TJQ9tXhuFzEQelomoWjNd9btJxKdsveEoUs/tUS
+ kg8l694PZhojwTArq9Xhr90/TrgA41z/+COwL76e6hxcLnESXCx8qlEuFNCeV0uzE4XS3v0/Jn
+ 99qMP7IN/939a6eyFHTdrdsCDDeA3OgJ8FfYoitI9qilNZdUURpO5jQLwX52e2xkDzCXTdCqF1
+ 6OwkU4+9NuAlQOV11gAD2opB3t7evAvL87V0SUrZCi2JvTOll1DCwHujOm79KlYbky2kESPTPM
+ aekQ2wPj9LK3drGqIOZw1XSlJk7+3u6fsFFAWnSTTLQaVdHRdKOKiGAGE+ILqwIedDNYTVJcQp
+ k5ZLAJUd4mGMpmoMdKw2rD+cOjGXqWSqAv53osiG4sWYsUjyeNWVTJG5K0EU1YAgH1yqOR2oTD
+ r2w1LjtiXVLNAIOlJizcFpfnGydmS1azPC/YalPT6WgWFl33o7yHpUdk3un07vnNs+wdH1mjR6
+ ESArQvsUyMhPkhez4lEKKVHKeWOFZFyndhfwB4qhyItvD2eR6w8GrWKaByAnRUktZDZ3Mc6Yr/
+ A7w8O9uz44MB2d3fkvUS9AXlqui4+6zetsQuo8y6wTwBdKMPeKKhucPzrK4MsxKVvfrQffTHir
+ GXva9TOerZ/U40T4esWzt7p5p+ms//JYH/vF78M5qLgQ1MY7pRx7wUI9rpLFBKyrtCyLRmCQYM
+ O7ebtlnV6HS0t0UL32l3b7g8EhqnQin2uZJIhCViV4P9ca5qWd6JqlKWnhyOmrydVT+A7QI9SJ
+ vliOEY7D5bkXcUibNaMkW6OrFkmvyGyVuzoufJsO6yHKQiGzp57T4PK79y1frNt1ZgrrZtCO+L
+ j4iI86b9LTZDKv7AgJqSsLEAkFBFQ8ddWtpcXI/vm6ff2N3/7H+y7j35ljYuXdr/XscFg21odO
+ PyGlbAe0Bg+POnDjhjOGFCezezqemIv35zKFpkCLxp9jNMAN2yP4XWr9aoomtOzMy8ohuVzo1o
+ XzTEcDOQbf3HOZCy4ai9S8j6+p2uVB5uhJCh6xGXzYFtJFAnXgABJEimzsuXCWi2mRc1kzYByC
+ SqIQTCDfk++PFBJvE+D0BcLFQSp7cjvBnfKGXSO21b4yD4vuinYSe5ZEgXFraoMezFXQEgDwcX
+ rUyRVcbVmr9+8dkvlMgNF0OMD9m4XLIVSoyUqJzWqMSTF5abudco5yIqy8tlxi2zA3C2X3UrZ5
+ xl7wbRYzyBoVqtOdak4jBRzxkhHbxjSIBXqQoxVbHds+2Df9pFZDgaqv7AfXlfwQMt11Ao1PKT
+ WBjUn6iM9x5FYcbsSiDn2l69f2Hfffmsnr1/rdqbBCqoV2Wu17rNuNXIyVpS1GvbWbc2yZbzhY
+ Ni3fq9vtQrWH27+7WqgGBCkbed0SQLTrHgbIF8sNqYnZ12tkxcj9fcfUdxdS25vKjNv0jLComz
+ r6xLtTUon4WQWWLMDSb/JTOT8pYnSyZkKx7DNIFXsAso/fCPurNM4fM691FRVoKc8uqcCa1yGt
+ MXs9+GHUfHmEgquZJlw8XikDPoDG3SZUN+xLiZVDMFoNK2WbkC1lzuwa3OSDuIeE055kWZnYK+
+ HI7jeZHEQ8Jlxl9G4kpbIWVYeoJ9udp3AGKxRdMxUd2O2HHfjq/QFCOnmX8IZs3TFtXGqbJi93
+ j/ct5/fv2/b7Z6kl8kIzQFf6K5jzG7rghTTPYFi+EpMJ2d7Ai81TpXsbLq056Nr+/SrL+39v/m
+ /7clnH1pvPrbjXtd2aZphu0zZqtXUaUwh07Xs3oGJskJSTBQk1bpdTxd2hqSSebjYLFxca9/Ip
+ GfLuawCNDFp4Q1SGmRCoG61lIEjcUw2AnKuiKItPDmvo5HqghrB+FrABr2D8yV3E/sGePI3wH5
+ 7e0f01ZvXr3Xf6d4Z9AUe6PspKvJ+Vge8F6oJ0ADsObdkyvDJPtmKRYJn4twvmhy1WknlI08cD
+ VtZSWZKUPH9QeZo1u0NBcY0c7FfNGZ5kTfUSSu3fEYXT40Bz3o5ic79umLzDE9PEOC+4F7xbDp
+ WADGQJKd7/Oag4pFqmAQ00TbyrCF5wfAOsOfzvDaCvTV3S6PVseHenu0dH8pamQYxBYoaAO8NX
+ 955W8kAyh/jdK/FLIy4KZPVQ/5YLdX1fXLyxh7hk4Nkd76wGgGvVlMzG/NqkdryLGioi+SipiD
+ QatVtZ29oe7s7tre953N5qelQp8g6bPPEzmt9niD5fv5AUXaDl/5R0ssCuhfZ+hQcNi2Qi0yIY
+ 0P+rkgpCp94s8h7Q1a6hsjRA5QJQSKBLtDSnlPf/nswohh2Uh0mj43rfy/d/6t/lu6zaOUmEY2
+ h3BpgHN1wKCe0kF7yZJPiWIVuVbTfnY61uy3bY7Rft2+DXtd62BfUnKLBfAxNLjedF4hSR2sAf
+ crSBfbOx2fudCmzz8A+tUOnA/FmIgUnuMxCYdVXA4l39wyKrzUeP7xKtOUwp9KQkszkwE+nssI
+ YBM3Dj7RR2Qzt/Mul7R7s2M8ePLC9zsBqmU91opm8jiB9feQweZjz36XuYRm0YS2w9KKx+Gtsj
+ Bcru5gt7OXFlX30u8/s17/6f+zZ5x/ZXnlqD3ot67aph3SsDqBCATAgG9nleKZ9hG65ZDShJnZ
+ XbTSbCexpECuR5c9L0lWPGUm4taXGK/oIoE/IyKVWWXl2nTj9RDvwAEB5SKK5XLmxWqUqXx0Kr
+ gQCqJdBvy+aDksCgBKqiM87PDq2ZrNj52c+N1Z6cnMaiPuc5h24Z9kZa1qVd+Cyb9g8YNzmNgn
+ Xfg9n1rNpZqvbKgNEKQBCxSR6CcoJP/vd3QM7OT3T0Jc0lINVEnTGXAGBBqimghbvPz974/WRR
+ XjR1GoyU/OB5t7VmtwpVSPKVCl5IusNh0ls4OdXlIsaPrBBoE7g1glKMrC3lh1Cy4GezHl7Wx3
+ WyubTpCpAXtSNu1/mX4XO9jT46Baw930luCx1rfDJef70qaS50EyAPaulThsH2YoruxgBqXqVJ
+ ziodRqtmg2HPamc9vd2bLg9VI2O2ofX43K/giLYpwL0euZ+A/7XO1J/jM7+HxPYr0eSLChkAeA
+ GZ/9LwN6jVVK/JtmRMkt+rzmWFVE0ZZQbrbbVOy0ZWxGtNeas1bbdTnS2Mu4M/xqm2cSoNC+6x
+ oXdXG/E9gHT1HTlgoGcL08qnHQj5WspB/uUIxXB3uVk0Z2Wukci0fYbLbKItJoIsHeOXopgAVz
+ qTpWFcixt18F+YYOdgb334IEdDXastoph1Ro0HsvJDbBfe/zE9Eh7IT0yx64u3Bh0jckulMt4s
+ bTL+coevjm3jz//wj761b+zq4efWX8xssNWS52mOH5u0cmIggVHRhq1xhNRC3DUC/nrLG08JwO
+ dWbmEeRmzbMt2eTWyCdLFUtmuxteicniA212fOZvcGNUpGzw8mTZeNOpunc2UgZMxcwbJyvHOb
+ 3fatruzrfOtebIMOr8aKWjglYNGu9vtaR/RvnPGZcvMzFn5xwNiEbzC5wYQRKGjjH/pc3A1+F3
+ 2Ab4klrdMXFMAScFv5vp1tx4oifIiSEJRVasN0V9QWmTE09lYlBavk5pm4c1fKvI2GzadetMXz
+ BHbIyChEGJlQY+Ae9574T1x9AJQfq99TPegF5uSpr6iuhI+9czK1dQX1UDSiMpqo+kSy4MDGcB
+ 12q0Y8O7ySjekcxWbd5AXDWAC2eMZjMbpTN2TQDZhAnBPTefp0yf2vWo1F1bRXGG3b0CVw/fQm
+ zr/JAW+VPfEa4u5wnXr9lu2vzu0/YNdFfm1yqeuUIYqTMvfwKFMFeQmell+X6BJiozJ+t/fIrn
+ METCDyDyxzHiSEG9s5MvZxlLjYEpSlRKsUT0JtvJPWJd7rv89ZfaxikmrmQK1vFkfKGb5njusc
+ 0+bNYxi3aD04Je5N04e9Jz7XZIVoJBheUhTU8f18GTvPYC92baOKBp8aerWiYanCstIFf9SG7D
+ zhcn8KeUMvj1/jSgO1vJprGBS2qTF3I0AUQjRAeDJeKpI4yT9MBthu6n0m16TFDmuxinQOAH2v
+ lTyZXZqCAM0aGYCLNNyvd3v2s/u37c7u/vWQEoXx6SVhW5+v/8TjXPDf0lb0aLdlTvkb6GMoGA
+ 5p9jIg7cyOxnP7enJuX3x2Wf26ft/Y8+++Z0dzke23SH4dnwwCZbGdbpjoRZWKmaSFWO1MGXkI
+ aoOuTXie4PNckmumThzThirdzWyNyenNtacVCR0W6JmlnNyvZXbBaMWuji3C3zwya6XS3Hp8N/
+ 8jSwa0CZTZ7/QtWMJkHxpPDC0rNNpiVYhCGh4OxQPxdq4XllTFOqfGCoC1w1Ic3XGE4LKlTds6
+ UL5dSTLTPp37oNEyagz1bz5Sb49LTzo8aj3ngJx+NWqXVyeaRC3LIlxGl145i0f+CpNWmM35lu
+ 5ksZpo6nA3hUzBBsvGCffGLUhiVoLYNNKyzvLYfc1R1YrPQdNFd3J6inGsmKs16w3GDrQA5rUx
+ 0isQskmvj687lUbiEBTzOxzfrywqkwpX6bGSc2P3hT2/OUze/zwoZ29eaNzy2B5zh89Elw77gl
+ XgkGh5Vp+75xniAojJRu2uzuQ9BX3Unj8Jk2IWvGvZ/gFbM52fbPY+lZZZoaBBdC4kfUnwA5aJ
+ lPmSV2RU0jFAKPkrUjjFJ/iREEVYL4YkRJhvfZ5vr7PEsLgfPOaRQS6QhAoFmWL7mNvU/AUg07
+ p3l/+Ms0RsyXVcTICONpaxY3H8KHp9+UqyISnPhx8oy0PGIp2FLQotHIzK6MIYXtWDC3QKgoAO
+ llkyIJQB8CQmZUweUlNF7GXWaSK7CY9AEWod+D2Idfp9SmTSvyo2yNLK5RJLqUrTkE9lq1O4cQ
+ lkF9p0ETaW7dGdrBHqUGx1mWKjXbTHty/q4HjTVY0MRhBih7faX94U9KTji/NtI7At1p5zr9SA
+ TcoidWWuHPonAlZOQ1Nk7m9Pr20337xhf3mg1/b+LNfW8uW1q/XbIgCqtu2ct3N06yM/0woOVb
+ UABbKgDV5aelZ6xRdOdp4DL/mcw1C0eoFbx26bynyVSp2fn4p8zKoGuDgzcmJGojSeEJexz3Be
+ YLCkSwvdOErqUkcgAAnXyn49xQkoW0yFQwGZEgHA7wUWM5dlQN4Qi3ROERQuL6+lEwy4+mhXEJ
+ KSybP59MnwH7yM/uDPQRS0uHOtl0RKDTa0Au60BNcqDdvXtlihRMo8kr2249NQ1lKJbu68iEtW
+ 2X2E5qImgDF4rwDGSqDoin3AHUZFwt4QGE1pMYxZroC5BTpJTJw+wsVdZPRHQXZat369FjsH0g
+ 91Gy7+6hsQ+I8eUE2VhKpALy2nOeqpUYiEpgAmw0bE1/VuykavQ2vX72SIoexkyQhSGrZFqqcl
+ vyFCKY+opOmQK2NNY7RVzdLzmNlZe123QbbfTs6OLCDvX3bGWyrUY/AkaZ4+TOcLJ43k7oigG/
+ QOr+HDj/jsONjM33QGsCmtPQfGGf/U2mcO//in69YkmNZUK63rYadbadtjFfTA9XqWK/dlh9Nt
+ 96yNpkQU240qcY7IV1tQbfjliqTnKdshRHgDQeJxMz/mAqV3kbtVTa/uVPxVL8NoFQSFBlOuvS
+ b/jsAt5asqcCqaVNBAwnUPQoXwX69CBSLuhtgT1bvR+RgT3GWQRdkmWiePXvbalTtvft37T3kl
+ 6gOxM9zzIkUK9y0cQ7Wb2Pf93TzsdLxRSK/ZNgF1DDFOZ+nysCT8/Hcnrw5s99986198av/y04
+ ff2uV0YU88PvthtXbTas0W7Qr2xJqieBV9mw3VUZcPjmx8XwqUKfDlsEnWC3A+6PgubgcyS1So
+ w41BCRmxpbLonoIHJiXAXaiRYIPp8OVcwTlAhBwfTWsWq3+PikpDejg1qHgqvF+svWFmfZr4h2
+ p7rMjnXppSzUi6MPkQ09hVBkzSUF0+zqNkxuvyb9/7nbEBCt1z7IyQGKJod1WVTNY6f/gdsHtk
+ SDCfeRTuvr5hChbyauHblboSorSruufeDNXFMYpDLtXzzLA3ntHAHsPhBTACQaSComnx6hYZWd
+ cLFUrKtkKS4tu3/YPD22wuyeTOoKI6mFo6BPYh9wzyTqLy/pMHpBZljjYxxLC/xsrjgzsBdQLF
+ a0fPXSw51hqZbdcxjaB5irRabr+dJj71C7p6uPzFABXqKBW1mg1bHdnYHt7O3bv8FiCDqg+7KK
+ LVg6iZzdW9Bsl2w3OfhP83/3zrQXaQuaeF4oLYF9Q4/yxCrR59p4zI2JrNgq3mN6tJb0/ROn8+
+ X/6n6woskph0MOuFE94vOAxO2pYkzZ15JTy8CCLT77aXlvxYqc3YxDd5Qi4jmIB4IFkEX1dkeg
+ 76yDnhcgiQKd9537RQxwt64lDT+9NzVM+TShV86OhKlQ36a5Jpyc7cVk24JdO7Gkm2YoglO4KB
+ nYDXBNsBhwgRO2w5K5t2f17d+znh/esW8X9z8tbOj9pCVcMgllKkU5WGlGeuiNTs5gvvFgaA9j
+ CA/YDHTuWCtcze3F6Yb/7+nP7/Dd/ay9/97FUOsMmDoVNa3Z7Vqk3zUrVrHCp2giXLJrWOJ6r6
+ 0uBvBcB+Xyz8dT1+ag/SuWKrBWcTsC5kdXGQkVfAIsEQI1Eq4VoGaiUEfx7yRUsCowxEhC6joB
+ BNso5xGOFZh288aF5OJ/IIwHNqL4rY1T/gSwW0P3XlYGryU3ySFeCqBCq0X7UK9xJldUDAY3tI
+ 9vcHgx9CHoUbOnqlg2H0XMwkfkeyhrAPs2HxZ+n2er4tjW3eKWCslRK2E/LeM0lpRpsQm1nziq
+ EZiIPQqLnIglR4iBLBBPXL2kn1gNo+ll56R/Ge8T6ijU7PdveP7A9DMd6PQE9AZPVNAVTz+gpy
+ ibfm7xHxME83We+L+mLz88aqbhRbwP75UJD0Z88eihXUa5tVTYOGMJRI4C/R/rr6q1JzMvls5T
+ XK45xTWnOW1ppy6zVYtZwxx4cH9nODmMTt2VbLsouVibZ872Rwa49ywXAu/FsR8a4BokFOeZtY
+ B8sfIbu+Xt5Y5JLZ1vKS4cZkuUB6rZaQ+xSIEJq2AwczPmboHZuqpP8/f56iS0KX4mV0N/CHSD
+ 9WS4B/9l//l+suHkosG53erIN5mZWAQaPk2SgJDdDV7QUJYkiOlxKDumYDb9QPusdFFmK7h22v
+ rPe+OorAgGsbuo4x0LbgkFSyBJ5gAT6yuBSbprLkrTCSMefgk4WXHK5ZfGEpZORADm1cmsZHVR
+ AUQZMJu/KA/eK930ym1dXdnR8aH9x9z0bwEPqTsoHAmdKg2yD+VI6rlwsXX3Zmw1gCAsH8aCU7
+ VTI9S5TNb1Av0wX9u3ZhX3+1ef2wa//gz394rfWGl/ZQa1s22pq61mt2aGLyp0755R8l1aCmql
+ hWGZ2dnYqzpxVHlSevO9H19Ln21ZFkr+T80tlbQQJXofaBk5f4/jKjAJ0T3rAFcqFxjNv12dl4
+ tOe0uxcfp9G/gH2WxX2C4UTBm2MSxzJyIzsP/n4qAEp5sOKpovwrKFdabQk3cQL99/xATU0MG1
+ J88+gk929PQUR9k99E6wCqnVx93yxcmNFy/uZySvb6ak3SMnDHsdLOXpyD4ylgoGd9EY2d9VEm
+ qjkJRrB0nKNQCCvm+iq9tULoMl4x5lWJPxOCizGNAL2pbI1Oj3bPzi2PegbrISZyoa3VK0i7pz
+ zxzGqwziatTxRClDwQlg2/DvrWi1Qo9nfi6+NpkVA+s3rl3K/TDQOiiHu6VqtrPOhsZc04TGIR
+ 0PbfaAKe+FKIgrZvgpmT8AS9PiDfst293bs6HDf9vZ21ZmLm6dow6xwG9q1DNvimQ8ZRZ6Lpec
+ tvTB+DrnkJq/tZ6gAmPpYoVEG9jmWAswbz+ymT00RTLI3bvjXFMDaM/g8NVZH0VoGX8jmtUthF
+ x1Z8GYxtig93QR7/e2//K/+mxXTndwXnnFlXkzy7j+08PnU+nQSihXiYmTBQ2TNnxpQLiYV8eI
+ E9vKpicKpS90SwDv0ejN1WspEds9nxHI9ZXIC7zCMys9xZNSxs/6g+sm87UueZXHyfXVQBPvwz
+ QkzrwT24n8joE225rZ7sGe/fPBz2250rKoBJimhyo8h37+NvCJ2LKdx/JUpCwsDBg920uAnYzF
+ f7j+bLuzpyWv76usv7ZMPfm0vv/ydlc9e236zasNOSwX1SqMlF0k8uWQTDBVHlrpVsTEa/PG1g
+ uxMrpRlqSvIdK9nUzdVQ0et0WuAv7mVgrpLfdUFkMtPf8mgbW/qEtgDfkzOjWAN5yu5nmwbXDf
+ P3wF5uYpOp1IOcaxke7yGVQKFPEBOmW84cwqE0yQkceJurpZ8hhK1A5gi/yMbRs/PIHC/D8u2p
+ SKhe/Yo0WnUte8EP46Lu9J9fchi667IolmMIFVmNXAhmkpDW8LPSd2tUaT1+9TvB2Xe4X+jzlw
+ Fqhg1iX5eXbE+cWqGUVqzJRfLvf0jNZpRkJWOHtqGupqsIJI82kceer0sY0zXQT4D/SB1shvOa
+ UeBXUbz+H4zUJ3JYID96Zs33uwV4z0rAnun9bj2yuLn87B1cHDM7CJinGZSz6HPx/W520OtNZQ
+ 0c3uHBqyOF/qV5UOJyt3PHTLz8c3xKCWw9N4BB8D82cp+1nO/QQIpEdygQt6CDxn+r9krvN1xs
+ hgkPLmM7Rd086rTJCGIMvGbYK/PyfoPwjomUdWbtE2+oXjbxrH91//dX6/gG3mIND80aBluIOe
+ scudIgU8G2L7zxS/d0IWlhVrM1+iKPGomuiYVUPmvblrfzfjYeH1BXZT4+wxGowCcF2LTBQy1Q
+ 4B9klreWLpltwzBJZdqijqJ5bd3ZsaSdOHuhNdB4ygtZrqRTWy4N7S/evCnto/WPoy5slxhXSG
+ VDHzy0/cDYJ/m74rajYYrKfioZaxWdgqtcHVtJ2/O7bvvvrFPPvqNffPph1YbndlurWy77aaks
+ lXsgss1AbSa4Zpo8mn6ccdNAAq9vI9KnNv5xZW9PHkj9RFBgd4KRvopw2dfACdm4/Ke4MirW1W
+ pdZAxyscfLxuUKzFYBjAXDz+ZSSXkTox+jlX8Zpj4aCTwZZYBQI3ih3Pptr7uHgkNogQhwB7qQ
+ +sfgEFmaB5gxItXKsrqkQ4ygJxjZXtqGKs0PPkumWSBUEQMMFGBmQlW4uPd1dItJJyTJohB2Yw
+ JTJzP0JZLNRkrGWXavkYMejOKnmyf8yVeLtRDfMtwEi3RK1Ztd62/u2OHx3esP9hWr4JoG9FeO
+ GZGYpYt/+PJKVAbvrouJg7rIJ9ROiGYuAH2yH6nY3v+/Lk9fvTI5wWoIcrtxVnVEHySuyZ4ke4
+ Jd/p8O9gLqyr0QFBwr1m/37ad7YHt0HHb9wasTrvvg1ckAqGIG+s51bQ2H6qEHflTvpZ83qLI+
+ dFgHx+t81NYYQSGJ1S8UWPwP6w3iRW5d09qk09XjPLMcDDBQ2IvNsE+iyIZjhSz/RsKnf/2X/3
+ rlUUp6N0AACAASURBVJwllX3FSSoCaAF6PU/f2PnigSt78dcoR0CpUrQ41b6lz8i9QQB+LdEjU
+ KwbCaQtpozDPzv523hB16WOmQKoEIHWClRvi9phAZpp65UZSSsUcsvIvjiuhTfmMOpPhc5w9rx
+ Yjqw37Nk/e+/P7M5g1+rsUwRHrU9u3Gg/NrP3pbAfh98MUjKlDF+86MpGzDplotTF2F6fX9h3T
+ x7Zxx/9xh5/+qFVLl7bQb1sPdRVzbbVWz2BCU9qtYkdMs1z+BP5wGx4dJqtKLadXl7aq5MTGYr
+ Jkti29De6dQH8s3Oy2mTN69pxPgetPftIRk53K8DJEZCRkvmpOLtY2tXFpTK/VtunHCHlZIWhg
+ FCpqIgKqAP+auIq42IJhcH7U2drJBUqxuK5X9O1wcRLZ628JRAH0XG79CKvZ/UqGJddPQS9c+f
+ 4WMEJcFO4Z2iL/Jq88MsxAbKSWco/BzsIpycJMgQi7ReBSFOpwlwsMmZ/NhIV6Zk8r/MWk5LN3
+ OvS6p2e7Rwc287hgcYLIqfluKsKmPEPqlID3je420Jmm8A+dcc6RZ/fez8E9uwY5/7Z0yf25Pv
+ vbUwQjtqDizRcBcd1pcDq9xDZvds6p5WVW04QoJO6LYBFg1RkiCtKiHoPfvlk9yjKtrEDabWk9
+ oJ90BQsTQrzRFRNiBldVVA4pKy9GA9uy+6zZyuDtrVU069KonVSArmW4m4A/Ga2nyFmisR5ll+
+ o5vmuhQ5QzMKNMOSOv7dQOGsA/46ibem//5/+rRYTAO0ysqvUmLQJUImDz4fqrkes4tQ/zxI9+
+ yoqZzwDd1BI2Xi6eElrm92LQZZrYba2/AxHSl10l3ilgFG8DEXeyumkt+X17kiZTi8g4NYGhZW
+ JgpZTJ4AgnL1onKCUrpbX1uw07a/e+1N7sHdkLXxlCqqjPwzs8/PlD6ubnvkoQD83YywSpKSZ2
+ 8VobK/OL+zRkyeidJ5++Vuzkxe2V69YX+PkulZptSTLLFVSkYiVHW3snq0D+Ew+kkqHISgT/we
+ FhFUErwVMT88vFPzoQKXGQ+DlgeczAF5WCThMAqTqDlU27I6WAAJFWa5dq91U0NDQkrn3L2hkX
+ 73uFhU0Q0VAr8bkKBXHdf49DUnLeIIEPPv4mm7bsnj1o8MjOzk5kVySz+crSUBJL+Hu0ZMfHRx
+ qey9ePHMaM/osKOAyNNwDlRdD2QYZfyXsTFXP4v7WdXHZrJAxVrgq5EeiDRWlVYcyfDp9faTga
+ qtmjXbXdg6O7ODojvXojm3Ru4C2f0vDYgB7qc8kkMg5Xwf0wn0ewoYiR589R4o6hSfiLZk9NQ9
+ mDX///RMFQFxOkzRZBWFyhrBq1iBzzR/w4jPX1yeTeSdzEeyFCQnUZA3hFDCBtdmoyi+p1a5rK
+ la301T3NcOMEJJQe8kCS9QOMsBL+WpQW3lgK4BnIUEtUjsFscumpCJSXA/Vby0Qr31ujkRFCqe
+ Y5fsiIWdOXDF4M1hkxxYuq3lJIY9kN7J4BY88CRD+/PW/+T/ydUkqZERm77ubInH+35Rp5gcR/
+ LhWGb6B1FKfNMf8LhXYiooEZViFSK2TKWYk+P6gSfzeXOfi/H1eX8honBSgNyJcMQjc9r1XCdK
+ J5kAisxd971QJ6iH2AZAf4wWSwH65tMvl2Cr1LfvFvZ/Zn915YJ1aPTpgYwmf6hFp45vT3QUqe
+ Ut9rohzmigFx7gknuGDHY4fNrWyLJelwR9P7OJqbCcX1/b1t9/YV7/7yL75+DfWmlzZsLKyXgO
+ 76Z7V2u1snmpp6UU+AAseHpM3gJtsAkrn9OzCTi8uNSUL/hTAh9O+kBxyoWY7HnrOCWAL169sd
+ 2vLwT5scMkCAXsfpu2rJJReZKiyLMZBEYsCAkKMz0smYhocDrrERLJUhBSIRiaE6oN7gms0n7k
+ 3DUXNvd1de/nyVeaBn7gNp2bqPie2VLLhcKAuX+ge7950Ezv2TwE2ZKveM8J9zrSvGNISAOrZo
+ N9PWqxGYE4jGT1geze2CsALqDlOVsXag6HtHR7bwdFdzVwG6DOFylbJtqo+uUrPUzhVet0rB/q
+ 1zD1cS28UaItAHxh2G2dPYKQo+/2Tx3aCZ1CsitL8BVRUsq4OK2vAnkMuJno+IyHqEQS1oNAyB
+ E1BUroMD2aooarVktXqgH9Fnbr9XtcGA5o6e9Ztd0W3eb3Fm7KEPQU/AE+wCque27Llwt89CX0
+ HUmQF3AxkflRWv0bjbHD2azROBO5ipu5bClyKYfeOB3FsxcPb5PA3s/y//p//9wD79SVK2mBWI
+ AxZW77xQlafZev5ll1zTAMLkjsH7kwettHV50vqgtSQTIgdp8AVX74fenIC/FKQiKlHHk2yiFy
+ 0Mf4hoPfTWShu6aQ52Kdlr7e454HMO2hRHbiC4mJxbcvSwv7s+IH98r0/tT4Oj/GQ83GuHil8b
+ YB9ptlNFhUZz3oT7PWnQqFWmdOybLPSykZLplNN7Go8s8uriT17dWLPnj21L3/3sT3/6lOzs5f
+ W31rasNuWcqZKd2ytDn6GOdWWzRgnGDSK21RsyUfn5OxclgLYJ0DlnJ1dyE+HtTymXJzF84uLa
+ DSLKFQuaaIUNA4FX6/rmAqa3BeaIVutyLuHoiiZqgZiFOa2ygxOmT6ZH3743mWrwdkJ5KUw8mt
+ IsJnS8DZ1ugarZHTceN6ow1UJhIMtK9patel0DPRTva5h5NAw7siaKFpX9vC61RwJoS/vHWR9v
+ GN2e/pNk3fJxlAVH8eYK8k4LhqQJitGc9as3RvYwZ07dnDoHD0rkjRUBnpIz7qy+qCGxKzF3IQ
+ A7KQgy56bDOwji98o0OYP2G0F2qVqFs+fP7XvnzxRAJTEOvlLEcxCgce18C7emk/niv4TX3n5K
+ j9l9hnY81nYHruZUdwbWCx4z4ysVhCLlFbyysd0rdtt2XDQVSEXiq/dGsh+GwWPXFrXnjGn6jK
+ SvaBQSjhWIOCDcfj/H+wTmOd78uPAfvN9m0Gj9Nf/5n9bqUBHIa0gRUqZ8m1gn6KKWwIkPrkQF
+ TWQ2jtNPbN30OZiJMDPKZsYEJ7BuLe763lJqpu4XGw3DQKRtj4y+xsDSorRcO3iv/1CCiRELUV
+ zU9A4vtBIdIEfq4zQMrB35cHFfGyT+cR+fnhs//zPfmE77a7a3qEaUqNIwgPfi2KhJ+WC+erFe
+ xDSA1pQKhVoyUThOAdcstnKbMLgDHXY4ocztrOzkZ2cXtizly/sy88/lCzTTp7Zfr1q/VbT6mj
+ K222rS+vt/DY6eiSXomPU1FS10Xhir09P1VzV6w+Vu56rCWoqjlkyzvKWd+Gqk9KDoe8bJmnuz
+ 6NJS7Gs56FnOc79AdjzhXyW9/jAbb9nNEMAGihNhpozmSoG2KsRj3uFeywyXj5DZnR+R+/s7Ip
+ LPz050ecqCIclHe+lA5b7OHWiXo2uNDDbG8DCllhLKNf/q/s4iqqSO0qSl4psMf0swF73sFbHL
+ qlMRX+XhSKxNFvVOtYb7tjhnWPbPTiyfn+oHgKKsUgc8YiPeTqaQaCHWM0nXgNDyVT8upHZBx3
+ qSYL/3xqdE6tmT6dckeMrR2i2S2X1z5891cqLM+pCivhf0C+AvWgdsmxqO0HfpGeY+MrnSW8fN
+ BdyYomvE9jLFy2sVSLJyPoGNAyI5KBk7U7d+v2WdfsdG/SGSlpo6oPXp17jEmxf9atYHxmuDv3
+ W7D7O3trfnK5ZDx5+frKv+Nws0Mer85/TK4uJcY4CwYdkBdos4qXwtPb5kSWltUoErhuAXqRuA
+ gcz2uev/zVg75+eGqRyQzRvvFnHyyg2RoErLaX5b3Ya1sAeva2fOLddpRicN2ZlGUhO1MfDSDH
+ Ub+Ii5+/ZW64UyptIYpBydn79Mm1erOx+T9c3/iuwV4t3BsXRMJWveBxcnX+EH5etq7L7mTLqq
+ 8nYDnZ27F/+5V/ZYX9oDbh/zRz10YdO4a6URfv5Wgf84r6lhrOUxa8Vmgv3WxJb+LByDNMA/JV
+ dR0fs6Pzazi8n9vpqZE+ePbEvf/ehPf3kfetcX9qwVlZhtIlradv18RijcUP4RCX2GbjHhIsGq
+ rE6eeuNtrTp6Ojh0smCXa655dpwTNsmMzs/v5D7o4qIDDNHxom9ARbHqV8iiqxkfg5eJWXskuu
+ JMnCNPhy+K6oIvKlW4iDvAOFNPsmJk4eeOQpp2Mjo8lKSSxxANQWNfYr5BKmjVfN1KyiHpuoN4
+ GEkOCA99I5YL7z6/egrLjj9ahjYUfPye85nEEuVw3BxPQ/sc1oNmFZPuOCXaw0bHty3ozt37ej
+ oUEN+pGwB6LecInFfHb9nEj0UuBVy1mQ+5vdUEewzWuMGbROr1qBUsufKYV4BlCI5lhFPHj3yV
+ RHzeDU+02c5+4rckxnouiTb1opHuYqrplJ9yTua83/8PrQ1ofwLHCoMdikayDmIun8Q16nWKFt
+ /UNOwFOYsDJje1u3Lr6tWa7qNC/c0iUw2FtHpNQX7MGvzzv1EQUeSFffiuppnA+xz8jwrum7AT
+ xZoMjRaiwQ5P+/nbJ2aWePhk0CDcx3Pf0pyi+FjHfDitklBI4G9FjtR4V4z+1F2VODKgy9Kmbl
+ AKMmhkgonHlQNWUhUDI030gTntqupocCX1bFjkYUUaZxNrj7b5trYwWj6iiNPpE8xNqfmr1xVl
+ PP0sjQOmSkf4WoXfyD0gIUTH8U7Vxp4dk/GKV34amWXtNoPevYv//KXdm9n11pIxTh+QDNE9wn
+ skyna2iCJYlR9W0NAvGZN0QrYLOHaTe6YMwaYLOfi3WmMuhxP7Ww0tdfnl/b86ff21Ufv28uvP
+ rH69MqGjbLVKyXNLGU0n4Zfr8iuoeYBG1+xAcbMscV5sbRVFfWTrb5CwYOXDvwzmT6cPtOsOD+
+ AQK1BI9Lczi+v7PzqUqsHziyZsvdNeD6ZgloCexcLmDcd4UNDlhvdqgJP5I6yXXCdP+CixqYag
+ 7G7ktNyjsej6/DPcYARmIsy8q5h3k9myL5Kp69tTny7BaBPHb1JVUZQrsjPiAJrMrAOwI0OWFk
+ 5xCwEwv3cyjazslVoZNw/tON7P7ed3V3r93s+WFwJUdy5QQGmVCojJSKLcTD9MWC/IVDIjM9y8
+ YPPoHWKi/OKCufF82f29MkjuZSmonMCewfiUCJJGZRcNvOQ5IyWZ/O3gn2AcLruemcG9jCEuTN
+ kXoRNxd2V1RrMKnBOn2Iuks1Br695uFi9oGKCptNQ9ixg5imgy479Hl+v+wU8b3D4t3L6G41Qi
+ WPfLJrm3H2Gduucf6xEbyvA+gtRH90O9nmQuVl0yM4tNE4Ca5czbWTEhaVMEfiKJyblMykzTU0
+ U3g6eWtY9+nukjWJgAChgrigfQF9MY9gb7+BOHm9FNU9+gVKhtxh4sgJyAsgA7htgnwJYanBIU
+ 3o09SgPF849JpWBSbFCFqgmq+XSLq7HUhH8i1/8hf384Mja8JeRvXvUjmJfwQHzx4N9Xq/Q2ci
+ CQawXBPRuo4AOH3CZLWZ2MRvbFSuPMZOgJnZ2MbInT57YZ7/9O3vz8EurXb2xPmAvTpR+i4Z0z
+ Z5F+zYp1Mo9U/Nxo4wnGaZ3jvFwwj2jv0fBw3/xDpqgUx+PBeLQElBDDDuXu2YM3Oacyo2TSBV
+ KBxVBw4aC68qKcIJ18HIuHhtqgHOesj7JRRkkI3tjttXw7E/dtBiVpY92kkLzcKMjVjLTGKlHM
+ 5WPrHEqA75a83VFRS3UAeqgm6S0/kSUlyLsvFAbJn8aNKIRi1HU1bVZ2BwqEvuR7rbtHNxVRk/
+ nKOdH5nDsN8BZSdc7BcD8KSs+zn8w2Mc974N7wvpDdYiVfOyffv/EXjz9XoV0p29dLKBLHydWM
+ BRgn8QXawlg6lPZyOrXViBRYBY9GwmkJnkJ7Ncz1IxajutEYMRIr16vWKuNXr9jw37Xuj0mq/U
+ 1QKlBpk9NYYuaAmt4t3ZZcihppR0rjTyBzZOPHJPys58lwW8B+ww7srf8QBPWHxvs/4f/5d+66
+ DCGHBcz9WKimTJbXz6m5ivn0NN1EqeZ5F7R9p1AOK0EdAFDK+ug5ePetPxOGUpsw28Yzx7SzVP
+ cdhaYUoBKHvV619u/1sE+H0AeNmqx7MRwLIIQDyjHFV2bCk48GlE4xDIAr/mr8VjZ8C/+9Of2i
+ 3sPrFvD7DhWRd43pC+WYT85sxcxvX5MyaBNi0vAJNrsfcpVSdTK1XJm10s6Vec2uca6eGYnVxN
+ 7+Pgb++KT9+3V5x9Zaz62XnmmZW+lUdNUsWadfYfOwZvdHReXZEfopKe+ssFy2RifV3XHQmgcA
+ g0UF5w9zTe4Up6dn3oTFXYIs6VcVRnSTjIAaOOjPw4fHA3eVmbNMXjh010rIT2W0u/Lyhh/Fmn
+ 1F6oTLGY+lQsveKgpecywasB7X9ytd1ALkufB9yuzh4P3GoASixi+wT2npi3dg049qP1f+M6nJ
+ F6A6xrRJLznoW180A2yFO834dMntrRZrWrN4Y7defBndnT8QJJCZvCq+Uo1KB82noaS+xUvrKz
+ 1tOaSyT8U7D37hG70Tug0XofVyNnJiT16/NBev3yhQIf3DbLSZBTuGT6rgKB0khQ6wweHu9uy+
+ 5TgpTvaV6ohUwibBFF6YW7oqffGAxAUr7/cuf5KtWSNZkVNWu1OzXa2GabUkWyz2+pau0FjWtO
+ qdOZSk1JO49fPTds2LE42FT0JWYrKnLeCvS8l1piboj4oVq3ZU/0TwT4LvgUVTpaaFjKCDCf/x
+ /8V6WVwpWnjG4WJzSJAen1226Xu0kxPHNyWilfR9ISED0BPFzDIcV9+Fx4mjwCFBikPRFnmUAg
+ 0m2Cf2RWv5eM3Qf/tYO8Zs/aHIJRWHAKeVDuIJXpYJnvHJ26RC7vCz3s5s58/uGd/+fM/sZ1m2
+ 7A3S0WxWEWuP7633MD+gpvrx/SbdQonZfkoIPxcyinx/yXuPZsky44rQY/MDB0ZKUp1VZcWXa0
+ BEIJGAkMCHFBzd83WbH/eDmdnOTskR3BJkADVztc1I4cC7O7q7tJaplYhM2LtnON+331R1Q10A
+ 2ssslDVWZkRL96791z348ePgy6A/n4ypn89omrY/PaHU9sfTGxre8ceP3lgd69fsxd3b9h045H
+ VQbegAFZdsEXY58KzHhQRiu0IgeCyjwIbNOng6/Gv6J5kJjDPCVhwyoShGhquUGAEMMNQTNHYn
+ A3GEw7DZloNUByPbXNnh26TDAQ8cgaVQb6cQ761GWOOKnznoXtHMRD1EtBo7GmgUmTKDlPQQ7Q
+ LFovjndyiF8glc+6rsjMYkHFIiIMHXktrS+tV0kndW7xmQE5ko/qaW1jgMGFUr+9HkQM++Ifw9
+ Kk1rH3ihJ06f4nSyqXOkrzoMTUQdEiqm4n6CGohgDCtn1JT1M9I40Q261LQcBpFtrX2/IU9fHj
+ PdjGi0q1NlHFJhkomwOnfhCFOwQQV47eP35/qNO4nlQN+8RkzHtu9fvjaM6ZfAfzi/R3s3b8LN
+ SJ29lbhoV+3Nlxg0ay11LUVUDzwAGvBNRTWzHXNJIgZ1F6L4JlekqAHjOYo6jXBzwX7wJ9XRfU
+ zX/uSYJ+OlJcgI6go//M//OlfTNPJHDKzzwH7HPgFgCrAhLxKtdjCcTIm5WCjUpniYwDxF/2cG
+ q8ickoLwDmjKEJITy+//FDh5A/DtyrvrLb0y9xVEUVkEUKkpv4zUUwicIaXiyt/GJUj8vQJUvg
+ MAfb9wdh66BQdDezUayfs3SuX7fjistXw+n5QBI2TDzHhMPKXz6PPB/tysOcg5QoIyANRYEVUz
+ ulGU+nmDzU+EYXB8QCOlENSKk/X1uz2rU/syacfWm9jzerjnnUX5qzVrHIoRa3RoDXyZIzPAbo
+ G9BAGoAggAejICBDVsykIdA9b/hFRQ2I5YKMUbQamZjv7fabNUPhEQXVzZ5sNW5xjS4/3Q+qsA
+ aKkYTiXT+CCiDk86mm85bQa1gmbscYYjSiLhLkKwB7XrSwME7pCASE1jHd0evMc1xoj5wmLZSq
+ ziIYBgIcnk9Qs8RsHAD4t1gUyB90jgj1tivG/C7bQWrTlE6fsxPkLdur0WY7mq0JpQ6oiHqgXC
+ pMooRzJqg8p6E4nlHhI+fe5rPLVBdpXc/Z6TVFGMRULh9zBwb49efzYnjx+yFGEkXnwUKWqCEB
+ VWBYouBalF4dyGhyEV/dO82hCLO6frj0YXIGsNoSCPN/NLwGqvmcKtYOT8cje1CviSQBm4S6gS
+ xqTtdCN3STFs7zcsW530RYxjKm1RF8f2mYwM5Gc1xON8pSv2Sg/4WUUdz8L2F/99fJhwhNNzyI
+ +f/b3V3H2EdkXr57dvGBH8j//4Ac/SmDPm+twGa6L4i9dlZM04EUUFKc1fkwLWykdojGkSbMSy
+ 2hzxvdHRI+UkdI57iev1nu3UJ4iQoYXYP+qRqoE5n6zZpTtqfGkHNnrBmnmrJuLgQ7xolpELGw
+ a8QVBkOCGhmvimJI0eOUMxiPbHxxwBN+7V9+w11ePWH3q3igY2uERGcA+bdEvAfZpc2SHBEedh
+ FcOngU9dNDrALCf2gDdjGiYgnc7DoQBjM3GttMf2PP1Dbt355bdufaBHTy+Z/XRvnVgUEV7BRV
+ t58l1wgANAAY7BXjjIOqGVwumYRllhGgMkmsmbibkhq6koXOm2c4uCqVDm2JoyXyV6pgdjEP0A
+ uwhZuDOmTWo3AJ/LVABJ4+FyA7mMdQyC1at1zUVyoei4GAhxeOKmbDfCHrmMOnhM0dW0nHOzng
+ 5XgYUMIbQ96khTPJbh6FMZYLvlQcMe/ddjQSo54ELl8zFZTt66qydPnvJjh07YYuYFUx7Eqx1q
+ Iy0YOm+FJGqA3BEvDnNEW3TKRqO7PDLgD21+lz5ykQ4SwCDYrbtwf179vz5M2ZJqCOAxiEFGeu
+ Y8lavJThXoWjfQd97S0TjiCIJf3uCfeadVWTOxaLWYTFLq3BHpm8i2PthQah0Q7GwHhD+IEuaq
+ kGrVbX2Yp16fQy1P7FylLM7UJxHk1a1WteAdsfBUtD48wD77PJ/3mAfWJXAP+jt+PP/+vO/nIb
+ bZFZHiDjXF2EiEATm7oqXdPCuXIkHqao8NjNsByQ/xMPkJKnwnPejUz7XLqVzbS9eB0ClgkCRK
+ sqLA0VeRflqT4+NWpxvftCXInzx7IpEdCRl56F7PzPddyBXZB8yUyNVAYUHFg++h30E3jhGr5w
+ enCFHtj/q2fLSor195YqdPX7CVPJTpKECdHk84ZeJ7AUMBWeM/5YLPsAJAw3j0NRwFTRcQUWDA
+ wAzZqEaAUCOhlLYbB/07fnWtj24d8tuf/jPtnX3htWHu7baqFoHzSrYAI0m/8QzxcMMhQmBwrX
+ 5qF/Sb957FViDAfcNf3c2O8FPBw1b8t6B9QIiyN09TIqS9QCCiwase+keCTml3gvFUsbccMWEE
+ yUjOGVZEgLI0wffSz17UDGhqnIQ43NntB8GYXIR5T31BiTReNDjq9AtJVDo2gswol87MlMWrTC
+ chKeRVFGoD+B+La/aiXMX7OTpc3bi6AlOeKsyAhU1hP+jfgmfxfs5wpVN9XFXhfnaYczpUXIRf
+ pYje9XQPudrjo4O00VkT7CXbQS6ZTmZamPN3TpFdagj2O9B8jZP2OjfU1iYxN0qMnYdpaKDcA/
+ 8Wmc8tBLYOrUsSiuz+HVkCvmyaii6OCctMksWWIar+YwDl8DrNzA/t2VHV9u2vLxM6SYOYTTfw
+ YBtfg7WIar1qB4VHzo3IytonBKXkEl2ZtU7pci9RBP9hMg+Am43/tMyfpm/yJmX2b9X/uiHf80
+ gPtIWnQ7ZgvZTmWstCfn177HRUoSe6enZTRfA4IWPGBuYir3cI9owHOnmBlHMEAAU0Tjiskjxt
+ IWeF6Av4PP/jQtPPcFZGpxFJMXnKPJF31ep45cFeixuHi5ovIEPiLxjcC9kB4EB1mgYGtqgN6I
+ R2cGob812065evmgXXj9jLfwcG3BmikvkdrII5bNZp/LD+Iz/AmVDZQgjer910Hf7hsJkK1A6j
+ MiJSQB8UCITetLv9gf2YnvXbt26bbev/Ytt3fnY2qN9W67NWbPWoE1Ctd6g4gH3BL9QsMUkFQ4
+ 04edx2gNFTgp1FM2xkYpNSnVeI6Ztkde2Cn3gt7a2eR9ZSIUplnvo8CDnlKshAVcHttadFrIib
+ 1kJu1wWWYt7svB+cKSgLJFTZO4RWkSoiPix1g4ZmYtaBA3DKBfgh+IvH1+0HepzacYsR9TYFAN
+ eSJ1NbAiQ6CxZ98RJe+3sBXsN1geYt1qvG0Q2GD+o5ipkR1isoI1UG2V0n9sJBK7mYJ88dmbWd
+ 77GcsBPiDtD5UTEnSJ1fSZM53r6+Ik9vH/PDnp7UtCJf01AEaBVph2UJeRNjvmqT9F8ZlGenml
+ xKpRWeABW0WFf9Nn4YkgKQkclp4SKfiDu2aj1ueKQs4SxtjtzrtOHcmeRBXPaMbTaHMgDS3AwF
+ D7Thco0Hie+hoqSm3/SHOg900iSy2THkHP1eQHXD7NZAA5Q9ol7IT5RJpOHrQUFpB8phbRW+aO
+ //FsFws5PEzZdWx8AGBxl/DD/PVn+KipXYVCAE1awAfbxxsG1R0DASMr90CnrC/MxXwyVzCGP1
+ BCbsbCBfSqPH+a64dnAD4+kc7omVlDw5+lmZEUg0B2iBLRx1XyjOkEUmiVPk4c8pylx4MbABr2
+ h9aDKGQ9soV61yxcu2JVz562Dxg4qPGa4eV50obCJbOSnQvZXfBNAPZotUEDDswh3TNoVg3qBP
+ JJWFIoYYb5Ftc14TBfPvcHIXmxu2f0HD+zWtR/b1t1b1tjftJX6gtVr8zaP6VK1uqaWQVYKlUw
+ F9grKmtQOL8te0W16H+rYx7BUrnL8JSJHXBta6/FvW1tb7pwp4g0/xkYi3B5X7NB0jFGzmprSW
+ qTNcrhfBg8vzX1wmtG6H3xxyWfIIIkETeP4zZNQHa/snZ7qsBEdAA2+3p8FW1gq6IN62QAAIAB
+ JREFUY3+hvwBfm6va0OatvrRix89etNOXLtnRYyet3WxTO0+xKg4W0D7UnftaIzfiQOy9AhFMF
+ RgYcbgAuRS9J5okWxivAnuPiYoo1emVDOyxH2EX8eDBfXvy6CFVUJGRB5cdGXLw9AXH7Bp5Bqk
+ hmcyimLBNyCgcbd38igqaSMyQfh77T/LlDOwZcczKI8McUPcr7wcK2iTRJ1BV1Q5pMtdp1q3Tb
+ dtKt22rK8tU8XTbi+w/wQwEUJgwzFPNTtgQxfvy6VTE+C9F2dEUl4O501/60k8Ae6AIDlM/V/K
+ mqoRvpayiHEFW/tjBPiokijTi7FC6ReijOiAdTYpIvMiltNetAXBosIFqQQDp7xccO28+02jZB
+ SclDmRpCuW8+KUHKTmYTil6lbjxGVvnFdyVfkUkoUCzvIjInkdhLQq5kUp5USZ8O6LIxOzBo8h
+ ko+xgz0EbmLnpDosoMg4Oh1iZdu7sGXvrwiVbgoQRNgSz3PzPGeyDQtPeUTFcEb6a6QHyACQ1E
+ Dm9jMNtrAgfaiKAGoq2azt79vDJM3t464Zt3vnUbPuZNW1odQzPBp01rylmKN5Oqgs28Zb0MCz
+ DnWeHPygd0EUcGwhtO8b/yQIXv6Hnx5/wXAk9Pp4P6yfePQTwgQYftYY4mFCk9a3MZx90WqwlL
+ YiCesR7hzGfGvjKC+ewIgDnJlLqkCqGUZPCl1H0VaOdDoMxgBvAOzi0Ic6I5qLVl4/byYuX7fS
+ ZC3b06HHrtFvJInl6iBm4yFDGlDLKSuEwDcpRsoCvxyouumELzv7nD/bpToGyGo9ta2Pd7ty5Y
+ +svnvI+cLRiGI0prHINT1lHwEfmRdoErBlgJ7xwsOe99d0bDrF+HvmDCEJGAB9gX+K6PWZKIJB
+ nKXE9jlsJHP3QoE3D/CF197ClwOSsZrtqSwT8jq0sd215aVn2yo221aotHzPpnjsvKYT8eoNyC
+ tVU0C0Z558OAscB4WSBr/lB4QCoTPYLgH3gZjoI/stf/e20VL2P9e4An0cYcZPzSnraDA4uUXQ
+ JMCYV4hao3FDkYZ0G8LmUSgimNgbHzyzBW9JB+QYD52CP16P1LMe7zVbBC9yXR3vx34kzLJ8N6
+ b8Y7ZGecG42OfEJGMjfM2JxGaZfJ5QhpHEGIzo99icjKkZeP3nC3rl0xY4sLlpVI199mwQOuZL
+ Br/FnjexVO9SLST4KHl98L/4JkT8icAKjd5HEPUEwPhqpKA0J5O7BgIXbF2tr9vDuDfrpDJ49t
+ OZh31owI0MrP37X3WrWo3xJHDWNQHxijKSTzQAQfB4DSNGlimulQmtiY8yHdc6e8sbYjF40ZH2
+ BviqYjyDAlThGYDjigVVIeEnfeJRRqG5clsrMrSx548RHcLqJNkSQ4emh0388SLzTlvdtTuoeN
+ tYdVm2+3bHlU2fs5IU37NS5C3YEU6Ugq2SEoMInaD944E/olgrhKo+QZEmh+myAfQ708Vw9Q4z
+ DLouIZ/dwRP6xf0phTwCiFotkh/z7hFkqfHDu3rltO1ubbk0gsE8RPPera3iyYCsi/RxUc/olw
+ D7WXUx5i02oW54TP/qXAPm81+YlICMN4d+f3ZeUfUSUH+o7HgB4bW+cjJkaC8a5AS0H/ZWVri0
+ vLnIoOvx36FlEUUKVc5nJcmSd97yGFK0XFE2owErXnal5CqDPfHTymCVoywzsia0FgjnNHqdnm
+ cLhvfsvf/3fubJjsdALx3XD0eQUyhzNzlSUz8VPeYTriakAATVQvJaaWVAUAe0yz4UVkTk5Vn6
+ v32woRQj2vqkPp2zgiMMib8bSaDfwiB5h+MbOF3we1ROEvCjEYlzmpomPwCIMOwClJRc9qWvnZ
+ /QimUBMX9OhpKHS4OrRtARXx/4U1M7Qjq+u2jtvvGEnl1clv/S6RQHqWhVJSDGToXzGmfSZX4Z
+ dQvwSNwqyIMBeXDJ09+DXRYVECq/ofzyek6SS81/79LHZh9Yahdv79+zuh/9s+49uW2O4Z4sNH
+ 9CNYdsLNdos1JoNb6CJQqYO+TgoIyggEGAwtRfCJZNEAxQi+FGKainJTWtBRVfSOOGPjsMMP8P
+ vCb4+Zpx601/QG2F1kXxtIrIXOkT3KIGPX8r2A+kp8f2gFUHdjXANcxUbYmA5ZK6dk3byzFk7f
+ +mqnT5/gQPN2zWM+MT3YzA9OoTHdojaA+goWjCMbczLkGuqagWzYJ8FCP7v3IGeXfjJnuoYpcX
+ hmW3Oqc/SJS8dCFPZVj96+IDPvLe/L+98qHBi3GH+JjNrNoA1/en8fUrvoygbrIDvK2JaFuW/a
+ pELPx0yZ6iK+Ld0t/L6XMZdv3QIsUIiZ1QCNgoqTtEAr1CfgunaYrtuy4j2Ya+8tGSLbRRx4b/
+ TsLn5umoa9JEKSioifJ/Zka4hvp4Bcfw1sSZlv5yUoH4G2Kcf9/tTejyz9YP/+tf/DyN73n/qj
+ CNN1UmfTqJoY3alhzavilnxIcljk8t0jaw/nJhni9cK/wzNLVUqS5e6eTSeQOmitB1/SvMcdqe
+ +4JKO9+XIfja6KXH2np4n2sgjmjh8AuwV9RWLKhqrCh7fY3QHew7DxoxW6OwR4U9G1h/17cjSs
+ r1z5Q07feQYm5U4jahUpJ3NP78ovJe/H2CfzNNCHeXZlmaaSo2DA4A68Ghk04Nn4w8mD4rq6ds
+ IgD8c2sFwaps7B3b37m27d/0D23pww6q9HWvaxGoYGg0fmkad1sByPFRHZlhjKGpUZsTh397/O
+ fQBItTOI6qnVbT08SzYe+aETEScvLtEepd2OGdCFcWDIIzVQkmQbfJIiYOzT3lWkgMKQJOkUb3
+ TLoWEykYgjyIsMoshVE3Q7jea1j5y3I5d+pqdv3DBTp98XUBfr1kVE+enh7R23uvBHbRvw37PM
+ xitb6p4UJx15RlphZB46swsssGIeJlhZpx9VrT+iWCfwKi8dgAYGgSj+glUOE+fPGFXMi2Gvbs
+ 3UQ2kej2rnuHb870TxdpZsM+ZgfLY0jJ/nyjZGSDLo/USpRN3K+oVWZKQaCWXhlJuzRfSwc7P5
+ sNBCPzeDYyRyrWqWbMxb90u5JqLtoIoPyL9Orx3aobBLagl6nCNwmkxMDfuHfPdEmfvQR9Ru8z
+ Zlw7qnwD2vEWz92mm0Fv5b3/z3x3si0YRxjYeiSawzxqKcuomp0cAJnKqFO+fdM6RIrkkEx+WY
+ MMNjsKdDJSgSUbEE9OtKgCw6MLN/HTiwZEXThPopTDJf4mC1ROPwmxce3yuKBrjQFJ3S2ykLFL
+ ODzCPVmXuBI5TXLfAfiCwHw5spduxty5dsXPHT1iDhmioZeTLN7jjnw3k9dNoyS9H9nFIIZrHP
+ 0n/IYkmwd55bKmelOzAWwdUD+WLPi1qNJ5abzixFzu79uTZI7t57ce2dvtjq2yvWXtuak34mKf
+ BFSqc08qaI+qgVHenGUQ+YfELhc1opOkrIDHGxfAP2QeLxlPDnfPknjVGfwPtgXkguOLIVTk68
+ MshZ7659O86sOPAY9Gd3bH8qlNf4sahnJnHs2ZjlYC+h07iZseOXrhs59942y5dec9OHD1qK7D
+ YRTcmMwEcUvAj2qHHDObUDoY9g/JHpmgFf5dH9qEGCo4+4dVnqXG+MNiXFTkqMob98NjW1l7YX
+ fD1a2vM4OW86RFqUuNkNE4pgCnufHD3JRqH8WMRHPLwjeXvny/fwYVU+dVprxRCBb3kZ6P305R
+ Dq7w4m1xzPVOPIDFN/fJgRT+DgwBT16ZWb2BWbt06nYYtdju22l1mEbeNsYnonYDaakGjGSOrF
+ a2TUzPKKhMOlzj7srRUNVK/QT8F2OdYPft3/jcje093GU2RtlDBNdrAk/eEv682SUgmi+OT82Z
+ 9EcsQSapXyvA4Tg03QSkPIziOKtPgZqSK8KkoRs2BxilalmWKhJPTny/MjDLO/iXOMqIVTxHxI
+ fnpZk/7NNJQ1xDePrxy54b52tSMy0bZ2zSIkKA96Ho5wKjCoQ0Ph9YbDdm08ebFS3bxtdc1ojA
+ D+1D7v1Rd/kK4X94AZbD3+0jppdoVwPLgd2jwQdmQA3c1lKwDBEK6B0aAw+ELr5v9ATzyR/boy
+ UP79KN/sIfX/skqOxvWNRPg1yDJhIa5atV6zerQk8NGwW0lYnWHcyKKtrKHRscsNhQOhmLCmUb
+ 4uY2Gc1+x7hjFU0bqzW1cNwIsBikz+uNYG0UUKIllrDXCH8EebxQUZdw4cESoOYwZ1feRhS4ds
+ SMX37B3vvnLdvnKVTu1vEJ+fg5dszBWY6Y34GAXgP0BRiGO+lwbUtZPeDhTOgflFAdzF0NNCul
+ lBlizYO91ZFEzemAltjsaID0YKAdBxXeGwoaF9NHQnjx5Yvfu3Lbd3W3uLzZThRvubKSIB5apf
+ vLIMu7154L9K7T1POR/yn0QB0p8RF5O4u11PyWzdhwRZ1tYpId5pn+TdrYsm0NQwvvqOCNHEAy
+ pn7NGo2ZLGJe42OEQFUg2O9Dot2Az0qATK7HODddSxJ8FIgXgq76lVMNjZQf6pKTnh5DrZdC/X
+ 0RnT7D/z38lsFdEETpm5yxTN5+nbLQDdedHps5Fq7aiddFBijWjSOZdbe5TnkzQuK/0PjEOTyl
+ VORpXVhQDhvWajDSyyU+KEBRD0obBbyjTszBn88qJ7mEhnUp6Yc9E1JjkSiMHeWeAUoE2rJkBR
+ ijQwdf+oDfgn+CdYZlQa9XsysWLdvX0OesA9LJMKVKNOAhT6lHaka+OZopvmQF7UDHMSiQNUzO
+ kP08WuQT6pHPg6EjbA3XCIqonWELq6l249GHHPzDKh6LGbHhYsa0efPEf2s2PP+Aw89H6C2tPD
+ 61drZDzhFdOtYpeiAVrVOu0UmB2xujQG4cgBaVF9ID1DTpN0rdeoMrn6TNMCYQRjbtHEQ8vcvY
+ KOOAkqUAjDgetBnG0HuhH70WK/FXs5P8jqkCGCcBlw5wKwVxRvkbhajqqNa3x2ik799a79vb7v
+ 2BXzl+21c6i1dBw6c1XdEDt9Qj0GIKC4R+9gx47iVngdQ+azPNPNQEH+1wVF9bFsT8VOhVqIW0
+ 173pN1UnfPyXp5exaKuA0uRFNRDmBwnnw4J6N0ckMVV2AfQLELNL03orPC1peAvsA+DiMwjYlB
+ hZFnSXZreWRbWCL/pRiLrz+vWAcxeeZIm3sm/iZdPBnBeEotOZZQGG3HFG3RBqQgNdqZk04bXa
+ a1u00GeDBWx82DC0MiKdMuWHzmM0M2aZjl9RMkriHvXJgmpReWS0yPTq3N8gUPdo3Gf8fJ1u6Z
+ TMZ7h8D7PMbFKDvQBnpQLwovhfRn+wEwv9fi0eNjMVCUqwWaaCGHog20UmGyVDx+lqiGlYS75X
+ f9Ph67osTx6AaW4qO0hJ1w4MS8KAbw2yAxV2dlNo7RSE4Xb8fEglcAUT+WeJrnE17OGaBlkqcv
+ pp/oLUH6F28cM7ePHveutWGSXCY/ZqRXor6yv79J/jZz24wAvxLYB93yJUvvMP4rimLiwBc6u+
+ pZhHQQ5ygHkcd6jwaUEMZy0CgB5/6QY8Dua9/+KHd+uSa9V48tGU7sDoHTs/ReRK1bnUda1wcf
+ GC08WX1O0AkCfA71Gg6PBtaOQxlfkaQ9R6AsENgVBMqGz5viQlo7ezUFIEzPU9Fz/maEmfBp+4
+ 3RwEBvi3ZWaDZbDq0sSHrwN/nbdI+asuXrtobv/BNe/vNq3bh+Gs8xLEGcA+xH1DURpEeA1owS
+ B3dwezBGPSzOkR40KTVW0xm8yAjnCy1FnWdit5J2KVB9imqzwrKKcL/qSNk7UUcRttbm3b71g1
+ 79uwx35PDP7x4GfewpNHXjLKXaLPSMp/JBoqA0ulV/2YJPvQrKXJCJeRfT8oTf80Qb5S46vjcG
+ Q5JnFFw2qzTxSGRb0mPDPSH5JVhzxLdu7w+3QyarUF3slBFg1bFms0qKR500KOIC8km7JVbTTR
+ oYTCOGrS0jMNdwD+zU2UJuwLUcxrHm8OKpqpiCld+XQmfZjOxP/5LRPaZ+sAtA2LRxCxXnEL4O
+ z1XQubmKY5a/j2azG4ep9rkUXQCdwE/AQAKnIjKvElFHKGmRjG68iaviBIUJcZdUOFOaXxwgNo
+ kEbXEzwW1JH/2uFC5+KWblUBZdFb84kBoz3TEKiiTkNZ+TGMxbHZERD1o7RcqdvbMGXv73AVbr
+ resyjfMOSTPMQvS8mcCeyqEGJbnkX3gWYC9onrQZWGKNkR0P57QpdLnVrgaKwa4RME0DLjmrA/
+ A7w3s2dqGffTJJ3b9o3+w8dPr1ppOrDtfsSo2E6g5nxSEQ6ABr3a2Imt8oigjZXJxa5CJcOA4m
+ q6o0JFyiFOs4p7TRz86hPVMEInHgAxlZfp2bgqXp2l4uD9nV2sEPce9ndYqPj7m1/ZtZEPrgzo
+ 8csLOv/cte/cb37a3Ll21I52WNedwQEr73wMlNVBEf8DfB4zm4ZlEV07Ox42JTS4r9suU/YEy5
+ Miq8P56lil2TzRPCex5CkQheSaiL0X2Ofy+/HfsJVA4GGxz89Z129neory2DnBysC+Waeb6Sc8
+ ZN+/6/Lco/jUDY95/34iikMu/ZvORUmEzPPQdtFPxmJFx1pjFbSzKjvs3dcUX/TOzQBlgHocC+
+ fdZ4AxqiAEMekgmBkudemPBOu06TQQ77SYH6NBemdF+h9kuQJ/yc0zQyoOReA9XA5YILQ+UaNC
+ XDr8y2Gu5f06k/0d/+TcezEaBVjcqFS3C35mFHO8cjWaqAHu+CZYDj4b0xGbBXmgmkANgo5FBu
+ vXwycDIuGJ2JOVMuvOKaMiXh6RPHyx+Npd9+o8k+oeFl5hZy5tWVMTDtyfPSArtaixxRc2qR4R
+ 7p44SKlzG0Kf3rX8Aid3QhpORTeamdvLkSXv3wiU72lq0hX9lsNfxh2lK+C0pJiSEkltObDhSA
+ VLOh+4M5jik2q++jntzOEJWMLGd/tgera3b7bs37c61/2Hbd25a82DHFmsYJFGzuYUao3yqdqr
+ gO+etwglXC+SsSW1Mw8tGYKx5s0MeoFTo8Lnr+Sm60jOXXNSdSWMeLB8PLCNcKeZgyPQ5LHo9G
+ w4AoK5fOZtz6KKvDjADt7tszVNn7PLXvmFvvvW2XTlzwVbqTUZmsLKGzxBpu/6Q3j2YsYvfiOx
+ jqE3IRxXQKHtNmTStEwqwl1pIFhEqZOZBWJaJZFmJ0n6HSQf4IjvlDfhcGNaKhm0FKJw7du/uH
+ RsO+3x+0JOrX8Ed9BOIOlZEcXGmIF56wwzQ+fUc7AuGLdVQ4meZ1b/iystKlqKfolSE9/chcsA
+ uI3oX/FoCxIkfWU/Gqw6TvBn05bqgpMVoHRFjMIUPID31FxYq1qgD+Jv04JFWH125HXrvYJD8f
+ K1Brb4YDKc4HbBV68q6gx23EthnqsFAyPyGzX4Wvscf/ehvPLJ3rpoLLP4u+IZKhcDmJyQ3G4A
+ BHHv2QNRSXETJMIbKCzUhsxKd4pxqWkjYwNK85o6WfCBh3+DRWTTM5GBPB0qfWav0TjdQfisBF
+ oWsMrZBfBbeMO/+VVRYfDKZuamYTNjziEut0zL62jsYWO+gb2Pw0JVDG9uhnTh2zN69dMVe6y6
+ bSsv/epF9sPco0oIOoT0FwJ6ZCTpoxzYGHeFqEdA5lYmyBBZ15zBLV8A0B8rHKtYfHtrOYGhb+
+ wd2/c4tu/nPf2drNz6y+mDfunCuBK1TBXWnYEDOoRgaAQ5Tk66mE4wF1KGp4EsWxpB9klqKGo0
+ v9lhuvO8e0UfLfJK1EUCLugsO2lBHcBP4+4iSBDklpZTef85GC1WbLB2x42+/Y5ff+Yq998abd
+ nJ50To1uHviPumgwdhHyG4x+hFgD54eEb0OK1y/DqM4tEKNpHhHT0SSz+ga92HcbqMQIz213AL
+ Q1QuQor742ag/6MP91EXO+F64XN69fcseP37A96IDJC0e1ESY1j3oTW9YxD3TtbwKlv1JzYJ9B
+ sRBtwW1k94oi1A9/y1QZvYQyKLhyN14uTN7tBTABiaoIFjy8sm3KZkANnD6K3vAW1yMgluwiep
+ DQGDidda5KWke2IwQ9JsNW+p2bHkJg1Tgsrlo7XbXqijmukFkJtEpisqpS9cDnWws4eyBmEs1S
+ 7JNP1Qrf/ijv/YDSyDPZcRI2wHPm4vwwqJLit+MirPnrMJl8YXE2fsNDXOxKJJqRQZ3h/hKnL0
+ KGcVBwQ3q0Xj8LCvk/uPBySWzogzsE8efFUUUp/gw5Ejx6Eiua48RgnlgEa6HEZ3Q8oGHgIy+9
+ vcHdtAD2Pc51GMwHduRlWV7//IbdmrliNU4rCNLVH/OnP3n0zihTEBUr8+H6F4DygX2mBmLaHU
+ E6SsLpJyAwgOOtAkfrRQr4LZxpwDGKPT2RhNb2+/ZvQd37ZMP/9mefvyhzW88tW5lZLU6TMWxY
+ Rasaguk7uYX4HYm10oEBOrwDWtrFIoxCUvKIBKEHs2XAj2XZiL6RfaQnp0DEXsOXOYcQYd+Xhl
+ qokworxTYD5HxLK5Y5/R5u/DuL9jVq2/a6deO2ZFWw6ocUI5eBAxmUUQ/8q5pzB4eYph6H/bNA
+ zZOIUNh9oAqCQ9HZIZasMK/oPHkNBlBR0xDU5SfUz7KzeSL4mAeh1YUd0uAr++a3fSle+j7B9f
+ 64sVzu33rpm1urHnxscrIvnTvvUM69oC0e8Vh8NJrz3whr/ulfeQAKpbUj40sYMypHn96L1Eq/
+ HpmSRAHmGDDze38AMgPDxVkBeYhL83BnsEiJmVFc1j2eYiH3v8cmKT3K1gDTh3DIBWomjD6s1G
+ 11mLDlhY7GqSyvGIdjExEpI9MmLS2UzN+obpnRRE6ZtCSR3lF9lOicWappwB73egAwYhAw6dEH
+ yA8cPCv0nGXwd5FTumW5JF9LJpc4xpgHXtQYB9Mu0yMUmZQ4unLq8hjpJTmFzddUiolqq7Jz4q
+ yWBTaVNK8F77iboEbwUkoBCJKcZVHAntYG/cU2SMFJtgfDjn8+Cvoqjx6zOp0y/vXiuxFQXF6X
+ gL7OTpVsqOW3jjynwG/LK+ZaJqLlB03Q7IdFu3U2kM4waHRG2HcYc8ePn5sn37wT3YPPP6Lh9a
+ qjOhNX4OlwpycUKnd1gPhZuNAdqe5UjBBgQze23Msj9SS1ipRjRPWCArQiWKcBwiB+EGjuWmbT
+ j0B4hjv32xa/egJO3rlqp194227fOGyneh0qTKaW1BnOK4H075QcEVUj36KHmYZ4JCH+2l/wIy
+ E4gXWtXyPuDwyrycEQOk0ReewH27eTCYZqTqdtYb9kPACbfpqJqgIAI0fCRrgc+Jufn4MFr9//
+ y4b54aDPiNRSAepxAl1Wmy5XMyh1vTPzyIiLZiVwzr4MpqdKarmgBU2JcKInI/2ozsOhggI054
+ tajchxY2IPw4WkhZzLpGMoUj+esTDoJRTJ3BRnOX6p8JNz8Z77UXnaJOln+elz09prQynTQxSa
+ bWbdgTD0Tvoyl22xUWBfo3NibKTKQ7qMtj7Jzf07c3geZmznwX7//RDRPYCoTzVicVIuwO/kcm
+ fyame6DNMigHfQLxMTovRIZE3PsWaeflUcqdBp2x4PVFc89OStIkfvWQSk+Wna6vDoyJz7ZTi0
+ f1WUBTJboCiKW0ovHI45CVLiJSl+EbDxsSD57W5nYIXrCG9PDhA52mPYN8bD63dbtp7l67Y+eO
+ vsUAZmzHsjhFdJw/uvAFAH758or30X+V/V2TvxWY6QRb15dJzRY9DmL7NweICRdCpDVAMpUZcL
+ pgaHO46fEa/QTno8MTz4+twRmhYAZtt7/ft/rOn9sEH/2C3P/h7m754bO3RgKMOq9Ci16XwCB8
+ aDV3VYAw9T9UrI98TpR1djkXXNO4hm+hg1OVFwtjMaaxgFG8immdy4n0E3uzFA3t1xZbOnrezb
+ 79vF0G7HTlmS5jSxeejVc5749YYsITGaMSDUY+NdIeQ3Dr1hIhf/QvKjkRRFNy7vHLzZ4eDGFm
+ Aumr5O/n8KNMNwI+ATEvFiblZsI/AJHJmH8zji69YRQxcVB/A2Mjr16/bkyePKZttNVFEXPA6U
+ 0iR4zryMmr0KsTRky3S+IgZjZM+9QzAs+7gP8p1lu/RmbsVQKdvKZqVxMjM7BlnKPhvcT8S1sW
+ 1us8WekSYyWRuM5R2Zk1lTvvwbdyYTc8hnHJdzuv9ExKEwD9HhSLAD22/KwD+Oes2F0jpoOt6d
+ XWV/jutdtsaDVE7gcERRDNZZfOS1wpeRaB9TrRfAdhH52nRTh4FB6UUAaxFR1JxU6NTk6czuf4
+ ilYk06rPAfha/kLYW+nnVCApGUDc9tgq+Lw1p9lO/8KdQSUlL0DesDygJVRH+LRrIIlWMPIDvk
+ aKN8kLOq+Fx/fRlH43sYO/Ahvt7dJjcx3Sfes3eu3jBLp163doY70e/+UOb876AMbTlXDngjcs
+ Q8EXAPuIKwtJUQAqwhw9QHOSxRSMOIUftj5G6e1e1ENSG0NXD42dMbppd0c6pC3kAymoxh8c/0
+ tTpHICpav3x1LZ6I3v07Ll98sE/2Z1/+XvrPblrrcMDa1Xlmglgp0WGt2kgdZ0uaEh6bFpuOld
+ agAbhkwwlgvLmxMvj+J03yWnJc09U+8GF8fnrFts8+w+wNcHLL1il3rLW8oqdevcrdv6NN+z8u
+ QvWbbVobwGbFM6PRU0Dv9FPAVkt/I9ofaDInkNXOI0LthkawKL5DFKByMo4s8rIkjtF+nhmHtm
+ zSxdZAXoNIpqP3pVCeqlnIIDUM1dmypfzRRnRfYgaEtiHoox9BFA/9e3OnVt248YtSkVbyHDA1
+ 3NSGNQiLtbwz+IeuAk9pagqMg/e6Gxc4asilgTKtPPwjntBWynIidreS+GrH5YFLs2+S2E//Mp
+ 6QNYJrAFFomswkQ1KPTZPBrAGLcNlJJWgA6J8jbJiOa8nP7QybyC+nhvwRQwytzCxan2eah3YK
+ h9dxRAVGK9hRm6rkL3qdEl4pvvnBWzH5/Q51UVUzoKCifhDFGh5E4QhAAAgAElEQVSTGiYiKJ2
+ YccqJSskWnTOkad0m2VRxUfHz8cCS/3P2oPTXFAL4kOWMMgp/dM/gkFbKU1GUDE6/4Ln4NcZQS
+ gu/CNhzw3k2oEanInrR+2VzP0tOc5G8oUloogaa7W1GyQB7oM87ly/Y1TPnbLHVsQVmEGObY7S
+ IiVL8LzrvSYSVRSZfILIvwF51F0xq+yywT5/MAxjXfNATJ/zix+DvAV6M9AE+bvrGLlORfbzTH
+ D+pyAgukDCFqlSqtEve6+3a46dP7dOPP7Y7Nz61vSf3bX5/02rTodUxDNq7qSl59A0U15aa5hK
+ Np80XjEBQcwFgAHEUwFGIBcCiOM4jmuotzF7w/RmzeeeqVl0+asvnLtipq2/ZpYvn7ejRVVtst
+ qnkwUGPP8MWmTw9gB4NU315BsHWGtQNp2NBUeRTyyRLzoA62XT7bnFAjqlYsjRWb4GGrAjs82w
+ samji7HNg1ZOIwTEcpM6k0EMiD7zYJevXxIya16AGsq2tdfvkk4/t0aOHfA5wddTgoWj512HFl
+ /SDPtX13HU0rjWOnziYEwQH1RJfyJe5HIKKXwDEEkYU3yy4yCE1ArEo4Onf0ysEpcJvi+Cv8Li
+ XulCrjgod6uBjFq06h9Mls0gr/l4RiWpYeR0uAX0AfnwfsSnvKRILjwAJSx9++ouQaS517OjKk
+ h1dXeHIxEa9oabEkH7GYR41yYzLJyXNj5hlIn5B+uhzVsnBPtkkuIY8TjilS7NgL35f9zEeV5E
+ CJW7eD428cJEgPufg+FLYpEWjUxpEHoPKWeTUe4WBGRQDsCbWyfmzgX0O6qX1l+IoLbRQ6sSRQ
+ o8ceKYc7Nv+1rb1obtHh+h4YFcunrX3L1+xY4vLjCwB8ezqJfkhzTg6OBXZ/yxgr9axMtjHGg8
+ Ja9J0MPoKo0z8JME+fPoBXIxU9Tv3ixdHWyg+WMCir5DsMKpzVRYW0Vi2vd+zJ8837PHT5/bk3
+ j17cee69V88tLnettUrY1JbC/j+4Ftd9RDjIPWM3RaZ0ViMEFQkG4rj+cgyfJtjMDprTL4h+Qf
+ HBy7YpN62+rGTdvrqW3b26lt26uw5W2lWrbagQ4FRuA+mYSctniNnFiiS7/cVzeP+aKQm7A+UA
+ UF1w+EtMWvVO0yTZ5OnVQLaEELgNTA3V68n+wqAfeyrYt+FCkc2JvrFyJ4WyoqKS2CvLe5FSil
+ 9VG9RVoYO5gf37tonn1yznd0dNgGBQpA9ubL6KByHUZx+Ompd8RkKqbavwmIl54DrGz9b5br27
+ Bee3St/pQOjiFojs0mvl0QcWdSbY4yvmgg8WS4B7ea22gB7/p7HkCSXhXtGKZGIAptQfemzznY
+ H5AdX8ffUfBb3g7Y+yspwR2sLFdJnUOsg8DiyusyIH88D9GcVmk6lGykTCSzkYUAmQndidjiP1
+ kXFKv/ph3/l2aTazckL+uYrJrvHI07nXBo0HAoevYl4Jv3KzH8yJc3L/z4Lq8V/B60U6a5Odd/
+ olTlGh6QRaLjlUrsvGdm/eoUVX9Xm0n3Ar6BDIooD7w199f7Otg0Gh7Y/GtnWwY6dPX3Cvvneu
+ 3Zm9bhVveNTcImIXvNwQSzIMuzLgz2iOwGEInuOI+Ru8APZX5pLM/7u/CF+Dm0/tMJw0Ad4Mbr
+ HvFjaJSD6dNO0bLOLSgPYz0F0w45CcPA8/Ppj29netd3dfdve2benT5/a49vX7enNj+xw+5m1K
+ 1NrwkNEk05SQV4NRTrQA/DRZavFHFSPRsTjHiKqB4iRR6X/keop+K/5CiTCFWZQ41bXls9ctIv
+ vf90uXblsx1ZWrF1VwVgApkIsCqzo5AUdByqRRncAe3bDqksaQ18C9EZormJkLvDXAsGVFHJJf
+ kmnsY899P1Gm+yBxA8+6lIqHK2yKOrGqmM9wou1+g5JIakgc8yP/Rcbn7UoH+6Nn/Vwig6Xn3z
+ yEb3rcVuhA8ecZc6L8O5RvD/uCa1N3L48j+RThpIiZ+fmEgpENJ5dla8/fo5ZbM8ie91G33kzY
+ K98Oy1vZ1ZUFI0MID8EEiplUl5+Ns/O8Dbs9IaBX2BKHoS4Fj66vWMpBo2TH2CxxYogOLZc7ni
+ JznFHEciZIU2eR2ZVt6Wlti1Dprm8ZO1O05qNBq0XUDSfn6sp81pAUKXsWtLyCMIc2PN6hl9s5
+ Q9+8KOS6yURIpM9JkmSO8QJS8sfLRqqBPBlKZYi+gD/zO0tXUzaG6VTMpdpReNTRMSsd8zNy0A
+ NUj6OrEJkFkZuX5zGcRFZDrcJ6cX7ztI74T0j6gQzXgeDnh3s7HNE4f5wZGu7m7Z8pG3f/vrX7
+ erJMwbTBKaOkWab5JiTyvjnCPZI9TEXVpS23D6zyD49OjVYaZHIFVNprVsJY4Sh0zgo2pKu8GB
+ AdsOKTpNXkeuM8SwQ6YMWA/9/sA+fmH3b6w3toD+yrc0te3Dnpj24/pEdvHhoC+N9q1cmhvJ1r
+ DWAP6Hc01X6hbskjZuZHdBFFzUOURRpp5V5jgqksQVoJU7sgtKmYwvLx+zI+Ut24c237crly7b
+ aXeJgkQp7O6Tv50xeNpkB7DGURBE7KBscdgB6/J0ZJ4DP5wXIX8i/5gVdKX28CzbKBk7pgKemM
+ AA2FHj+o0Hi6SUYCLD3sCLXzQdF6V+bjeyTmVaK8jyIiElznASGRsCRPXz40D786Me2sbbGQdu
+ ddodRJKm5GbCn6yeM8QD8/iv4/EQ5RTE6ldcjsMipmFxC4gs0i7RmI/vEGQTYZ0PHlcUEdeMF2
+ ggsddKJgvK/649C0o3/xmfCQY7vqy7A3gMmZkVknxgJysd9yH0evM4KK/yzsCXFGX1td7dZy6i
+ hJFLwgT/4UUh8KdFs1Wyx07Bmu86/Nxt1q3MWNOzE4aVfk/0CRrTCETgkm+nze33BTx6ugv/wp
+ 3+RwJ6nv9+QXPaY36Ayd1pUwKNo4NqMxK8GaAdA5yds9ozzWLnkYyOqsJAB6tnCAVPVc9ooewX
+ 9Z+HsEzefVRG41V7BEcaghZiUxEYgDC2BR84OGmsGdjAc2/O9DZvOj+zffOPr9o3Lb1l7vi7DL
+ Lo0gsWTt8vPDexJI4rYG08AdF5ohnwxRfMRcimqKKgc8fAI3pGl0AWTPDSAXlbO4Rkve2oVL3F
+ akGqhY6h3/Xn9BPdlOJ7a/qhvuwf7NuyjeFmhHfTjh/ftzvWP7NHdazbZeGZNDhxXlKIB7/DK9
+ 8jem1v0716nIeDr2XMaFNcJmoBkozuuwLisZoftri2ePGtnrrxrV6+8aadPHLPlFmSgygpQa6l
+ QDSNtP/2CSNvI74Yqm4EKrzgAKKvkvGXsFVEjHNjjdQ2qd1J9wbtjnWairXQayCLKRz75ypqkx
+ gndvbL2IsIX8KfIXh9YBw5n2Hpxkxst7TKfcaCDXI0/WhN7e7v26aef2PXrn7ApsLvUpXkXJ5C
+ h758JmmhTXieK9Rj6HtLVAPzMikBFQilTAlhLfDxXS2aRwL/PfMdMZJ948Iiyg5N35VYMVipwp
+ uwXn0fXCewdQxjk4HkPlFlhvnKjgXsAEBX9lWqNDJ6jeKt7HOKVMo4V/6X7UfQ6FFJKF71onEG
+ pFoWaDFjRan3OapRpwmETg1Tm2XWLKL/RwIS4urVwrQvy0ofwgZE/7MW9HlYaxIT98n/8338u6
+ WXelTdzAhZgr2OilJ7kPJlL4NRhGg8xeKSsMKKl8FKRnWoaVxaosBtaHBl1xa+U2ruhWXBVCPC
+ /bIE25JZpzmUoeXgiF+8d6p6wQpbyAlbCcMAcWX+vZ739gfWGh/Zsd93Wdp7ZL3/9q/ZrX/uWr
+ dZaBOARMB4KEuSwAI65kUf2WU77BQu0pHH0EAn4BdjjnhRRsMA9A3rfbNhLsktAdO/jBGl4Jx1
+ +AnuAHEALuny3BSbY19CHWmHEiNSYhVeqaxZsOJna/kBdpvKIN44/fLG2bp/cvG7P7t6yyf6OT
+ Ql8Y7PDoc1PD60GlRJkan54oADLfix2RiujQ7SFg0GFZvD0czaZq1mlu2ydU6/byplzdvLceTv
+ /+jl7DU0sNU04Q/MYMhuANuwfMEuY07M44HxkAxiy+SGHiF5FapdG4mdwaLsdMgQl4biZgkgW/
+ YMa1Y5mbYcKJx0e4vz1mdlMxcYyWSVkwegrpZdBHxQ6b6e44gczrlzKIAxHx/3T6E/ILD/44F/
+ s8aMH1gB10F2S5A9RLWg5d14kVeIupJg/QTuN7BcPOlcbJYuHvOTqGYgn9+kng4YJ80P9QzGAJ
+ 27ALIgHX65TK4KxMlWUjrpQqvjBmN9Uvi6732XPgXVfbzRpWhZgz2g8ee9o/iuvkRmh04mv5AI
+ c/aKWGdlHyk70WVHDKyRU2r+02oaQDM1YUIVRlDa1+bmJVasVa9QWrN5c4Lzc+gJsxNWVjuFBm
+ BaHqL+Ow4CHQF0jYVmDWLDK7//pn5PcddltOpFnUx/8AznU7EEX6gk/BX2hyQdev/n9adJ8KC9
+ mGC6XoIk+8vehxlViyIie4mSO4qz07hHVSSefovu4ziwFTpIv/7egffLDi7Gpm75xRKMX9/Rsx
+ dinQjAzIXVGshsVqT6abPaHdjAY2/Pddbvz6Ja9++Yl+71f+RU7vXSM0fxhNApRiukwldESuh6
+ eCNmhWdpjM/kHuMusSOYBZdKrvxTZS3bJl09gr0M8/OzpPZMoHYEg6wAceuKUB0BqikljcP1DA
+ WnKqJiNRaOJzU/nyS8CPECJYHhHv7drB1vbtr93YOPpvA2nC7Y7GFKvji5UNPXAhAzUxnQ0tCl
+ G+A0HNh33bToeWgUTrg7HVjlElImDYWLzUD7h+TQbNr+0Yu3XXrfjZy/a6bMX7fUTr9mRTtvad
+ ShM8HmV5QBw0cyEASPDwwVF7W66ho5iRLFpwDlAjk1VoldUkMdvpzW82Brrk7SmgzsLrjwgEck
+ rOiYlBsBntoRUyushPl83N+ATThV1opBdej+T1keiBXPK1MEzQO0Qg7VV24DE8vqNT+3ja9dsf
+ 3+P+u4ozgI8pOX293XFimSkZT6eB4H3BKjgjLXsg2fCUiH2Wjp88sAp9Ly5yCM/tMrMgSJjty/
+ gJRYZq3/aQtyXve/sF3FLouGNh/x4RBoEUT0kj5ov69St09Ist7koQBZNEgjIJeDlX8FI8FArg
+ byulLCYNPG8k75mtI+RVcA7TIeNa/cqE/ruQKOPOhNqZHEoYGJcvdmwZrPFQxvy2U6nwwyg3qh
+ ZvVq3yr9DZO8nnyhcL5RlCpTg3MPCoLh5bqsQKQ918kpR5AtSWAfHyI+Q1eW3J7oNsVRwo1EgW
+ fCbrciiqP5HllHchIj+ZXrEKDy6ZLMCl1C7sE9N+vuI4F0HpGyxeIBFgah4SLp2L4hmNBOiweF
+ wYHv7fbpCbuzu2PV7n9hrJ5bsf/qN79k7Zy5Z0xr0HtBBiasQt0zuIrcF8Mh+Ng0t7lt5kSlIc
+ FCgHYBqJ9HlGn4+Uqh4bO8vwQPMPwfvt+/BoG0A8uogBTgJ7DC0RROtpDWGQ6Jmy5pz22NXGSD
+ yRrDes+3tdVt7/szWnj9n9lOvd2xl9YS1l4/YfL1GiofyT47/8wIcDh1KQdGdPJAbJQ+Gng16+
+ /Rg7x9ssmFl9fhrdur8RTtz5rwdP3aMXvOLGKxC3TdhSPUJjiEGVz626eHIeofwVQqppQ4yKnE
+ AytEgleoVoGKUBcl/v4h2I1CBhxDXLM3lNAAm7BXgmQMqCD9LLye8PqZ2ecQcIFFIBT0GzgA/C
+ RZ0uiTRgJQaobbRgwZUhBafQ4PG4xTVP3r4kFEf5qpCcgmaQIe2r/DwwMrnBPj+0C5XZExaK4q
+ 3TuPgGiUTFabwOr12EXhDUBMblaif3GdHNJ2upXiNBTU2el1Ra75gD3Jgj20c0XkE2jiUOE4Ut
+ RkEDpUKP3ur3bJmvUH8KWqGFa4XrcZCkq6DUyKBL/0riqpxMAXDwgtF0VY0ZmAqFVERDM9VrLq
+ Aaz9kBolrodVyFcqyqtXgx7O4aM1mQ1bLzY5V/vc/+UE6apGahEGZOJbw8Si6YMsSSn+YXqmGx
+ 4cobg0OJ/+YFqAeqJqhXCrpoMyIB9wpPSTAOUH6FeoKt2mYGTmoKrSfkLHoUwdtWq0iLGLRJYm
+ oYJZ8mss1+XovZYOhcNGhlaeU4ku9noDFg45TNt30bd/BfnN/z27c+dQWqof2O9//VfvO+79gi
+ 9WOzU1QwCwGXc/TysBtA9KKLDKWVy+mnxLs+T5Zc9XngL2f+VI5oAvXm4MovQQweXSjQqZoCES
+ pnC3Loh4KWPM28cgYdMzhCN7uO7a19tSePn1iT5++sJ3dPQ5zWOyu2urRE3bk2Gu22OnafBVcs
+ ZqrcHDQCnYBX0PhVS77NLxjFjUmLdQfDaw3RTNQ1U6sHLGTy8dspdW2Zg0DVJDtEZJc6gi9uYq
+ 2jM4dyAdefC76CaS/JtgH0GVzewPsGa27LzRzMe+4VDY6sQkKu4dS7yDrGw1gkKZsItmEI/LHo
+ RPUR+jwk5NlHtVHmFGAY06Hym642JMBQxEIYEv19vfI0yOq393ds3a7Zd3uEkGOEj+Ei95JoVp
+ ZPq9YmWDMJWBQx6wnZgSD0vQeAS/4a7nFdChfycnqwkUOTvsWSz/Au+xqKQ9491RKHH7sjpcj7
+ MQScNyoghQe3uMx+yWwhhGJIArGYQcuHMGm6pXhaCZbkJJwxYUokmH+BLDPMGd2H8eY0rjykLc
+ GDa7AOw7J4vPF8PdaFZiZiQBYwsI9m2oyYHWBUX671bROu63IXpvcfXAcuKWqycH8ZSllus0Ol
+ HTDY+1CLoahL45IhO8BHooFBHej5ILB4tdBQKCn254sCeIhJa1ydsfwoTjOzmkineDi2KP5KkX
+ GmT94vER8j84KnY55JB1/Z6EzOjg9i8mvi5w9inujkfU4vAKTq3q2dbBvt+7esp29dfvOL/6C/
+ d53f82Od4/YAlQ50Ni7CRc7ahnZZ+//RSP7Ai6SbQKje1dPCZRcqvYZkb0+pXdCu/+ROPwY1ac
+ 7R57bi7RoOALwSuaoCVU49AcYybe7bs+ePrBHD+7bk0cPbW1tww4OBjY337R2e8mWjxy1I8eP2
+ 7FjJ63bXSX/SJ8cGq2iyQXSWgF+bECsn2isiSi2065aq1Gl/1AN6TjWD+SUlXFqMJJ6ELN6dWC
+ op8C5dwKwO09msxH4GT1DVTEV2YwPOKcaSU01csz0gIABiHPzfXjzwxlzoJoAwB+0lDdh8Zmwu
+ BtyTe6gNE/gJZwgTRL7sgjE8kw07kkB9JEZ6Jmtv3hOrh5zZgHqAHqM1WPXLAt8MbIxux8+7pE
+ HUiFz98Xg08GYfQP0Re0pSI2uZ0XoZRM3D6Tc8qI0eDzRHmXOnJkvJj7NeGbFfi72bkEVEUtcG
+ 4/DnQFZX1ki7gd4bkS+6BwGOIpyVkDqE46KCmq4CdD7oKzqeWVAljqai+spyfJpKRNZg+4Pi7r
+ Zgw+mgVAfncleU52Hh766s4r6p8s5WftEfQsUa7Vi9XrVKr//Zz/0Am2AfUyS0mEmIMyjWn4lR
+ dVlPkzReUSIeVQQYK3h1KHl1cqJ2aFReGWEEjLK4AVD/z9zV+PQiOkvkjgVN1fiFG9M8c2SmqK
+ 8sq9PFB3GRTQfh2AkipFNRGdx+nzu2MghJhhiAd+Ug75t7+/avYf37fHTB/bGxbP2v/3e79qV1
+ 89ZDW3ZlQXXraOhiqJycnvF4fTFIvvcbFqHqyJkmjV5JPv5YF8UzJVWu/bcI+poMCIYgQ5wIKQ
+ 8cXhIO2QG4ZOx7exu2NOn9+3unVt25/YNe/zwkW1u7lDhMj9Xt3Zn2Za6q3bs+Ak78dprtnr8d
+ Wt1Vwg2HFGJpp4qzNOU5dXqUBvgN3orkPWpSUo+9EagR8AA3rjVaFKrrB6GQlCLWoToGc1Xjky
+ BlDkLpD5BioFP1C+ca88iQoJ9qMNi6IiDfRTI0b4FYEcNYjRCE1YB9swW3EaaPDd7FxSdRQzvv
+ Gqm0gjljYBBgbC+v5xpe99Ntkc8P+fPgfK6d++eXfvgXzhYHFPElpaXbBHcLoZls2u2yFbjnqj
+ 4zDf1eazaETzg/FqKvR5gLy5QVIxH6vyAPrDGQSI6e7XmwsLD1SopU/osGkdYlYN93Lv4GvZqW
+ BSzCxoRPag0BChzc9ZEPacDE7KG5iVzeWuokuTWGThkeKGMKsfBV8B9iF506xR4+N/53T6cKag
+ qHoZhrZHXGqNaGBx/sFuVQ+GW11Jj3kPxJqHlP6TnfuXf/9kPS5F9MQUqIgJfUGmMV8Zd+VUy3
+ YuhF8GxeWEDizuAAqcsrDwRReB9WPDwu0k6Kqt+5w1VOu00FSf/FQud4w35bPTv4XefKByXQLE
+ QnTjOgkdMYJ8Xw146YZ0yekVkr89u5LE5nnAgj/PtnR179OyJ3X9wz7qLDftff/e37Zvvvmddt
+ OXDIIlcziF9WGiK9nMCe+0jSfHU44eh1ooeP4+zT5SAexzFusTnQ2MV/O6lnCuakDBicDrWcPh
+ Bf9fW1h/Z/XvX7e6d69Rxb65t2sEeAA+rEvbGdavVW9ZpdxPYLx173RpLqyyOQU9cA9AT3NGRi
+ 85cRCjgKGF3A9nn0MbDHvl7cOEorGIMIApax4+csBMnTlit0bbJ3Lxz/3Lm5EQrRtI+2YyDaNC
+ TUABUBFWqRypqRYNV8PO0XfaCvGbH4nNJxRSKHNA0bMpity2cMAeaajVUDQCRfURwHLbue8dx0
+ 5EhOiI9cMm8mnSofAbYz5TuuYu9j2B9fd0+/fgarYz7/R6pCzguYlC29PXgqkURSRkk5RX3oiu
+ 91PHqB1PUhkMCyoNUM4MTK5DvKS5MhSXJwiLmJAvtUzEzgszP5+xnwT4CzSLYi+BTKivYXUBme
+ chovwr6Bm6ToLCqNUXr+Ezs2i6mU8WBpQyqOFzEYnwOjZMC33iGDvhJ2SK/HGZLVCLpMIz7F30
+ MwqdMUhqiCq9C6d90eiR+n4GAirt8zbmpwD4eDN0IvU361TRONtQkaZslO2NhDTIyyOXoMyGaB
+ p2GVHIcHrLjFYUQRBECDF2LQFvRaLxvgL2Aq3A91AdXETYVayOSD2lUiiQUxcWjF1XlPjjxnpk
+ sseJqi7zQWeLpPZIqRxLeMg5QGE/sAB7nvb7t7e/Z9vaOPVt7bvcfPaDK5De+9237t9/5tp06c
+ tyq8zXyxxgNEp/n/xew98j+c8FeiMWPJRpAv1JRl0ApoFL05QsPnvgoNA56trf93B7cv2W3b3x
+ kTx/dtd7uNikK6BXmDBIwKBwgAatSwgYpWKvVse7KsrVWjllz6Yh1u4vWajXVLBe8N0cEDuyQw
+ 0DA0/etj6Jsryf+m41PkHsecs2trByxy5ghcPqsNTtLZrBj4MhMRfYomArMROV4UO12ylosKfJ
+ 32gZ8e6xxgTR+Vgc1pbdwradCyT1yYBU9GhLsEdlL8SElDp8DPVUKq4FKVo+aTeOLwm1kej6bw
+ J8RB2aUOtd9f2RREbYDDpx7sEa4ds2ePXnC/Qa1RndpiTTGArLt6MSMSVlxQCazM23Y+D+q7SJ
+ q9YBMB2BZZZM+UwRdaQPBnE5ZSqw1LUVfi/F9oeTJaJzQwUfxtRQFejGV1Rq3vGBjnEf0yB7J0
+ 0N504b6Rp34MROB3dyusVcAFMPGHZ/84BP+lN85/y81/gULVNheFNl7gX+CavcH8GxaGXohxI/
+ uYI/OveCdqj1+LWqiY+yIaL449yC9/AvPfKaS+kQFuHTVOiWKC0/PwzeR+Ex4wUCxAfe4Wh0i/
+ wX3GZHlKzjZEP8HBEeWhM8ka1rx1kH7+BL3zVikrZqNWdg4KEtwZUICe3H3L5kVZQdF8RravAS
+ 5pHMu01clDjVLs7iYWWSe2sHw0Pb3Dwj2uzs79mJ9w+4/fmRbWxv2za++bb/3m9+3N89ftNp8g
+ xGmBAeQDv5snH1O4/BDzNA4PxHsvUYQC1i+H4Uihqobz+Bwr+W5jeacPXvx7K7duvH/2o2Pr9n
+ ao0dWnUyt22xYt9myOroSEc3XUPyqk4PHK6OLFM8WdM1Cs2G1VsdqKJDBFRP2BujgRUPTSNkSG
+ pvgOYR6SG+gpieANfn9CmyTsVUO6Rl+4uRxu3Tlil24eNmWV49ZBYCPaJwTsUTDAJIoJUWrOia
+ z+GAOrb2smIcgBs1EAUg8D9SwJhGCMh7p52EcB0BHZK9Rhcw8vMP20NU56d56NCuwdxIngpOkz
+ hGHr2ch5MjBFOsmYzJeiTxYY7BG+Pjjj+zO7du2u7PlKpyudZa61nDzs5i2xIjeA6w8w9aLO53
+ ja4ETzagPj8Mrc4J0Cjb2VAIvhRX8XwzC+Sw1Tg76EV3nnD3FJO6LpWsrwBdIgM+gge/ofEaXL
+ AQkaFaqsTDdaLZsrg57BNQJC7BXrUGuqxFQluiyANCsl+GlG+9zZCML1vmghxt/aqEVowejLy6
+ iU937Yoyl6Bqvw7kpYX7Y8O/eOS730bSsdHd+/09+MA2JZNWd7lLBJxWdoK6BssGNltyYTA9RI
+ 9rYnABtNKbSIwVnwUfmPRGZS1aJGyvWnHxdCrvF2SmyL0cvBZ9ZgD1tdWPpOd/HhEokVorguTx
+ Todm/QZwRbwA3Sqann31o8ZDziCsvoMTfwzGyP8TUqn3b29233d0DW1/ftIdPntr62nM7f/aY/
+ S+/+xv29fe/as1a0yqHfgrDAM4Pr8/m7Eu6tbRZdMpjAcwaMuEG6T7jX3AQpfuVF3PjYPSCo5R
+ SoD2Q+olaAk0Srf+iK/BaI9vZXbdbdz61Tz/4B3t07R9td3OTi3FlsWtLrY61CPDg3tEEAo90b
+ SwdRCKY+Ce+5AWvmBeM71ERlepERs5ozoIsE58FDVC+V8zYmTxvY6h/xkObr5otrS7ZuYsX7MK
+ VN+3Ia2es2ujaeDJnY7xWGlqjv7PKQ1to19OTFxeFERRkPqlMFJkXeNFZi+jdm3MA8qgfcAiMy
+ yxDi077Y8+OEvfO3V/46Tg5U+zSrMiXc/WCfswHWt0AACAASURBVA8rE8XqgOdZGTJVPr/xyCC
+ zvHbtQ3v29CkBsFGrWRfeK4ttNuGEaILXxeer/pEYjZgwIe6Nd9ai5V9tBUHd5H4+hd9WcVgWe
+ xtLDyGDVFZuB50AIXj6GPnn+9T17TFdCkPtiWk+v5dfx/PkBDnYdRxwdCRoSPDwtBtA8xENxuo
+ 2V1XBN0QA+vzKLmaBPv/vOK9yP6tZMC8NuXfqs5Tl+L3jezndlh8GOnBj9rV72XO/6hmRo3Qpb
+ KGQCoovC+kjY//9//ZnpHGias2Cp1MspD2co47IME7PSJ+KrkAtZFw4tZ70btDAYtJSeA9Oq/e
+ H5pESAZ5rtFxomW14CqojmrQClPnzWTG3wPvQHBd0BB/W7Pemh6oCdQ7k+WnOh+QRf04LxXXAj
+ wW6XRSB0LSyQ7Dv2frajj17Cm35M+t2a/a7v/M9+5XvfNuWml1GlDAtY5XdH9pngn0siOC+Ur1
+ E0dbLDbcCe/wClGg2gBdrM85V931q8zD2QpTrY9gApgBgXBt4ebUzQ8IIfhzc/GO7efND++DDv
+ 7eHN65bfXNfMq92xzotuCfWGdVDygcTJ6yHNP7Nb1pEg+B5UdSMw54LPtQx3rGo4LbgismPUzo
+ Jywc4Z/p8WAj9MQN4emj1dt1OnD1n566+Y6fOv2XV1rINJvM2lu7VBd6IYuc9mkfaL0sEXEsc4
+ Kl4mAh9SDiDnkQHpiglDRlHs5RoGyhTeGCw+SwOE7TbZpw7wXPs4B7CSXHbxS8NueeviAydzoQ
+ UMUZk4t9YZHQg1iEytd29bbt986bduHHdtre2+LnazZYtLnWt02lZte7TmrgIHUg8/0cwJ6Y65
+ dh8Taq0Dg+tQruJXKIcwF/QrxFwpQJ2RltxrnFIPOPAyOoRyuDh/VJw46lWByYCmSD2rXs18Vo
+ 5UGiiucD7B3K1nJ9nXaLZalodyhsVguSthddZALUoO8JYaUW3rnPmaZpVZn+cSiqemfF56oQOi
+ 2SxB6F0K7IgYhV+LG+c95/lvSKDo2yUh/sMRhpVcN7Rm4loxKK5r7cvIl7Dv/uvfzrFX/Dh85O
+ LgyCyw0GaY6VBuCkAdHx/aFe1uCdqz+VgaXQsutd5CrijFaPg6aIwi9eSs4oH+1lBSpu8UNnkk
+ klG5x4NxNd1nsyAfaZ2UJ1Ah07i/cOe4RXSp0g5A+x9z5UOBqaMjPJGlF3u7OzZzt6+bW3s2fO
+ na/b82RM2QXz3u79ov/Xrv26vrR5nc5U82KViinqFVltwtP4QvhTYa5sysk9gX5iy6m5D+TS1B
+ Y+MMEwFi1QdudENiVsl3h5yynt3r9vHH/6jfXrtx/b88WObHw7ttdYitbzgf1GXwQYCjYe/N9D
+ BV1Odhgs2eFqfEpbA3n1XGGX7elIEFe2+enDUI5MjVy1oiqiNnvo+7wB/ejfsfKNmS6+dtIvvv
+ G+nLrxpC+2jNpxrkHNPTqljPANtEE2kAvcujT0OPb620w28eDRheVMZRjkOBwH28L1HZK/AJwa
+ ZMB33zIFriCFzkdImd8wigkk4H/ws31+o6VGgw5JPC2OCH3YlPAgBBJADD+358yd2/dNPWDCHM
+ yt+ka/vgq+Hf703ETFhyWiDrG+FIQGvoTiE+Ll83gHFDxkVpSIjDl4BXZxUCuBjFJlm9EI1ENO
+ 8IvKNcYSiLmCjLXM80r5cozjYMD7RcSsCNWR/sLsYDJnBYD/SNBERPWa9NhHRw2nVpZXus0Ulk
+ k9L48ooSWl19aUmp/hcTpXkxdQ4kNPndronvidM9IRDGemfUcPCAN3TUELlNBXvkw8VUsOZZyN
+ +MBdMfhYyILIP+WK8MdMaeKVkI+GwgLEJ8G8obuA3O/JY+HJTpwB7urChGKeqtqY7xYAQny3K4
+ pYuBO+bG5rl7pV8uIzGvbrsxbX4CAIQgVaia14B9jkNI57cvXec84waQP59xXtEkUUPIMA+Ihb
+ 8iQgTTRq4R73+gW3vwtr3wLa39m3txYY9e/rEDkf79pWvvmn/8+/8ll08e8GqczX6vOhGKLr/+
+ UX2OgbD4xz8dEgKuYBcBaDoYmpzdDOco7cMpZXwjoS0q6LoYTDs29r6U7vx6Yf24T//nd3+5Jo
+ dbG1Za75qq+1FW+12rNVsWB3+KpxypICAlF5NHuHFkG0/7Nld6kO5vQmPy5sNTeKNkzIhUXEeM
+ DBiVoFWdgPoJPTeDAQhY1FA+DjzzbotHj1qZ6++Y6cvv2ftI6dtVKnZeDrH53YINRF+xpvEVGz
+ VQQKKYX5OE5vC6lfRuhqlNNlLoBK/RdtoEAnpi5fAPtdSp/CmBIjFFo0IOTa/Aw/jAR3MjOwBi
+ WwewufwSfE2sd3dHU6iunXzum1sbPAaUYhFYRZg32rheXnTkds6pCzLu+mjiUfNL0XEyEAPvkF
+ ez0mxbRzm4kOyJqwQZXiJ1+miGM2ZA718dgoVSvTSpEjWr41BqmMIvht1EQx+xzxo2iDMzVN1A
+ yuEHOhpxeK8b7JfAW7FrGu9eXFQBWZ4EZvB6Yw6sETjBE3NDVa8jlROnlVmX88ZhfzZk8pxoHw
+ psk/9DAUuhW8P13N2MHNK1r//kx8wsi+AEnQLeCw53ilV9sU/HrGAxtbqGbAPmVrQNEzZWdEuF
+ zkEkNgMBbAl/i04eMqQXCWdaBe/+58B9orT9HP8Mz00j0YIKsHTeyFnRtET90H3IqsniGfKn4G
+ fusWXwjAM3GB/cGDbezv0cd/Z3rfN9W17/uy59Q627ezp4/a7v/3r9v6771mn0U4GaN6n93MGe
+ 0X21Ns7WNKGwIE/N6tFYxe7ATEqkf9+SKnExCAl3bNHj+7YjU8+sg/+8X/Yw5u3bLS3a8vtth3
+ pLtnyYtvarTaL8uB/qyjQ15DdYR3hd0RKWXSaAgnZDjBCT7UagX0U1qO5yVeAnkRmVyDaxAGfQ
+ T/kkj4xil2F7C6xpePH7ewbb9m5N9635RPnbFLrWm8Id09FoIy+CfITNT+hTsUIEEKDsE9Q1M+
+ uWKhs0KhDaaVnBB7xaz9IqhoFz3C15MjLlDWXwf7lTZ9FdvmSjIhQ+j8PXhTlypMKPze2Z8+e2
+ o0bn9j9+/est3/A7Ax1lKXlFTZTNVuwB1DmBGBJFhhu1UwgdOGGDhXniRngONjj2UWBNiLSkGN
+ 7kBYBVqg8CmDMtpVn1SFjVSahpx7BGOljz3LZoEnqRb9wGENSenDQI52Gwx9OnvV6w4vQLhrRs
+ AO9bgwV91GZev2cttKDiug5pqq9CuwVBBYPKWGqPryonEyFlaJvp+ZKFHLQPtH/MZMFiOtQWpG
+ Ui04dRQZdiFz8/WFxnKxX4ThYwZgscK01fshw4VNTylhg70Y72MTyP5eUTC8eq1iXEqCrZg3Pe
+ dKC0O1Biz0i+/jXAG2+mnLXJCFKyWLslmgRFy9TAvsw3GVKyE7HUOyo8KiDKR5lRiM5p5qW4av
+ APn+osfDHkl9iStPO3rbt7u7a9jaonF1be75m+3vbttRt2Pd+9ZftO7/0S3ZkeZX6dGQ/oaj48
+ pF9cd/Lp5J/SC94hqzQqUHd38gC3ekw0tDxdGTbO8/s7v2P7cMf/53d+OhDe37/kc0NobZp2bG
+ VZVvuNq3Vblir1RVPj+YcRPY+ACLWkAp9GS+Y0RIxMCTPqlTI1HEkpUv8tx/ZXihG8ZhD0gHOb
+ N0XlUaahdmWN1bBM79Wtfbqkp04e94uv/8tO37uqs3VV2wwWeA0qhhIgtcDWHBdoiMYBWsHeF2
+ LqBopb/D+UnqI2nHjMwd7BUzaH5pCVfK0TeFNKZLP7lPedJR/T4ry3NmYqiSEDj7tC3tz0D+wu
+ 3fvEOxfvHhOhRCuH5TayuqKdTqLbFiTYllFcwV3us6gMghwSfIDz5aQIJpNBpDERretd2l7fUu
+ 8cUE3FdPrXI8YssSULRddtaEGyqNaUjoeRHL/GszAYD2iPhI0TMG6Gc8OQQtVN502xQE197zR4
+ S2WQPYWCkZB4RTBaRbRlzeTN4llGcdnai8je4vmq8J5Ng4E3edCghv3Ow6GhKtBe80cQlGfSfU
+ t379h4pZ35TEj+j//7IfTSD+xYXATwysCiwBytTgh8Cc2sVqrs+h/Riecy7VI0bixPqNFd5NQt
+ 2gmpcRNT9G8op0A+3wmLCPVmRusf8++jg/tpmiSyWVpUxbNx3NkpEVjo5xGKXPl2gwZWPmBpaP
+ InRDHUosMDke2d7DFUW8A+831XVtf27Kd7Q1r1iv29a+9Y7/+a9+z06deJx8Zw7KDF+V1fUHOf
+ lIRqL2UgXhcQmWOT68i7HqaLNAvOFXSaRhTOOjZ0xeP7ean/2IffvB3dvvTj2x37YUtjM1WOku
+ 20l2ype6iLS42rNmqW7vVZcbHHgodz/qT1gru6jizceI/gwqMoR1R/NQgD60/2hnEgeFAT6qEf
+ uQaEyjDvBipiMgbTpbihwFiBLXqnIHHP3rugl18+6t29d2v2dziGeuPjQ03AHxkZ7iPEBSQ2mI
+ Pic+c5aHguvlD/F06/7hGDffQdTAb8MakHOyzMqw/r2JdvSqdj0wzxTehoPKIkUNoiLMZtTCd2
+ PbWhl2/cd3u379re3s7BHtcH55TAfZyK8XPI5fGoZYmUgV1AesJV+ZgV+ZgD3mTaCpvLHNaMOy
+ y6XHlK4J0boBrHBgsYcTUrwL4tL2KQDKEIhKSRAAzVc1rOmUxdu/ggPUy7FWAO3o2RCvK553+T
+ SjK4v9ciIBLCzsF1QLc8z+amIqtoZ2URem5gONVSzvsoIPrz6N7ZiLRuFbqYygwMf/86SCIN4p
+ IPzKFjCnhdfqhnzAOnw2RPV0EoQnGhphfYOQOtzR8mDxyD+BOo8vS7EO9JGEipdfOuXlLcdBCV
+ H5QoRN+DoXEqaBPMtUFn7qDeRSuM6kZb4IfEik69geV+sf8huQF6FIk5Wmv6IbiXyJ6ij9fyas
+ lekTcLz1yxiPb7+3Yzu62bW7tEuw3NnZsa3PDqnNju3rlnP32b37frly8TOtRALvY1i/P2R9SX
+ xtPIf/T/+4yTLXAu8mZH1gJ7EkBgPPcs6dPHtgn1/7ZPv7xP9rD29etv7lpaE9aarfoktjtdGy
+ x22GrPZpTYFOAyB6bke8Rmx8+MA7acf9mjkynmpQ5pi5U571jwSuA8O5NdrFqVCIlv+SNVUwV5
+ Qga5tCG/aFcJ1Ewdbtt0FgjwFp1zo6+/pq9/f67dvZrv2ed1VN89rt7BzYcHrpUtMqf43Bxb95
+ CZAx5J/XzHtlTnEBuVd2mQYkI7GOClR8AWXGS8eHL7GApAypEOK7wyLJMgh72Elw2yc/FXkIZY
+ 2hPnzy269c/tefPn1q/f0CwxxGMTuWV1VXrdru2UJP4Uao6OXCiB0IhTNAc7qAaB64WaqKLdJg
+ 62PsFk5ueoqlOUhNYBZA39ilzqOcpMpXII6SsqtFoMwSNrGKtD7Zx0YdeFJ3bOtwhd+4N+gRQA
+ HwLtCKkv1CB+ZxqKQ3nCPjJRsF9cwjErvwJykmmYn79WbY1S+kU6De7soPd8Ogen9cZAXw2jrK
+ kP1Mow7wekvU0FYyJ8CHWizI5DvBgXUkF8gI/0tCVFP5VNLwEbedoUsFNjcgdgB5qG7yhBkLF0
+ AiNidMB6x1mDsr8ENFiTddLjRAMqSYftp+OoTniTdbT46WJX3YzJb9/KaXEtxX8i7cXh5LOjZf
+ EH/nwE6eCqAyIIm6wv3pMeG36reTyJcf88tdeZT7lHZXQgxNwsPhghLZvO3t7trG1bWtrW7axv
+ mM7m9s2Nx3ZayeW7Td+63v21fffs6X2os1PpIiJz5wWT2qwmQXvyDoCF2akl6U153cWjUPen8M
+ DnMtO+gYq3nVS2Mb2uj16eN9ufXrNbnz4Y3t846YN97atOjm0dqNmyyvouoTypkXlTXdRHZgwW
+ 0IEhUcbtGD8GQNpyGimppKiSClzL2x6OWhGWhocNyL08IUXkopeRKOXBqMjYnX/edouO9hjfi5
+ NzsKHHgcyXDMnNpqMrLJQse5q1974xV+197/5b+z1M2/Y5h78jWBMV3Pn1akN2GIvKaU6dn2CF
+ 66LdBHGCha2AgH60RQTNE4UnLOlnsccaS2maCzbFXljoA4Jd0R0eSz92RUT8Znu7+3Ql+ju7dv
+ MMOnPPxyzUQjUxuqRVVtc7NIWF42QuI/RkVxQJ9q7hRjKPe0pbdRBpg5evbcyR63jqMnl9UCu4
+ mzUpIIohWQFfVmQfbQidsWJflZ3hkM95uEtdcguZQD9Qe+Aawt1oxYHp0Ne6a6pHGvp9cOsnif
+ q5mU9fdz/sBdOz8u3YYA9iaSZCDpjsflj/F4eGjqYU1McBVlyTQ36UrXvqFIUwB1UdpG1Z1LXu
+ DHh15QdSqmAHDQtaBxIlKARxr3ElJbQyAcQxkPi1HmnZNS8IPVDAHlx0UivC/onV/vkTUyuweM
+ Dj5qJOl6LBRNCQdrexhzHpMAp9gp+hJpgTxVJyTi4x+ul2blexM13GrMWT+nyz82zJfPDySdp5
+ dXxEf3QEVWidX9Id8e9/X1b296258/XbXNj2/a290BAW7tVte9891v27e98y06uHrPqBF1FHrW
+ m1MIjCqLjTPjHBzJLKb3ie7IPyPvDBaHvozmaS1DxSr1Jj5a3d+/ftbt37tqTO/fs2f07tvf4i
+ VXGPavPH1q7WbXl1UWaZ9Eju9WiNTGywLn5jAzy9JtR8EsctV9HWC8wklPEI75bYK9rVeQDIEp
+ DQFR0INVAsAdHT5sC3Hf3jYcJGbpvaTHsDp0TTWhCBMwCoGc32Gzt14/am1/5hn3rl/6tLR4/b
+ 8NJww5Gur/otZ2M+slbCN8/6EF949p5TmqSR32aS5vZAkPrHnNbxUPrVctrz3PQTEMfe47fHbL
+ GrCwT7fwE01gLDvagrNZePLEb16/Zs8ePOVCHc4RHhza3AGvpDsG+1WlTrYTMh8oiBnxq8GKQA
+ 8tz6NuzpqlwZhSFpp6GOIh0X3OlUeljptfNAXb2O3L7AxWM5d7ImQ9eG6OX/Pw8h+Ts7+6ycQq
+ HAqY1wcMdBVma6pGfd2O9sC12gI5+IqTDeS1BgafeKyib0rMKypPYrQbEBORM6rIquuvn9YqOu
+ NzOQbsp5Mqj/SThnN3e+ev6Bakm4tdJYYLbyvshk9d1+Hn+4w9+OMXGANjjV2jk1Q6fXpWnklw
+ JC3mSip5hPlZE0PQjjJQ2Jlw59cKf96p1bKcA+9gH+rqig1mwjxmWpfTXeefIEPSQ/BT1QmDUH
+ Qo2OSKFolIuvlo3jzeyIEnTgcbNx2az6IQQQz1ilyc2gJQhfYL9ga1tbtqz9XXb3tyx/e0Dmw5
+ HBmvS97561b7//V+xS2fOGQZmR4pWUEVfFOxnDZnKq0Vg78cr7wkiOk2mGowG9mz9iT189MTu3
+ n1oa8/W7WB7x/bXn1tv7alVhrvWqk1ssV217vIiPWywqRY7AH1Mw4GpmywF/AampqPwbc8pMPL
+ XPuGI0a8mCbgaJ6yEXfroEby4/8yYi6MRZbKnCVOiIGBABkqSg2Tw7y51mwTY+3xdZq3oLkUm2
+ mzYkVNn7O2vft3efP+bduzkeTus1K3XgxJpwKYnRK9YjphihRoBzNgIcgRwza3FoaSNW/jApzT
+ c1S2zYBjBycvUVqHBjvpKvh5TNsyN4JQLotfplNLfB/fu2K1bn9r2xgb3NoAc07gWalVbWl221
+ dVVa7Sa3F9UEvn142Dia8fUOE6DwuHq1hKZckTPzqe1eZG2GJOY2e5maJlTqdynNCcs9mKoXbj
+ zvdAL9RBYBbIGPvQc9wTB1N72Nqk8BKls6Ot0KCJBpzaoGjxjRvChKPKanXh6gFMB9kli6nFT0
+ R/gH2Dm64zsc22+Z52krCJodRIhnleuUdHznM1248Bgh1wRD6dWk+IwUeZBxMrmDkjpSAgsdVa
+ bVf7gBz+ahqE/3jqidxVSpM0OAAywTxF2ZpYlfFWRFa/Djk2ePEo3w4ky0SspisF3uzwzK9Cqy
+ SJIhljPKswElgt4HV/8UImln3xEsk5d0VGhxCnGHjKZzNK5SCv1LOIzzHoDFYoE8o2HGtA9mWj
+ T93tD20Vkv7lpL9bXbYtgv8+BFvDCOXP+hP3mb37X3n/zHWvM19LglJ8f2PPu8ObwY1BG6AAPf
+ ndeGnN42zx9/txu37tpDx88s/X1XRv0xgabysHuhvU3nthksGHdhtnyYs26ix1rd9WQs7S0wnS
+ Zk6i86zQvJFHG6NOeEl8fNY4Ae5c90rXJ52qF0kV+JgKjogAqOTAaSnCfKX30QigpneGQkTeiP
+ hqYeVGaaylF9ZpVy/FWWOcrJ6x79JitHj9mp86dt8tvvG0nX79og+GEzxC0JB4QGtPUNDW1Cus
+ LoDHkb4+6AVU5tF4uNp/APcva8sg3FUBhcp1FhNpwqeFGgUf+PF0CGYEJ/pFzSwGIE9vaXLeb1
+ 6/bwwd3OVcAFA7vx3DEqHf56BFG9qA8WEwmNSU6bMoOZI/snS6Nln05dPphFs+RMurg6wtNeKh
+ w1D1fgGWa3xrqOw/GhCkaWiNPxLBtgIV10+YWUAtSkxgO2+Gwb7vbu3awt0sgR5YJq2IUn0k9A
+ ezRXAf/Kcxz9UH14XdDqOeB5oPYw64jJNvpkhPCZ81hEdN4I1uuJGRU5S0unpHxEBEml36JetO
+ s4mhK4+0NXf28sqtibRS1TD5vfE6nkVIw5D0dKeJPAbtJjRORbES/lEH6dJf4XmAkObBkPFRoR
+ uMwCOpEIFx4YJQaprKQPDZBgL0AwXlxPzRe7gSLolZxGFGNw2jEsxFfQHlxI8AmIqmIzKP7Nr6
+ e7s1MOhoZTXyunMtXpKqCi3TKE4I9TcI2N+zFxrptbGxbb6dnk4HAfuVo2777vW/bd771LVtst
+ FLx6mcG+6B8fIhBAntXKIkeMQ7LxhzYBw8f2t179+zBoye2u7Nvwz4WW8UmAIfelg13n9lkf82
+ WGlM7sogN1bBWt2PLyyu0x4WeXuqTQmIY9zCBfT6gIrhF5yoZwbFozPI+XwvAA+rh88B+wqYm9
+ X+IqhH40zCNPyvFTCHVlfRXGc2cHYJ+hMQYQ6aPnrP28irtFWDMtnrsmF2+ctVOnDpt1VrL9g6
+ GttfrqxcAET5n38I4ECqVQ+vDKgHdmkNE+96g5cXMVKPwyCSXJxfurTKVK34p20wBklMAgRU5x
+ 8uf4z9oOhGyyiePH9qNTz+lHxMLrmNdH+5Jo9WyI8eO2fLKshrd5tRTEZw9JbJZq78oJF9F+Lo
+ DlKJ6tPKr8SxFqK7y4o9E12TKmJ3DnsUAvp9MviQbVIwVtuaLkIjWqszsOOO536eefn9vn81TM
+ FekPXYTA9PV2U/A5/NxahdOqozuC4CUBFNZuugYp6T9EA4aJ1E76QFEgKl6ZWABm/o06Uh3zP+
+ uEqIOsRzxee/c4CkCsvC8p44+0bdefM26rum779Y2FI3H3OMIDPKDxQPryn/8879kB22kSLFYc
+ zDTjcBiymgctzDIQRSRRfBE8bCUYYoPj1ESWjov+9CU3Clp8xkAEBp9PRwdDp4fkb528tkXUQL
+ MjLYoDq1M/uXLmNfl11hcf8E9xkHA1N8jm8SHOa2DryMCZBQ6RlFvRA78xea6vdhat421LRvsD
+ WzUH5pNR9bozNsvfvOr9v3v/oq9tnK0qPp/ac7eaZxSKJVF9qyvSPQHeegWvPYfP7bbd+7Ys2c
+ vbHcH/vDwOcFagJqjb+PBro0O1myyv27L1Yod6WDwSMM63UVbOQqddkcyOE/igroLMJLCQ1G5Q
+ N2LmNlBivvIhjQ2UUkFgp+BhTGnCaXubL0J1TbOzXNWrZuVQYkDKgL3nS6HMX3KDahEFyCaRzf
+ sgk3mF6zabFu93bHWymmrNRfJZyPBqDWrtnJk2S5eumRnz12w+YW2be4c2H6/zyHjWHvsEK4su
+ D3GPt8TQJsyG6ePVHhXal6icIJvJZDHcPkosCpLDpo3HGdzEAl+HFmOUnb87IQDSu7cvsXC7N7
+ ODt1YAfaoy2FtIvpdPX6cKqoKwB40DME+/Hxk3aHingYLFQ08AvuQWjLIYTYeB5Uz0BGdxlqO0
+ XpEKC7EdDjgvcloxywLT80Z9Rqk3lVbXlpiHRHPvt8D2PdsMOhLcutWGegChtsuePpQ+hDsvbB
+ LywROMBMlFJlSDA8PCidvqsopJz0LIWiKtONQSe1NmRomjm/BlTT8PMGLI10a+xCiZIKFnN7J/
+ i6WKKwmEnunUYTuhkmzy7CfzwMIfP2PfvQ3hEQ2oLiVa0TqOcjhYxbj4YpxYcGBp+YEP1Fi+EP
+ kNGG/UIBkkabp/Yph4HE7BBG+WRKd5PxkFgclT5ww9fe1px8pc9k6uJQdBEvDDYrPl40ai40Z1
+ 5sXoYuDIyIQvSHSfIIbWJDB0HZ2dxnZr22t2ebGlvV2BjY+gA302BbqE3vr6gX7re//ml06c57
+ RCT/tlwZ7UVzZUkp/jYAC7A0GrKgQ+8Du3btvz549sz743IECa1jWIuJgwXO8Z6ODTZvubdryw
+ tRWWlVbbDdsabVrS0dXxPlGtOERe0rpsyIlN65H8tLLe9bn3CbWXjROSfGiBhn41gvsgwZRXYT
+ 0jTdO4f3AzdN1EmA/gC2y6B0CInep1gx93+dqUto0O1Ztdwj2zdYRqzU6DGY4uKKG9TixpeVFO
+ 3n6tF25/KbNVRu22x/YxtYO6bBmo8nIi5Hm/i5N8AqzwCKzVV+3Q4QHCnrEyaApyUwifJF5oDa
+ wNkbQjKISlUW5l74fpAjGxjayzfU1u3njuj19/ITe/yi+IrrHwYm70V3q2uqxo9ZsoDiLg0/Fb
+ UpJfWKX5lC7bQaL5nEZoMJk5CYxBDJEB/jwNfLI0mPRjGYNWioDTB6AqionsCegqTiPW4SeHkh
+ 9EbGjngCQR1RPrGJCr+AEmR2VLrRekX9XUqPQrXiOYE8bd+eyGYkjI5qxPE81WL8PJdD3PSrMC
+ F2+gDwKr5KZKooP/r5kOVECfcjXzwAAIABJREFUOHesdTVTUaxlvqzvZOCrTCnRoZ5Np6zBX5N
+ ry8cd5roOyl7/81/97bTYbFIYvBLs3SwttRf7DSuklEVUrwsUl5m/nm6Q0pzk05K936yhWVGdK
+ CL7uLbS6MEZMkw+PAK/UP+I+XIzNe+6zQvJeKBIy2fpmVc96LT6/QYzcsXCZ2SvYtagP6T0khT
+ O9rptbW3bzvq+jdGfD8//+tROnVq17//qv7GvvvOetZvtojDM1/2iBdoM50unURHR94dD29jcI
+ XVz7/4DW9/YlGQNtM0h/LxJvrsJGLpD+zbub1tlf9e6laEtNSrW7TRs5eiyLR1dtflq1exQoRi
+ pjchyZiL4gnYod1unDACZBGSr7ExVVyrBvt9z7xnMbdUBkdQ5tCqQzwgKzKAoIB8e4LdrvlPES
+ ddEbHKMuqrbfL1jje6K1bvLNo/+gGqXQ1U0GhJmaohm0YQ1tXqzamfOnbVLV96wxeWjtrnVs+E
+ IA3owBQuHS896e/sCeo/WscSpQkozWLUfItstETbuHZUemYfzqbbkEVuK7r2QHTYmmvgk+eNw3
+ LeHj+7bzZs3bGttk1H9hCqcPrnuhfmKLa2s2OrRo1arNeTzb+Dr+zpAk5mhuGLNA/YxgtHtioa
+ zjGenbJe8f9ZF64EzXkPDWjxjyVUVADfKNl2rwjOt0LRDaYWfg5NqZxEZpGyoMYQFlBSo0pCCR
+ 6Q+8sa3pMqDaSO4e6eHFqrzVqU6xw+crKkz1SmyawzFU1I+uRGbegG8lseuZXHyCezxb9HkE5E
+ 9aZygoIs6QMqJ4jW9YJ0zLHwvP0yT1TYPNtfaRzjBOkdE/oU3jsiQilX+8Ed/owKtR0zhoSJPD
+ N2s4NzzQeFIlYIvjHjSYxENDEkbPpzvsqjVfUgitY3oWX8WKR7/K/OB1obQu8Uzibi9oHyK6FY
+ cXaTEKpp5YieA8tCeN3YOVFMZ7ONgiY2YiKNo9c8oHE1DUqcmCoxI6/cP9m1ja8tebGwwst/dO
+ rBhb0RKDIqc5eWmfePrX7Ff/uVfJJUD2wSV3jxySqe1D1OYwfMkuWMGj3RevfPcoB4VArwQfUE
+ a+Hxt3e4/eGT37z+07c1dpsHUrHsRk4uR7owYDKLofjIc2Nxwz2qjXevWD+3octNWjyxbp7vE3
+ t9oc4affkz6kdkTVDLeZJRdt9ZtUbiNZ6q5CFCFDBktk8Zh0VPWwTQ9c45eA9AV8Qvs8T0jZih
+ DNLYlvbfuG9cBovZqkxF9Y3HF6ovLtoDoHLYOhqlhYhcEXk4XYvPMTUldXbh0yS5eumLtzooNB
+ /Bg6VOpw0hz0Hc9jCJwdgS7rbGKaB6deYYT2bWWuyzA08rOCrOzgUZahzz0vDCKRkhKpiu2t79
+ rN29+avfv3bWDgwNG4ZRVQoJKm4SaLR9ZZUMVDOrw+njWMnUD3QMvH92IGEuY7wGuq+TH74KMb
+ I0GvZMUFH6dpPk869YBr/tBDAIIwtEyk3AD6MN0EZbEMR8WnxmF5shAmPFgyLyPOR2P4EslAzR
+ lg1IXMnSqYPC2XDKDo9elh9dP4ErujQMlj/h/YCFeI+izRMkk5ZKv/qxeWBzgBWAlXX7WkZ/ov
+ Zk6YVg64HXYo5G5wqYF52CYygmhlkozCYrQ4v9r70t/5M6u627tS2/shWR3k7NqNGONLDmQDSe
+ xEyP5M4MgnwM4sWUnQIA4tuVN/hMCG7Y1C3eyu7pr36uCc869773qIaWJlS8BRIEih2RXV73fe
+ +fde+6551b+8//4E4J9pNE+HVC2oHXYFcvvWb4U+WcdlX+asWcQr1JCqDHQnOKDw+ifWDQOn7J
+ /TdbTx1ZXG/Rd90p9iTi8LEcLzp4f0v9vt/HE32u0QDuA8jUcDDWQQxsPtwImsUdKeZeTLx9cY
+ mFdf0ywxNhw/Orj56AWQOv2TX9gr3s9cvYj8OLzFU28atWN7e+37buffWi/9+//rX1y+diartv
+ F96JXuPOEO7d8AZwumdemdpmasgxdmqAkAHzT5dKubnr21ZOn9uTpM7vtDWy9kFUvInM2r8DhE
+ rpq33Dwg+fhXa+ttphaZd6zg+bczk+6dnJyz9qtPavAL98psHLwsyL9tS3da+VuQREbN1Q6HhL
+ xM8iDfMYmmfkUvCwkgwt2JLPgGvQMo0gfHg67BKT3izXpm4UPNYm9QhCB2gna686RNfYPCfaVZ
+ gwlhzdRI7mDEnhjz2CsW61mbbhmHh7aw4tz++D99+3w4Jgqj5t+30bouN2Axxd9gK9do3OXM39
+ dieY1sQSG/gwVEcKP32mm2N8eyYTCTfOad/XrYQ2B/cYiY71ivd61/f3f/W97+fI5AQ9Zmlw5A
+ X5b2gecnJ6xTwLAF5duTNnC+jPUiMEtfmGJ0lGznCwtdG4Y8VLhFllonHad7dz1nKkbrUFB/dR
+ x7mQvDJzBOaSd+gpjJrH2rpmHQd8Kz1jjKZE94z3Vak39m3rdAPaM/BdyAyj7G/AecdlBeqo5G
+ 8FE5Pm6unFDGegGc15LAP2DvRDvPcBeCZAatnLgmYqFxWl1l9JQLaYidF4LrCXDtMBnZxr4tvB
+ n5bohUvUggvsVgVrUzzzC12Py+QR4XRRoA+yBE0F7qOCBAdGZ4wqw5y3H4gd09nkTwuPllwN7O
+ dilQQ0J3N1edgfsd6Pw4DITKPtsTj4AgrhkYyzGvhXsweeVWUPOMFJ6focuSiBG2gha8624ZKp
+ CVEi67Q/sqndjvRtE9mOCPVJruMq2u027fPzAfu/f/a798JPvWqfV1kNNvL3nOeGtHXN7d7aQh
+ ojXGY35ABD3CcLwkfF0Zq+uruyLr76216/fkMaB5hpbk7I0HBpOwtWvmAlKe+M0PWhj1eXUbHZ
+ je9WZXZx27fTkkDN02Z7vjoQB9nr7KvpJ+ROt+GH3q8KbItMoWjoPjciNAycE9ozsKaEER+8/v
+ VErLIips19XqMyh1j3ku161JNA3WtboHFpz/8hq4OZbHdvAxtlVXLVKU6k4f6rayUIfQKFesy5
+ 6CdCZ2UHn6bHdPz+343vH9MsZDIY2mS3Y5BXqj8VszmfINv0i8g0AVGzi5T6fHcC1dPdKXtRR1
+ OT8CPjzFIFS6lZF5I6B9eoIfv78mf0TKJxezzboewi1Emtxxka4k9NTO7x36Pbkfhm4eyczX0a
+ QqtswgnZQZVAHitKHpes9huKm7EJV9BUeRbSpCK10RL2UyvqzZ1aCGbDOsbuiBWuFCwlSUc3aM
+ NVlOOoxNw+BjoKzJUzuIvKPjDAum+jCxesgowlVoewmyktUNRLRLbmYSv+qYjYHFT60llY2WKp
+ 4AphJIRZnWb0EuQErxB4lxcnid2QbIQf3HcRulPARKprcovYT1g5CLQ/AdySsGDj+4//OSVWKH
+ qNhSsUMDHCOaEX7s7AEdcOsfHNAtlxjp9s/N7JHZK2u13J0mXr8I7KP91GmuNkTh1uygMKCYOL
+ hChmZy0b5L4OTrCawjxcowVz1tEwRlXjLB1upyXOGESh0zXOm+rejvl33bim9HN6ObDFb2mq+N
+ GRG7W7DTk6O7F/+zm/Zb//wh3Z8dOijGmHfUHRV/gKwjw1GuKYXvcb2oXD5/NUr+/Krp/b1189
+ o9MWWeRZNZECliyW6abPegPGOc4XooN3Obm2vOrXLU9gaH9CHX1bSKiAyKnObXPmiZNsDNjy5I
+ 2QuYnonK2kjXQjB1U8nE67dkmAvQzNZCHsGCgVOKHLYtl+3JQ5KeNQ4YFdI3bSs1t7jlKp6Z9+
+ s0WajjS63LSNCzLCFbJfNN1RsYO83rN5oWb0Jo7euOyfCALBme/tdOz07s5PTQ9aexmigG47Ee
+ WPduMY6D9Fgxb3khzDIyqA7uGOZVLqXShT+0pmTDj/JIGM+LH2h1larbGjP8bMvfmZff/klFTn
+ JktnXHVt3f+/Ajk+P2SeBJQrHztJVlDvcG9iC3uVeYeZI7WUKRhjY+XtRg4/sB5Sl6VmJq5GSJ
+ a+BOpkFbRqcRDuDsPWFfYkPBeeEM/YPIJBSL0BqrNpurdXsaPpUq8Xnib1CmWkMd8cJJ8MgLSD
+ pWrIWst7mlDgvECur1/vQ/RTFBvUfqbDr1b+glwuwFyYloU/6vDkAjdfVOklAUM7vLbFLQV8gT
+ oB9rHcY16VCMbuK8d6jOutY4FJZsrr/6ff/EELT5DeBTR+gTglTyHgKwAFQSd6zKyf7fwP26qp
+ LCSEXXIXP3UXLFfw84erOYqWv8AeIaMn1+5IxCexZ5PLmL+51f5ny1o1LJhdFMtzzgCPTWVfU2
+ k+fEQzGntlwPGBUf3V9a4Pboc3Gc4IYrFnbXSgNDuzzX//Mfue3fmSPL86d892IUqL+XJLDsnC
+ cLqPyxgmXTGitNxubzGf25uqaEf3TJy+sfzty46TwltdnRhOY5rB6IUun3esG7uKwnltlcWt7t
+ Zk9Ao3TdR9+pCdVFeZqlXoCNnL+xeCOAPIsw4xZw4r8NecV9gfIiOacpkQLD0jrAPaI7kNiSZd
+ JXWTMovC1hrGCaBJDxuLFeQBto2WVZsfq3SMCfaXVtW0V9IXqBtjCmIuMQiV5WboiCggwn7TR3
+ rNWq0sHRc3QBSjhUEmeeXZ2zH6DarXD5zwajFhUxkEO1UfIT9WcqB0XtSiuNKdH5nZ6KIZ0V/m
+ fucxYX65ajraEbA1QOqtV1nbdu7Z/+Id/tBfPn6lJCs8ATqAr73ytYjrVgd07PrLOHkwOA+ylx
+ FFHswsowqqCA9rRwJZBCWBfOnXEcA3uT/LbUscwc4tsh3+w2ygVexqfK5qDmPmwuL7hcwFXr7k
+ ZeLYoNLs8NAbLbLaM/KGMwhwFrBibwyi9VUMe+4Kc3kDAQaGoN/JpjRWlq0YpHT6fUFJABSWKq
+ By8ng4dr2VAlTdllVOu8pMuD2hWNIWzJmEmUcF+iRa9dWXQS2YCz6GgckiSO1DGZxA95RlHdD+
+ TOjOr/Mff/4Mt9KlIU9lkEbccwS9Hs6GqyVQO3PbE3akyjeHBvzyNI7AV2nKZS77zjlKojO5JK
+ vnq5UKvr6aHF8niIGicRJcowlBw5fyiZzvJUS4q35GKFc+REZtbNnPCUpIBzm0wVmR/dd2jZcJ
+ sPFPbekVcMOR9H3z4nv32j35gn378EfXEpAAQKbmq5F1gv7OVUJitVukTM1rMrT8Y2svXV/bVV
+ 0/t6s01Mwo+Lqf6FJFjzZRSY8RbRDTOJbmZFcK0pVVXA9urze3yqGPHnbY1WVir2hpNOVCueM1
+ C9sTuVZO80V0fT9MwRXs8fz4sZOHmYgH2iOpRiAPYR3SvIeMyIFMhV/JKRpCYIauOFr0uZ4o2z
+ Vpds9aeNfcPqcDZ1uFi6VEqL1A4HmOOKfTZohKQ4sMpkSAPHX4bY/talP7RJ8bPBUAfEeXh4b4
+ dHZ7weU3nU7vt3TCLwWUN3T5oFka4RcaZm6UUzYNS4v/QMenGgnmAtMsvoyYTQ0FUlLA6PInWS
+ 3v2AjOB/9F6b67kOspiKp6DOHK8X/RFHN07tGYHTqt6FqUKh/Uen4xELyJ0HLsjJqN6fk8H+yR
+ dVoaUqI+gYfzzhsBC3aIxfcrxlNGx1zpKx9wNDBlrAvtmS6DK6WXuEun7Cq/ZbLSs3dKlgD2N9
+ y36T06lMEwDtoEq4iQyZor4uWCPAN4BXQO86QpTyfR8nErzJxcBcOp/YMaCM+c0TvLDD/zYjcx
+ 1sefIPorzKWsquPU416Kh4nLx9Y+aB72dXBmVglX35FexL2VKwtSNZtDiFsWCxFizBLasdkcEq
+ MJNzKAV6HurbwDzL1ugvetJ77QRaxPFMAVF2VHU0gdjesUDLIFlpnbCcgE39+40K25sj2O9KL+
+ Ln55mpXNadNbtKojMtmwfhvMllBiKQhCd3g77dnVzbdcA+97ApuOZrReIKjXeEaZUl4/O7Tc+/
+ 8R+8P1fU6NSXLgho3pHZL8T2CPNhaEZ5JXDid2OJtYfjOzF85d29erKlpMZC8MsqHkmItTlUeV
+ w8pD3cW3JZULuhr9bWm01tr36zB7uNe203bZ2rUagXyGyr6ytRhWQNpXSf6k7IpqP56cRdtJO4
+ fAzEvPDi99PpzOuG5wmZ4jQvPNztYSjKBQ7AE+P6J33XfhIV17WBO2GWaNj1c6+Vdv71ugesEC
+ LWDgkgviIiNQA7ojmedibDWs1mtZodaxNoEdUj4g+mnVqCKMF+OTS6+zsPDjocuoTMltQUJPR2
+ OcKo2FMfQEeCPrXem0ogoSYvuRgIKfJbJcQ7KGKsipOS/knsF9MR/bll1/al1Dh4HsXMsiwHm7
+ Ua/QzOjzat2a7QaXVwoewhGOniqeKyAPspQzLRVXJPD2r9zoPd5AXNknhuRImB1dhPufv2yM5Z
+ pZexFb9MeYACIBhbIa6iaiOPM4P5zx8kpp1zTcGfkldhMlhMUUMU7kavAya7Waigri3QBOu4Qe
+ mAjMje86hRe0OjVdhh6JoGpex3ANUrI7IXhe06ipS+bgM003b8uD4yBZ80laoBCOPpl3zboG25
+ CmIbU6lBeMQZyoEHNFRm6wqBJEpzqBdQkwWigYFgb0in1KmhMNI1UF0qfF0Befh0ck/W42DHbD
+ bABVZBG8mL+pkkA2w1+ehqrnQzbqUlngYTVfvBnu/KHwyUsm7ldVxFln8xk+A7Ihbaahag94OF
+ GChIEGB9mbQozdOr3dr/V7fZuOFrZfqGoTXUHevY5eXF/a9T9+3f/GDz+305DQVcvDwJaf0TOc
+ dNYO4uSeLhQ0mMxtM5jZdrW262Njrl6/t5ZNnNh6MrAqZnptXUXeLSBVUDLLslfuEIKWlg6WsB
+ ZCvweSsvp5YpzKzB52a3e90rF2vUoWyrK1pJlXL/R8ui5SkL7h6bkwGhorQqNiJWa7ubQOeFWC
+ PLsk4kKgzQHoJvh4CF4K9R6yMFlG4c/mouidhedCyaqdr1c6h1TuHVmlBYim1FL63ulpx+aNYh
+ wIuGmtr1mi2FSUC6DsYswh//jojOHD16rDV9CpOhapUGfE3muioBejv00lyPkYr/4iXVvIGcsd
+ WnqdIW1Mh3TOGYrhHAnsPXnT5ar5sHHId7rWNBz372T/9zJ6/fG7LmTz+2WTkFhZ4LXwWGNghu
+ EAtbrmCEybcOjVghTJNGs35/NyQWCL7YpYgKsfvJa/5KFjItT6BbXD1nG0QhcqUsevcRvWMOXW
+ oSLSRRQvWa2o09IaicEJNg1N8bgEj93qTBdrIAEj9cZgNvO1bUuGgJwRHicqtBesayAASfjmdh
+ H6LmLKWZNBuDlfy9jz2nOKpxirRRTmgxF+Hp1MEZepydYWWdKN6T4ySPPouEF7MbCrxMzNPM2m
+ 9e5nAD4o72SdkA7roMwnbhcrv/88/38aorwB5WRnokYhf8ilH3i2pmbHup+12w7xA8vtS01QyS
+ PJUxIuOqOjL6s45K1d/6E151BPprIfoodyIhQvc400bF5g3OogT5S4SFZRWWxdSFEWoGHFlA/X
+ 3QZ94VKWCUtYFR2pEAA7JVURgqZNPNM5suaDmtz8Y2PX1jfWuezboD206hspkyegdTR4wcHrw4
+ IF99PF79uvf/9w+uLywDkCFbeRYJm/GcYoJcken1SVl5R1TtcFsYaPJlNpvnPMVG3u29urVa3v
+ +7Lldvbli5ACwBdhF5ELA98uffB8ilVCkeMGK0c9mZS1b2HFzZafNre3VoNlZ2xKzigF63slJJ
+ U7yCJLFMXsPCktjZRc+KxYaeY/UAY6YHwqNODxPwuOGRTd2yYrb55CSpEkGwIA7BW3StGoTYN9
+ RRI/IvrlnFRx0/5zK0nSiOIwa1GWzTUkeeHoYacE/ptXssmmsgkEXgHYOUUfWUBfYeycmfl8jl
+ YVuzyYVL/RxWa/ZZwFlEUAlpiuF8ZZAUI1+8OMJb/QIrsoCIakQv/BLOkP8+Mp6b17YF198YTe
+ 3PWVVLGjLY16RuN4bonqMkGQDFoCeRUzviYhn5KZzkc2qMBs20epCCElo0B0hVWVgRapO4RXen
+ 1QpopXKQmiSSftzYVRPubEHk8i2cNEGncr3QFVBUnnhe6DADqAHnUNxAgfXSG6K1aX6Bhey6+u
+ xflTKTdWFq9pKUGVuk46n4ZF6WDwntYt7+BALXL2FM6SLV7VBPhevscRz464ruqLztK9c98z/1
+ kPyu1LMuDC9gznMB7n+yRkgoZ1v9aLW+Qd/+lepJKDKrjrDtLcC0CX92qV3sgkQNruASS+sdJ5
+ TLOAX5VbFiAoQXSkNDUDGrwAN3xP+9eX4MiG17pko2+pazXbHPtLQDw+jIr790gPfi7Ae6YeGn
+ FFNaFt5u7v+1l8jpUw+6kMgnz97HE42VHkUhAO3WM5tCme+wch617ekcYaQ6Y2mbBpSZF+3vU7
+ b7t+/b48+eM8+/95n9p333rcDzAWlykLdiTTfSn5X0MKLjkJhD1LD+XptN6O5TTFkA9F7TADab
+ lm4e/HilT1//kJNMzhQ0NNHRubFKSopfNMCGKUb9pmfvtYNW9tBbW7H1antVeCvsLZVtWEb0CG
+ UfeoZBdiX9EBIMPEr8wXviIQzJZqioJ+ewLJ2NM5gTwpHVBAnT+EQU7Lp6+JKoi2au5B6Q3nT6
+ lgVQN3as3q7S5uDKmidGCTtLfJksKDtBqi0DqzRwvxczFcG2GvKkZwTJQlE9y3onPBXCa8V9Z9
+ obwEEERUCUPH1iHIB9uPhSPyw87x6mM7HbnHpy68l88LO7XIPyyBMdjJe0KY1w4bvf72c2YsXT
+ +3Jk6/4vQLsxZGLD0ddsdVq2MEhhnogkJBqRVmHbB60xksWdxEpMljyiDuoGZ0FPb8Av+hUZxC
+ FfeoFVtGiCooiaIqsmJdXODvWs90BPxPUMjXNMKZthAMKz6lfCNxfLkXFxYoMGeuNZ6LLULWIK
+ NCSuWjqgsbmmS3UtIcssVnDgJMa1W94RvJdkuHdDoC7MpFy2uj6xfoE0Pt5SROw0nuPSNOb0O7
+ Qc+nSZ6QeGvuc/cQ6OZwJC53SwrPjKnsGEL/Gd5RNVuB2xSo//tO/Cn2KOkxD9B8XAjehXPnCo
+ 0T0zlvA3l3ksMlkKQuwF+fF0WJOSRBceRN5cI8UlWl9XBbZe0e8n6c5d8A+a+h1ENSiXjZefVO
+ Lr0xA/J+KV9CDa3koJgz5GBfCLZs9qmIaW/hhlxQXvjUVJc5ZL9finIfDMcG+1+tJkz0ck6rAw
+ Ydj5F6nY2dnZ3b++NI++/S79skH73P0XwMdqXFJh5yLF4qWA+8TZMh4MaNB13i2ocUyJJHq4lT
+ kPBqP7M2ba3vy5IlNxxPbLFe6rLxIESBPCSKLiuCl2a3inxWvJx6zYRvr2sT2NkPrbMZWx4A/R
+ jHeXu/hoOgAHbb4mTepQI+UmvuTg76ZzKZ0CYVvD0fMwaZ4NudFAL4eUT0VOP68Ui8GLjYAfb1
+ pNUToLQB9l1p6gD6jfe8Wjb0RFAIAHECBDKDhyo82Ivt2l5E+UFySQjQWKqKPn4wIE09bUqNba
+ 7br4okpBzSCPZ4DAFaBgk8K825SKYJ+Dtg7uGCbclIWsiSc1WrVJuOhPX/2pb18idGDU172aC6
+ SssZVdvUKefqDgw6zDtpD0xohK1wYoFA2jJ6BaAjLXjzhqpgCNUU9ns5ro94F+4KB8D2rWkeiS
+ XFWG96M6Gor/J1oYu3HUPyV/DSzOzSNIXKHc6mDPWkjNjaqkB/+PXy9htRWeB3Qgmjewzo2K5m
+ 2wb6gDJX1Jh+i48Vncvqk+houi3Ydfg2SbTRc+ajDGGQess2M9W5u53R3SiY8w9sB+/xFu42iX
+ itJ/Sui24vrJP021XlypGyVP/yzvxJlREDT4AJF4Pqz4A6D7shc/jfBnoNU9d4LfhG3cPDp8V4
+ K8yNGELp9SrDPqaK+xq+BnQ+myMELWTtgz2PtI9OiDpAr4fxed4Aogb2/nqiMDGI5q8mSz6gfC
+ MhAJ3iKuUYn54KbCjNNb3t96/VuSONAnocUEosEgO12Onb//pk9uDy373z8kX33ow+oY29yBkB
+ YbLi3P4ug4pvVGbuwIZu3ZrbeSAlFWSyapKBeWa9VN7gd2Ivnz+3m5sZW07k6HnmZVBi9QnLWp
+ H215nOCx9+63pyFyApArmn1ytaa64m1FjfWXg2taTPNPOBT0fi6AHVeom/xyGGTjRebsF6IJuH
+ Zg88AkB8Ohx7dT9kXQICnb87GC7lYY2+8cysEq7cY1dfRgEOw77AgW63jpxds2YWaSEJeUKBlo
+ LxpdvepuoGMD/NL8StASO3qUtVksHcdvtM4iv7u7O8K3BrrfC2APiJ8UFLg8TUkyD3vPegB36+
+ oV5lULs5qz0aAQd0TgVqyS+yF25sre/rkC7u56dG/n1JP6PxZgJbCCBx9q12n7UMDkbRTNspAs
+ 5yR9MdyzuiWgY0bvHiSH35y6TSy2z6TpMUsVKc+vfio07hrYxxnF+lr9Fkw6/MLFs8mK3misKv
+ XZX/AQmoaDM7hMwR9x4FLTh+7lQYpyOifYAEXew5qnTmDUAQxvAwQ9ZcjNcMBoKCw8N5QR2Amx
+ i0ffRnedKWcJz3DHHU7hnmWpj8XPgW6EZOcuy9ALrEpu3/mfUdeYyz/jmudLpp4Dl7z+4M//ct
+ tSHxwkcIioeLdoNG1F6Zl8aLviuzJO3i0gq+RdjfeWFTsM3Sn14OiAxujiOw12xIHzlOa8gLzL
+ 4zIPg1Zufvho6AaficqO/tBye8r3gcj+wB7nbI0MDmum5I/3VlkeKSnFEuj8qB2gGc87AkA9v3
+ +gM0308mUITrkZRjufv9MYP/Bhx/Ypx9/aA9Pjq3lrfQAkjTMmU8SQF/hJKYRgB7cK1LrrdJeR
+ kPMWuSHM53PbTie2utXr+z69RtSJRHZ49/KK6RmLVIUVYK/QQGBgmTw0qAwOGClYrXVzOqza2s
+ ub63N6F5Fe9hFBNjL1TJH9dgDAU68rNz5En8eYA9rCUX2Q1Jf48TZi3+mhTGdHkXpScWhoikkl
+ qBr0AAF6gY2dtlbAAAgAElEQVS/grsnfQNKoIGDrIMoWYyiMBVXm9buHhDsARhU3yATIJWl6B2
+ Tj6TD1jyHxOdGE2I6vMLBTWUl9QYAv4XIs0lVCC75wfDWZpMpD7EKyuGxrt4VPhsv4hIQSZFmX
+ xbSLijK4EJfruz16xcEezRVRYEVkT3eB4qNrDUA7Ft1NvHBDI1OmN7boLmzPv7Rh6gTSFngdQG
+ GWxOTUOKs4tSkoj4BZvJO/ZQiAjeuY+MSsxNNVtKEp1C7wC0WMk/PykGteWQfgUNE91Gr4tnCe
+ 0Qmi2jbI3tSP15vDPDUJRP6fwVAMNtjBoPuXfQ1OG9Prt27hyki8BogaCyuU9gpo+OXe0/7jzN
+ x8XsH+6i3iEXIFx+VOlGgJ0W6C/ZJCZ6vgMKeIa5MFXIzNmcUEsaHq29Q8d7Mh7/6L3/yF7RLw
+ OJAnhWHP2gRXQResPXI7efSOMHZU7IbnL2aQUR7BNjH2EE8DPm5oCkpQCH71ueCQ3yslHH455f
+ cScWudIEkWWV+TVb5mXlGwbmsATiNU4B9LpYVRY+gdO4+GXr6YONKZ46mFoD9ZAzttYP9YGDj4
+ ZgzM7Gu0GK3W007Oz21h48u7fF7j6m1v3xwal2AmNc2yFESRNBcYhyWAYklqA1iJ4tB+aFjDwE
+ Q4Qo5W8xtPJ2TRnr94qUN6XEOR0cVoVS8alDRUGPDUJ1gD5AUqEXXNMC8bpXN0mrTG6vPetZeD
+ ay1Xai2AEfJyL/eAfYxZo6FOOcSI7ocTSZ2e3trNze3GkoxRYF7QX8cfM4lIyx9B5Z33dKgDsU
+ GvOibbavB/6YJ+qbpxdUm1TM4iOKUGY4pYqdssmVNRPId2CHIg4V6eqe0SN8QTLA2AHvZJ4DW4
+ sUafG3su+iyZsE2isCIBhuUBwKMoAS57fdsxgzLrN5spuKsqE3vQmUDYIzNk2KK5nSgJ5zbRaH
+ x+fOn9vL5E1usMD7RrY+5UHiWDva1ijVaNQI+5dLRn0DKwsdA+shHACF6G8jnQ8eNfcWmTBXum
+ R+JCdVZZYCj38dlxQ/GP5OSR8SFOy+6FUGAPT6LJryJVSAGuQIKa0FschqXkTcDmRWb7RDMYH8
+ iQ8YFr/GF/r2SJXpuMkIQAssFXnbR77H1WlVhxIaziwIvvi2VOfWaGv6gzcdFCpYTXbg0VnPpr
+ tsnaxJfyGZFu+QgWUEG18r7NVST0P+VxVxedLxrhZ1e/vC6WHaPFQx55O621GHoGK+XWAmBPfg
+ 7NDJUyemBy9TGURWeqVFw7q50eTtn7/bFAO2o4nuBNr5eqoTdCjTAXkUbgbD+reiKoEruLkQsI
+ F+uKJiG/YEuK0FDLAijKU0rSJdKLHyocVJkzygrONs8HqykJrh/o6C7lSPeW8H+Zig1zmBIsEd
+ kj4sQa9FqwDLhxB4+urCLy0v77Dsf2/sXD60DgPKCOVNPeNFv1jaB2RMHcHsByf0yKGYUf+aKI
+ 6WsM4wXXGCQypDe9f3rHrXrCeypZGjyZ4A9bAa26CpllIU189QQ3Digdja06vjamrMba29nBuY
+ HjUxx/FmEpyInywTjEud+oh1wWCTgPYLuGgnse7c2Hk84cQrvm0PDl5pHC32SgF7jBKPT1fbuo
+ aXV6gDURtMq4FXpayN9PKkpwZSyRapwGpxShbGKrTZklvBgwTg7aa3D+E/fw1/HI04qRpzi4a8
+ cfyeba+0HHcb0367UkASwTltkqI7ojJmKa+rQVGTvhK7ISPlQARTDzdGL9VD7PH/6tb1584LAp
+ 3WXFYF04W76BbBvVK3RRBSLA6lnE1E9a03eG4FaCwBN4xdlM5wKgKSawuvGn3bqr3DbAY/Yic/
+ UjTs1nF/IxRmK7pHTM2iBa6t7zqciqKuQiAcu0lDxP+w3kFVqzgYuZAKzWxpgYYMS496rqEYXB
+ Wm8jpIUFcRS/QUZ01ZafQbAzBqk0c8dvKjNYW2QvSmzYDNe7DfPznL45ZdhjF/1ukUEvpm+2eX
+ fo3gbZydYBdHQvsd2MgdhYYhMMj0vdqTyX//XX6bInjdrQzerikFhUbsbXb8rss/DhlWgxaaSZ
+ 3yuCBNk3Kki0nuCPQ4yO/a0sXk0I2KHtWiED7zt8qDzAGfq6llrKAoeXsG/67Wzm2EQ7pPCX/I
+ o8YuIvJM5lqek5Ew5i1PhjDhGH27sPub0cUHTChQmk6ndBtgPocaZMI3XYOetNet1Oz4+tgeXF
+ 3Z+eW6/9vHH9uHlpXXZSStuFmnudL2yISiZ2VwRULNtLYAvnhHAsFQ9OJcKgJy7NwpkjQB7dFi
+ iUKvUVkoQURcta7DdHRQGhefuRAhuX7SBgfLBhl1MzIZXVhtfW3c1tQbc0+p5j+hCXIl+umNzT
+ IkdVEGRAS1XNiK9MbR+v8+6xngix0socRYw86KcD+6QiM5pRSlXVoAnOie7xwR7gDvkklVq4hF
+ xQToJOkP8r2gcTSvC5wXQd9pd2iIE58vnDSqTI2rrPMA1KDncG4UHycEbxXDuljDO4oWLwmk0/
+ SnmStw/6gQ092pSigiJ6Xw6c0leWP1qvGZaTVf54HUh2aVIwunJ4XBgz55+bbe9N7I1YICG2bj
+ a08hEkFXgEocrZqPpjZAO7gR0FymUVgQ0MUPUD+6fPUS5/4Vgz+hZtJNYuSKTKWy6JVsWhUOvp
+ 5CQUiWizAgwAekt3oe6jqWMUaYQptkM2RhMBdDH8BakP5TQBtjz+4RHj3vV+GLGhaZOXilfNpy
+ Q5lWn5H+jugCZAM80ohcAzw2XIWTVeN+g+7DGuBAg/5REt2yKK7Ez12O0HwMXM1//DUXNNxiE3
+ GCaqFLn+rMqJwIEzxg8+K9AeqkPK8VL6K9DX7D7vRQlBycVhYbwz5EuX30KTO/4suKXmIncYWR
+ ytC6qqAT0nPqEtEgbOMBDsyOzOVFE6MoM8qALvU5wHHleZKaTCNmenqpazz+JNN2TJL6uU5i+U
+ OkD8bFRKSNLZ1b8MRh5BcCCM+KAOncMHYfkC9QOOEdsZUTUx/fu2YPzh/b48WP76P336ZFzdIB
+ JQvCNAe++sN5oalNE9Oi8bdStjYgcvDI27nJja5d66bLT4Ugj+9w4DA6c172e9W/7vEBwmKhTb
+ qgLMXl3u8dJ8Nz43FDPRBZTRSY4ubVK/5XtL4fWqi1swwKXDg3Xygu0jO4LrXR6Pp5So3CJ9aE
+ sdTJhcRY0zmS6sDmsJ9g4JRdFvhbADwVNvN/WPguyaJ6C6obuiQA3ZiXiVBEA4ACyV8SVNSyet
+ lA8RVSPjKDtg00AQF6AdT6dkT0B0/n1VECtUgJLZYnSS1ebqTyQMlItiK+LOtAJDLjMMV1qubL
+ pZCRJIy5vvaCsGSJbY8Ed0eZc2nAqWDZ2dXVlL1+8tOloIgUJ11wBAhQiop6MOn4AUqOuqUi4i
+ KGxp9QyRflZhinuXx41fA8uPGDWmPpKQt4cJ9Wjx+KQq59FFG1kyeoNyJk91iOfaWnspcLRjyQ
+ QYaYuAFZNSpO6qN/3wUqUWCJIiWw7yRJlr87OYs+WcEKoRkIXse9RvUd/rzFr2/+MsyY4zlARP
+ vpZcIbxdhrMgF2tU2/wMgfgM+AN2ip1STs1ok/nn7IspO5SP1HkDXYiUzsuRXWMlWxTny6o13Q
+ Wgy35wz/7awd735QprUzrXWy6/GcRKYlnDFVMyZo7zMbBT/RMeoxxBgTHHoXvftesXw6uK0WKX
+ uDKG8NvyUTf6DXvbpqdAuydbxZOdLHJkuwzn9eiih5UkL+Igz0Tmo2aVuTDrsHj11eaVgUKB9E
+ 9ZIXARkSOGP58fnFpFxcXdnH+0B7ev28nx5ieVOGFgQLrZIEsqUoZYbNRs1azZixHUZ6ABidXQ
+ fmlzSiPnu+wwVWaDu7+5hYy0BtKGoFKjFxrNdoEiLaJgQ2K3lAnYOOPT/Ly02/b6cis/8K6856
+ 1KcGM0xlhQlADWZWzq85R0QtqIQD9AMXr0YhgD494gD186hkkpMY6H8qCCKqzZ/XOAY3OamyAU
+ hoPvh3FZHD1NKfirzI6A/UDOwXwrFTetDpuh9DU37Eg626vlFnqogBQskDrqo1IEkADEKyc5so
+ qmru72Cm2ovlF1EODFNlygYEbPuCPMwLc7pjSXwqF3TYbHbkLfh2kh69evuS+0ixWZFKSpgLIE
+ tizLlMjFw5QgmJuu5E3jDqTQ2/vUsPkQKroVxd3LggSfIIXV2xaVMp0XksyQn+fFWxxXhUAcEe
+ nzIDJechddyJDzVwg08DisIBONCE4dM3dCLAPWwPemw6p2nv58tCxQaPePIlIAnqZicQcD39m6
+ lfQJYsmObja0gYjFHCMgpX1IHBinYfUpksyKa2LorR/p5Celwt2l5aJIML/XB39YT8RDWvuO7Q
+ D9jFw3SeI4TL48Z//tSh+b9f1+3QnCs8ReBll62ZSoSEiG3web7DyF2Wky4IReK6c3uqvy8KnB
+ OS6wfKfJ3B2WucuLZC8spMSJpQfYSKkrAVcrTbBrva+PJbZTA3Rgrt/FlsXhd0sl9L7B53D74j
+ RfkJCbl7wqwtQOZhYNR5T9ggJJoqzUOjMpyjSGiWPh0dHdn5xYefnF+TvT47v2dHRPg8pDi9AG
+ vNC4c1ObrCBze19Cr5eEfGkUXzRZUr9smovAP7BYMRxhJA6siqCCNBBX01GskpVD1ItcfFkvT0
+ qY4S3mNq2/8IaozfW3s7pfokfTLxdsRGcfTTtxIUeU5YANlCogK8H2JPGoc5+YtM5gEsWAazJQ
+ FbHbVajhr7ZPbBa99AqoGswgQoFZldH1EFF8fcaRcjInp2u0ONDnYL2eilvFLWrToVKs2gE7RV
+ ExJTboeAbNE4MvQCd4R3fiuyddijsDdLeCsmsyyjl6apoEJkVLm956LiHvHvNkB50UELwgCyIP
+ DIKvbOZvXj1ghYc4P5pD0A3SHWOcsocteEedVYrpAxrTTwhzA2YSW9OR1H54keGH121gQk5Es/
+ BV878s9okn6X8Z5popzOdI063S3BwSszC3X+T1HUxDEXvUZy113yYoSojQODCLmc+K6nDAruC5
+ /ZCis9zXZNuZXNSMfaPZF9keDGwCfuBAgkZrUH4gCCKijZ8Rl4+MgDEGW031VgXQUb0F/jAEKf
+ 3Sqrjm+tY0jKBjQH2+dl4Jh028KhhuHw+zmucu8qP//xvIu/3oSFe2fXyby5ulg86gD53hVFkx
+ M2cb3qO6gtjJAf7b8Y8+pN8oez+PoO90p/dAulu6pglftktLu52SefChCiq5e+IwPwwlpFapOV
+ 3wT42n7pa1RJNSSH912WGBrAHkPWubxjVA/A1Ng4Ht2GHBwcciAHAPz29bwdHh7a/B3VInQZrK
+ LjBcx2ARAVNHXJAReAM5GmipBs70zjij+V74lFRpWKT6YzvZTSeuP86IF3zC1AwYzrqo9h04bn
+ lK/IK15Mj2q5tF7a5fWU2eGHt9YQDNKSTISEruiAKf97VmA4oteLKelDTAI3Tvx2Qtx+AxoE3z
+ hKRpp45d1RMS4PqAsMqugfyp4ckFGDtEjgBu4BfToYo1qpgi4uBERe96iGHxOWJz61iNHsOHOg
+ J+n7gI7Jn80xMD0qzX0EPBdhHw4xnfdmom9GtGsFE5TBT9f9uIxJsoUYDIz3Z8MboPfZMsPlJy
+ iRE2fiskNC+fv1K3bnu7S8nUGQImIQW+13NwwAANPGhSIvOZ0xzigJtnlQlMI2h8HE6dunVXT7
+ 4bkZe4gW+XgFWqBgzFeinXplAongjEMvnMoafRCSv81aMZYwBNK66km4eP11qm5iSGJKuwFTKM
+ ID9Ihm/MWmIWhzNzUQpZVdMSDPlrQPAR78DXo2zP+C4ucYMZF3IoAgx7EbKokLo4WmhajllYPu
+ LwF6BZNDJtDp2+l010ag9xKUqGwixuor8K3/0k7/x4SW7zpK5Ah/Cnm96qgeVkxQEauIvmCh3s
+ nMXvrsdriXU4o2LErs7rrAs0gjYcnSvi0nRfclxBkfqOUKRPkcTVoB3jr7CJsLrF8FqeUdxNM/
+ Ev+e/KuxyWcRKtq+WwB7RGDTkg36fVsOj4ZgFWhxWOGQiSjvY37ezhw/t/PLSTs8eMNLf2+tQA
+ w7Q0eg38c74qHUqK/IEI2xQTKqKi8cTDBW1CPbi72GlC0AYj6fsVFUzFkAcjVPeV8DNHTpoSNk
+ 0kIbRfFXpI4rotEcYXdn65rm1FkNrVxZ87r7yssZ1iR/eQ+65EH1Dv3+szXhCsGfD2WRqw8nMx
+ vCzp7dJ6KPxEDCIpGmNzp419vat3t6n1JJFWI/kApzB30v7rIgexWw6V1JmCrDHBZE9bqjcwWt
+ QPy1NOy+KO2CvPeC5d/KdQfaTp0yR103ZYPSdeJ7uCxRnKwAHFAuiQFBL+D0u04i8mQV5p3GAM
+ w461FU3tzfcS+uF5ifQ8mCFdYPVRtSnVEcDILWoUkKmLXsKmdSJxkmGba70yJ2buxxy7MHEIRf
+ 06840LQflAPsc3fuUO/EJyS0zR7FxwjKfHSqUKJqWGBDTxhRhqzkqQFq6dqdOHBxZu/HgEhcjm
+ shiahr2uC7krNoDiKtXQdp6UqP0alrKg2izIT2G54ZLGbQkiu5wtD08uMeaXExBC897qht9GyX
+ ay9cj4cudC3A3a3LtfBKgOHb7vlO/jdYhi1OcxokHx8p8UYRRZJu/TXhd8D7yG6qkcfjw3GMyu
+ KGgcaKS/vN4zWiO0o21q4FH2hsRxrvkj3pPd79Onb5pC72DxonMIjaW7I/14YNH3HXSzGCP7yn
+ Fghs/QUEDbg+FnCk6QWeSPr58w6aqGTj48ZiRAaJF2OOePLxv5xeP7Oz+Azs8PLbuHgZniE+n0
+ RgiJO6QNWmcZlORqyKejVEUGYV2/7CR6iVrCEaJW6pc0L4P0Ad3zxiIaZl2WAyTAHVE2kSxECk
+ syh8R/WJjTXu2vnlBVc7edlpc8iqclTROPDP8imwHrf2gcEDZgLMHT4+Ifjyb8yc09qSZKJVEh
+ lO3Orzp9w7cshiNVE7RxEBodgDjUCqSRwQPUG91Opo6RaBXpyUvMXw+AKHr52MMIYCS/8Z19OT
+ WeQmCtvNL3Qd9MPPBMlGdkgOTMijApReqEQ6tLhRKkZ1h/Ruwz+juWbvbToNcMMsYxUBaIIS9B
+ OpAgzBZ84le6ArFJbpacO2pE3KVMS5vjeMDFYai5FwdqN5BC4pIjXAucUyRYvkpchYfkXpQlrH
+ vMlUiIMe+Y+CQsEK0YalEyV4y+YxGNl4KQUJ5cpfGgZmhms+0b3fBXvQOwVX0Qfo9z80K3dszg
+ n0WlYRfv9CYzXcIHJzSY1ZR9CfgkmQHOrLwLexJUH/qM2s8PTmzTqf7VrDXayvLiCzH6/j8T06
+ m4u/exkIoC4ho/t2RfQTA3oEMzj6+kH4QxW2bq9p+LxfIf1fmo7BWqXwZ2kf0IrD3LrlUBC45K
+ /H5ZWRPTrzooA1ALsFel01sqByV6x1nnX6ZRdzl7TOFFOWc3eKuDjqiXx302Dx5kztYhi6YzSR
+ bFldp1+tUzqsXr9nUhCIt9PaIsgH2APZTgP2jx3b/7KEdHR3TfRGHH4dUmm/N4aywFd/B3lULW
+ Ha1L+TGjKj5JFoH0bWnzJjXyvZ9gP18LpkZDjv3njhyflYeTI2kYYMXqBmAFYfcbK0xH9p68Mo
+ qg5fWmQ+KSkvuoI0iWsh4cTiU7UxkEjZG1+zERrAFBtjPFxyQjrWJmkK1Ct+bjjU7hxwYXmt3r
+ QI1C6kbnzAURm6M7hzs2TtQtw5nyKKjVv/NPhJq2kWnKJOpUWIp1Y3ktOKYc6T4VrDHHmOHboC
+ hOl5zhFaO2PQCZnSWMpBwKs67t0HdwWKh3VbXLiJ1ZD8oCOJQQw45nk7ouTSbTkTFoPHMsyXKK
+ QFeeIYEDXWZogCPfYPrGkVezQRYkfsHnRZ0TIDpLq3q2yKNHcyBRdTdyq+LaBK4ku0ffJ2DxnD
+ agQml20TEmoWnza5mPGf0Ca+8N0D0sT+rNHIwqJM8KlHqQNXdVMNShgmwD8OxmAxHBsDrhFi/q
+ PftZBVQBa3kd4SsCWeo379hY2Cr0bIHDx462IsO4iQvZBYF/uWLUxdkupNSaTkru3aj+wD7AvT
+ F2aR9G8XweDYs0MaNzA+dIvvw6cijCUvA9DtHxbPIAFJErptUA77VSfvtwL6MluP2KjKLeL0kr
+ 9Ti5Afxtqi+LAIHFaWDWf4I4C5VP/H3EdURcLkBikuKqZ/AXmyHABdgT9kk0rrZnOD25tWVDW7
+ 7jGYR4S9hdVyp2t5e187OH9j5JSL7czs6vEe+r9mS74dSUYVpoCIhoUN0T24dUMx6QZazxfuOj
+ aSAXxJG/Ha1QnSPDlX4x0sGGhe96g5aHwIeI3syx9qMMJNy69nGeioq5/qJtcdXHj/F2rjk1qV
+ 9MqYC9wywF9CPAPawSBhNbDia2gjrBD6UUaya+aSmwcDwA+vs3bMaFTjwrgGtg4sQb8mbnFJRT
+ RJMRfJN29uHq2VbenuAPVN5NU7pc3pBjjSOLhDSZ94cQxsJXAqlmTtonJAqw0cnIrC7BVqv5fA
+ JOX8aEZ1HWUmmGROs8NyQ1eHCR/bGQTiYH8zhLnM25w1HQ/4ZvevdsRLRusbxwa5B2xS/x+ehH
+ BAcLmspS9KMpCPmCEYQ5YuSiIBB+zhv9QiQBCCidnRmnCbxC4x1Ioa/+vqyA7uUBOpyUXAY6hm
+ arRXuusGX43szwGPTmMCNeILPTedHbzAE0DvYZyon20/o/WbpJ/th/HIUheN+8d7MGC6dPAfYE
+ 8xvJeGMz4j3hfsaxW+s9U0fjYE37OF4CLBHYMLakQwF5drhvvaFNDQu12BZcrCwCzfln5cUTb6
+ sS7D32lFQZv/tJz8NNa2AHlys2DSnRGICi9QfAhUBubIMtxpmIC0pIFukCp+a5EMdmVSBshG5x
+ +CSAEumMmkslwv3CbLePuy3YHDvsoGNKToqSkWWUL5muWlL3j6yBaV9u5E9oz23WUiUjnvXENj
+ d1XPrXhdouiBfza5HcdPD8chubgZ2e3NL75cRvO2nU9I/+/t7tDm+ePTIHjy4tKN796zd7Vi95
+ e37DrxYcfQutZtKG3F4GZ1uK8ZR18BiTwFDc7ub66gwhTMtyZ2cJKmKmUDqqPZ5aPexDFISQOC
+ JI41oEbNBm7atNHjg0Ey1nvZt9vJn1uh9qeHNfhFWwW344HPJP7UmzHIwzWmKqB4+OAPrD8DV4
+ /KBy6WAh8+MHEvbanuH1j44tmYXenr43UApI98b7D9cgOw5YKFVjokYYMKu2FabYK9ZphpfqNo
+ RQEbyR7xOyhDC+yYkhl6g2yFonB7hbAFKVbVCojk9sveeAxkMeBDiJoFvC5p4oVNm6YDH8X8wM
+ GtwwhTWgwPsb2+tf3Nr0/kk+dss4AqKuQCYvuS6cT0vfKe18/XuXEuJZnTQqlCOOcQLtxHIiqn
+ SO0pnPcBXKg9FogHozHaj6Smen18WybLYVXs6j1oTXpM4XKFh968lMeZZGz5HanICremNhHgdW
+ V5H0Bcun7k3IhQp+eLKtAleE53aEa0LPF2/7tgTWT3pJ75/9/ZHUyQDYw7k5E9kTLAl6fVHtn9
+ 0aGf3H1q321VHNvdp0Db6vOxdcUwrM4bdi3aX/SDk+h/h/efAwaXOW8hutQ5BscSlUPmjn/xU8
+ UYqECQCwG/tgqvT402FyUhzdAlk98kogKgUgoYG6i/TNKm7m10fIIx7pFPnVkq0hEcQnuiktKT
+ 45FqAMFzSd4hPkjKMsh7haqMApxRlOTWS5Eo+TCGK0IoE9ZOr4W3LPKS8YBBxaPIPfEGgtcchR
+ RQLxcnNbZ+RGcF+MiVQ7+917ZRg/9gePji3o+MTRnX19i7Y4y3jcLTaNWuBww7jLAxa9suXnjM
+ 7n74YJuZ+izqUUAyp2CRqBX0Bso6ld42nsKgXCMoAjsYCJ+0UePFsbNq/tvGzv7fm7Ve2xZBxc
+ NluZQAAZZDnBxKvDSdLTAlSA9WQvQe9m4H1h1M2j9G+N6hEZFLNjrUOT619dGKNFvhPgDNKCeq
+ kBdKziMYNLhklJJOSVnZoidDt7vG/cegYvTmdSLmeg7309ZLXSUfu9CA9hCSBlKTUh9f7uSWN+
+ FawF/xrHxYZZxIgxCmQFQf3YTiIIop1EpldzjCmc303LBI4Um+x9CYqNPlAtinlF1Q24KI3MKh
+ zBRWkneiMB4XDomK66CE71PNGxpUuWd8fOAMBLJmSyZRpRPZB1eiYB/jmM4zPxmJ5kRFHJMuCu
+ GvQsXdgN8IuZH8OKjIqmuZxZ7MmTaiYgbIr1oFZ30fPFPUaFE0jCwmMiVXXsBTYHcspVJdJHvk
+ ohiPsL4IS8qcZTWEA/A1qPhyBZdPJmI1uk/nCjs/OKLaA8ylrYIVEna/iks0YYhiYli5btz1Ju
+ 2QH8/Uf5fOJTEfqpTwmMX1eqXEy2JeywqiU80bnxtZD5ptyWoYbwD+EqsrMG5PeXGUrUBpZC1o
+ WIYIyijdU8n9xAcWmyA8rX0wpufCFyBtTG83JJL8KdWHki8KjsNBN++USnzWKwfGQsr+2LqZIu
+ RUpK63MVs1w8ZPNASItHMLRZGy3/ZH1MZ4QWvLBiFws1qvb6drJ/TO7fPTYzh9e2D2APSL7tsa
+ pqbNPvDDAvg1TK4/sXewnM7R0w/HY5dsO3cne3OLjaWQo5ZscOnxQSgB/UTryoiftgBSatVuM3
+ gN9BH94RO01mqINrp5b/6u/s07/qWo2PgUL3HPY1II3jswJry+eHg1UIypxer2+3d6ObQSwYnO
+ R/E6qtTb5+YPTc2sfnlgFVsbk5tXYQx99b3ahCyEjdF2QTfjedLrpV6hxUJTVsCNRRFLs4MJAM
+ VfmaKkXg1vePeY9MFDgHt41DuY7YK9QNux7dx5Jiu7LYqF2KTl+54gZKLFrWGsJ2S3kgQRCNwC
+ L+atRBFeGtrD5FGCvcYOI1m3rFA4klw12kfByjwEdskiIn3LCLEEnS5m1te6KMeL8BrUT54qtK
+ P4AAB2gSURBVK+UasoAMeZTKDP3F5N/jwcsGoLiMyaAMYU3Fd4X9xUBzhV/bnONgKoseOP5hce
+ 9LoooQ+X+GmXjkgbH4PoY9xgCj5SxxGQq2ryQm07zOShndLDfrtEpf2tXV9dkNk4ePLCT0/sab
+ enUVzgNMJZlb4u8kHhsxbWSJdD+TKFqZo25R/IPZb+qLURvRKx91JoCFLgOJY2jb1B2neLN5Hq
+ r94F7QU92s3izLFy5eVr44ci/RuGP6KEYJbf7ZnNkXf65UGuXv8rKoEhz8oKk/VNkA2pc0T1VX
+ FR+WcV1kKN03ZRR1Ih3U3KOocZhtBudgnSklOETuTh3Hvwm2ENiCLMv+b9giAm6RQGssDk+PQP
+ Yv2fn55d2fHJi3b3utwN7evl41V33l4CdwJvxX/RqRJqic6SnFk+JoSaI7DkNykGAn4XWzWjSE
+ VWCqBnadj635dxu3zyz4ZO/t4PxS6aPvO9JBSYz6WS9i7eA1wZfT1O4sTpmuS6DKaWXU0asWxV
+ MYWewd2SHpxfW2j+yLWwSqLgQgBB8cQmCi/dCKvXz8A3qdDVe0H3qcQlg/0amoaYml2dSZqlCr
+ xQ4WEcfLM011eeKMg+VN5EZhn+S0DAJCnaP5S+K7qMV3/c9fYDUOyGNvagGSiWxtryxEER5Ixa
+ 55yUlf4jsAfi08N0uWdsB/y8T1ZjiBICXL74km6JxaMxWUKcCUXHzqtuHnFOXVMqkXRJcIEURx
+ SvIih87wg7Oi9C6sp7kChMGTmEIR/fWFUdqqj7g0Tb2EqJyrx1ERBxgz25qb6yK2otANyY9qfc
+ kZtUyukc3roNueZEF9Uudgts20NvKXU6bNMJb2u0NZk3fWLXRtHunZ8zQUWzXXtXcA37/t4B9C
+ fix7nnN9IUpM3JvpFDsxLpJPusZEDJC0JzpIgHY/+XfsjJBmHczpQx00l/rQQAIVYjSWDDdqFj
+ 8iHpFhajFnUUTFvui8Ud/HpF63GQZ4t08Lf1BpIHFv/DFipqB+ojiBgw1T1ak4Cu1wIoA9cHBV
+ zrUu2c+R+T5a5VKnYhkROGgUOkUSbIwdWc/3rA+PtB17Tg06ACFzA2pIqSOg+FYYI8BJgT7kS1
+ mC2u3W3ZyemqXjx/bxfljOz49tb39Lk2+6LjoY/Gw2e5y9qSVJAXR53ODp4jskz2sg5E+p7xC4
+ iDzOTJy3NA4DYcIB4zRP52wKgR6eoCwCQlyULPqYm7D3gsbPP1Ha94+cTVVeH8vbYNLw6ciAVC
+ wJov1mn0H6DfAmkCSOh4vbDJbMf2FUog+HzC36nRt7/DEDs8urNE9pI1yFFTlq+6dtQR7NJs1a
+ Y4GgEdU3+mGdTGULeD3I6qOPSG5HjIVaepFCQUPGssqsJfjI3l6/hvuLi/i5tQyalAB9pENxy5
+ 2hXQa2Kyxk8qBg8pkcTroOKfB+MyK5jRexEE/LHUhIGuiPn85JdiDokFET5kurI19cAcaguTPr
+ gwOIMFOWiigiq7ViLQD1AO0U+Ef+9EBtfy7kq5RxpJ/BPSHgo61Jde+p2ze1yS+itw0AgCnVAH
+ kiMRBgyKyV0CpTDTTOJrvoDOMiz08cwT26i1Ax3ExCIRY4g2I7jEUwWAEgRQZeG8C4jsUZjvNp
+ m1WC7u6esOaCqjDw+NjOzw6Zu+EwL5kAxTZo/AfPL7DZLKmyI6Xsdf817SeRfDr9FnuZdHlHPX
+ HxID88V/8beLs6VToKUSSpBXNSumG40aTEyE2HSOkNFw4opjCSTJx/dnsjJsqpSE6fDEe0HHJn
+ 3VOhksKKHj4zK0H3t1V5OhQ6jsoBWMlPaWlwb/r28VGjY2VKBtPM8mJh6uedwbSCsK5Wa6f2w6
+ Df8QsWujtwdlDdQITMjY0QYEC0J9MaHMMju+Rg/3J2RmLtvV2AfZ0bnTOvlWzNr07vED7Fi0uL
+ rVv/PCon90QPi9X+1vFJrx1zLMNHh/rtKYpVwn2koHyYC6mNrl5ZeMXP7Nt7xm5YtgnA3Dg97L
+ AWMG5BofPZ9J/Y89ARgippcB+bPMlZKoaTkK/U88IG52OHdw7s8OzcxZnkVHwQvNCMDBYfjcqz
+ NIGAbJFAP3eHpUQKNKyicpT6Sjax14mbeMj6wj4gKboiPWNGLNjGd1yj+RmHUWbEXkpiM20jJ5
+ AGZDoQs57NHJpvh+/iHM3pJIz1kEwZckH0nCPBTXKC2BNf/cE9ospTdPWm4XqLKCoMCAIrf7s/
+ oTOvhxaooHu4P3jveIjSUkX86KjvyB7reCTgq7TZwy6JGZW5GhUWUGmK+IyJdfvS5FoyBjBWWx
+ eBiPsVJXdNFZqiX6MiQzg5L6pVW2wDqMOaj2roOsyRRf2zgD7iIT5NhisuoUye45UOwgpriSbm
+ u6FyxKUHs5ut9Wy5XzKjmbQkp39Azs8ObG9/UNrUxigqlfQONyDrDPJc0p70WuAcYnT+ddRyyf
+ W7dJlAvuwR5Z3UOFDRd8tiU3SM/3jn/xUiYsXJ3bBXpKhiCK4cP4wAMzB8SVgVHybPG8S7fFWs
+ N+NwGOtHXITT1VukvLQBMUTDQVxQezWHeKwke0rNPwZ2OPmjs8mMNGGjQaQKDTH/ivBvowq4Lg
+ usNctwAEmbHZZ2WgCiSE6aeHwqMaq/mBgk9HYmrW6HZ2e2KNHj+3y8rGdnN23gwN0ib4d7Hc4e
+ 6bCwRPnE/JWsC8ePCJKjFIMR8oqo3gNPAmlDiNHbFJ22G6tyUlm7iaJCGcysEnvpY1fP7XK8A0
+ Bh5ElCojLua0WczaVIQJDk1QoYSD5HI4wglDR/WS+tJnTColIRBDRatr+4bGdXrxn+8cPrNpoy
+ R0VrCj2JVUcyjRgfgYKp41RgF1E9XvWwtSquk+eor5Za6WMNPdn1OB9A0B0EM+0chGTkncOeqf
+ 8vQ4k/9/VvP83YO87dAcMRZ/pWkDYT0rBW/wzf61dCTpBlAyic12u8/mEVM56vbBKZS3ay2WY9
+ Gya7YI9LlkaqYHGCeqvmIaU5ZYqtmcaVfshKB3x3CHV1poQ0Klc2w3aeL5cSUZqLdRLfqOmfF2
+ IRt47vOXRa4B612w+5RlTRqRaYQNNdLDsBm/lFE8U4jmYx+f4qhs501TBNEjemYFTtZ0cEHJqF
+ SyZYZXAOdINa1SrNh4MODUM86UPj0/s6PTUusgs0QWfJL5BaytIibEF8p/PDVA8dwXY6zJ4G43
+ moQPnBsggLgBfQbg+b/ygN06mVmRYxrStmLHJ0g6LkGFNkLnpsBiND5S59hQgJRln2BhHEShHE
+ UWRIiKjnbcZoL17QYirChN/0Th3U8Y4ik4KpYOJrys3JlOqqPx72SxlMnffi6dkwdurtOJWAj7
+ ODa+fwH61os5+NJzabV+RPcCeqpzBkNI4OF+Cs0eRFpItdNWiQKuReFB8QOOvy/ZtBdryc/sWu
+ FvSSLd86gcI/lAfwPlITQ6iLj5RCeqMrnNuq9s0rOY271/Z6OqJza5fWGU2sK0XChFVIkpcMcq
+ f00eEkSONuhTBYxIV9fXwsYd9A6WXS0rp2Pa/XPD5dA8O7OHjD+zs4gNrdY/U1EUUcUWOO1mCk
+ 0cxDFG9KJw9ap0R7UOZgWfJ+DD04967wIuMzVRZYsyTpSLHN4pjieajVNJDrlSUTaGKF1zzvr2
+ 7hUIzpfMSqpd8DsosVpePn7nQgEfbP3j9lYq35Osd7GfziS0X6GoG/epGhK4rZ8bldglhZRwFy
+ jibZdFTPR5+kpwuiWg9aKp8gaopLSipkC5m3n73JFLC4VF5CCr0rJQq8IzB/K1W50WO18M8Bth
+ g09LZa2UxOwERPf4dMw6vS+F8oV+FRXpw/V6UpmrO/fh5ifEQZFonswa5ViEbEGn+YSOCTGK7X
+ tnN9ZW9fvOaGTOydPL1EAgA7Dl6Ms8X5lqE0RTtw13hxAFOWfrJQNf3Vl7veBZxxXpBv3jfEYT
+ jcszBsmngeNqIVAGEfDEDPjtbfQ5oREQBsvGQ9ee6uWJaVFAeEW1TppiaL6Lwk9O9bQVjxmIgB
+ C4q3djxo6zyK+3VdKgM+Pq38ZC4PT2NU8CgKCMuGY1Yk7RLkUNdnJmHdoxHPP3M12Oe8bh7gPF
+ exKvqYpTdLBUUBLeJDfpjWgyDq59P5gK6fp8rdu/4iFH9e+99wMaqw8N71mhjGAIkgXhoiASki
+ Gk1oLNnO1UekXfHJqKsj5TvM4NM0h14tOy1FafmOHGLygBNEcKHqxiG2uD7bqyyGNrs6ksbX31
+ h68E19bXUbyPCxGg7FHrJIcv3W/wo1EkqDs7RCQoqZzS2Pjxx0FAFwF9tbDJfGSSGtcrKDvabd
+ u/41B599D07uv+hbeodWlGst0s8Og19rsJTBvQNCrN7lK22O7CbUFSPixJSN9WUnCPHo6caQu3
+ waWhGmguqGyXp6xPeRdE/xUtSSmmzpWCj3Dflvn1LDLNjxb0TiXmm4DVDXdap8B11AhQHPcIH4
+ OOCnaEj+cam0xF7I4grPu+X9A2yTVgmuJ+OGoTcNC/5rWRYiIsmot+cFekNaijImiCvwmieGxG
+ dyDkDZvgvewJcchFged2LRc+0tKqp4AeAC41h2FOw10DGDPDeurx3vdzaeomMr2KtDhRZMprD2
+ uM9cXZBs8M9ulijG1mSY4xCpSurFrF4FlEz9CZJHzYUBWEOzwGNZWaL5cxevXlpV70bZpKnJw9
+ s7+DQ2h0Y3Pm0MFf1JLEIH6bPEClqgJGNZBbDaR6XAyfwdndL/LswrivrOHh5XDQ79FnYJZBJ9
+ IJraMjFMyndwFEPEX95m8ewhNCfh+VBNFUpKNEhCKDn8AHnlCN9EWWFhy91iVLUrBHXIotblvZ
+ bheNdyZEPPfYhJ4zeA+y9CSomzkRfAHTVPECsXsPs21Mi1/IULsypEJpnTJZHM8+gVRqmYRIcK
+ oGiJHzbhxM2VYHXW0znpHNu+7eMqo/u7dvlxWN77/0P7cGDCzsIsIeEsa6HFmDfpm5aYE9Okeu
+ xW6vQSMa3/ygBn5cjn43AEIUwFm95ccVYwTUsVXwYQ9VsvbD18MomV1/YovfUNtM+XwTPmCMUU
+ SwEoHCkIEYLykqX3DLNujb05JG98cT68MeZwkJhbrOV2Wi2tMFwILDfa7Gp6L2PP7eLD79v1da
+ hzfEam4VtKmuqZ2rVtjUbom+gqUdEhSItPW2ova+R8gn+O+Sh2tsoOntkH/JiUgpqjorDoigrG
+ qZSPdz51l8S7AvlVBlU7Dispr4sUToRyEjKAjdG55NRI5mObTC6ttlUQ1EozQvlDaddyUOHDVh
+ epI1IPmfdeidhUaxicPDYPux9pycl+G3ZTEQtQ1F+RPphlxLd6wL+1P3NgEJZJq9aty5GQEeB4
+ XZDWhABBC0K2phOBuuLim1XGKQuCTQFWlAq+4shO+40Vb/Ba3AIzBo1DSn2NIRcfQZBU+FvNHJ
+ StB9pLqe6mXRAsFAB4G9tOh3bsxdPKanutvft9OzcDg4PCfQ1TP5L07d2Jaiqc+QCbDz7eAb5z
+ osZArvBZ6KvCtfLbE+9TbLtSFTdLkErTEMg5zLLb6RoO2a66g3yFkkNDblSrgurUMjsgH05KDc
+ /+PjAtNkKlzo2UkSm8M0ilzhMRRUc2kDJkaxd2TzjEYpUT7s+9jHftpRZYkFQZE6UlutXU/rKj
+ D1X1VM675E/+OhoCnkb2KM7NAq0iOZn4xktEwD2SAsPj/bt4vzS3nv/I3v48EKRfRf+OA2DPzt
+ u3G8N9q62eSfasy7hkYWfrQB7XnbROs4MxX1zGLnDw31rm9nYJr3nNrn6yjajN1ZZTkNPxsNES
+ gFqB1A5MINDwxbMvJwbXnjnLvhNpOP90cQGkF3O4MZoNpjM7GbYp078oNuyw8MDe/zRZ/bJ93/
+ LusfnNl36JLD1klQZbIwxMAJF2e7ePv1IkLbTCI32B98Ee61nKDXkf65xe65f9UKsHnmUyoIFD
+ Ill8KhB1r89si/PwzefSXZa3Pk7t1nQ8UGGpb8NZXWAPVvwMXvR594CyMYTuIj2bDYbs9ENWSa
+ 09TDAowEasyuAPbhnNRWVYJ/fh4MTJ4yGXUHIMPNZ0OdT1hwdobokxXdnOlRgxWY7/ybah/pcf
+ J0AfM/KNR3Kp78t5jYdjamGIa1DOwk5mGId9O9E7ZDSpdcWJnU16FXD+iNm3rofDjvjq3VF9gx
+ C5EcPXGO2z0E4+HtJW2MSmOZ1162BPbPZ2GB4S7BHw+Dh0Ymdnjy0vYMD2kmzMxtqL7c78bww3
+ 2jOHgRNs3vZhpBENcRY0909VGKqfs/GRL7HjHsM5MrhJRzCW4Atb/YYR+edsCE/TEBbOPiVZkE
+ p8lBgz8oyFI8BwiVfrwsGAFREpxTrZLAvbz39XoqSmJfJwpX7bDBCL4YVK63EwuWNV/LxXAgvP
+ qe0x1uyNAtXMoygdbRGuyZrvwjswZFORjMWZW9vbmwC8y9qzAeMgvf3O9TYf/DBx/bw/JLOl80
+ uBiDI3+VbR/Z+cH5OYC/QuAP2GPbus4m+OUYwhhuvINFc2nxwbYPXX9mi99Jq875VNwIMblTP2
+ jTAReACNQ6nKbnSZ06ff0T2MGMbcxIXwH42wzD1LWmd636fB2+/27TDPfD2j+3zH/1rO//w12x
+ dbdnKs4PZYsqIE3uPkf3ePkEAxVo1lmCQCTxxVOgjqDkVAiAJD5WwNvbOGd9heSBJUnRpG+WiL
+ EEuKJV/Dti7osKhvCy0hJ//LwL7GM+J9UdhfDTGIBhMsMKAHAE6aDU+A3YpQzSAX2O6lWYe6Bm
+ 6Q6Jz9JmazTYlO8FcKnQLkEI4ocjcm9JcjcI9DNrNBRBSXRVAj1Xd2bi5EQrvFaaC6FIVZQTL5
+ pbVayjGoralDl3RGmtvSqvYNiVdCsbwPw542W6tA+VWs02VE2gdUow+lxdrwcugUWfhWlQkqFk
+ VsfH9mrhkVyu7vnljz1884746OX1ox0enzC5J4TR9Jq1nQXfBXjhSArkPE/eJVJEhRYE2ykRRv
+ w9GBusYQ8h5KdFKIfubccfG8BLRmc7veUMTFg8FOTw0OcY5J14URhURSOYXbcFBE5RmYzxr0cH
+ KDlw95qBjtB/czItXNLdLOlT8154x5F8V2YfJFqNzH2IRDz6APA6RLhbdeGW6BFBPDVYebeD9B
+ tgHdVNG9Du87C+I7CHXmk5mHNABoyRY1NLLfYi5tDPrdFv28OG5ffjBd2iIBrBv7XWo0+WEJR4
+ ScfbvonHKQ0jO3uOnuw0+3BgptorirCZtxdD4mDuqA+LyP2RRs6GNr57Z8NXXthn3rMao3r1sQ
+ jbmfRiUuFESCPkpIn3pwZHlTMnRz2ibAA97Ujizlc1XW7uZzOxN/4bNPnvNph11O1QrffYbP7J
+ Pf/jb1j58YNtKi3tosdKkq+VmQ45ehVnILUHf1NngggPLTJDSW4FagFEUEIPS4TONsNP19IzK0
+ p95qBH8XjT/KLp5K2f/8yN7f+nY21ptgSAl5Moi3hXZKwjIk5lAzYyGt9Yf9aiGsq2yK5idzWf
+ eLUuKDeAlx8eI6sW9+zBuL07nMxxNjh52FZ+17JDNa5upnF37CUX/PEegSJyrTwooP/GZPpMkM
+ aaaobgsIJY9RqMuuk50k84wBTyY4UDQF5UreaaUNnL53HIsZavTUmDIubZan1JliM8fkb2cQtV
+ 4hT3VqTdts1jay1fP7OXrl6whqN52bA2OvMQITNCw4aujdrxcWo3MMGePAm9RyeFvtYs5OdCIi
+ 7nERdFtuQOe6+HW2z68RCm9UkQ/3N41p2aEPIQhQFrFAJ996BYJkRVos4Z2NPS3+FVm+hGRB2e
+ lg6fIPiawqDmoNPMJJU5228uV9OASs1kTF4gKmW82XnHiUyEpTdy+c4Qx4UVcoW7dXGgKnm1X/
+ /KLInsULefTBcfuwSxpeCsjNBRrAfrYGA/uP7APP/rELi4e2wFsjvegLOkoTWWK/A6wdz++Mip
+ MSpo4m+nJCkuoa046O6C8bBGoGsVzcEMolVa80xFDL25f2/DVVza9emqGAuBmQTVCYGGkxCvQP
+ z4hiwVbNGohQpovbTKbaXAJDNFQpJ0ubAyqZ7a22Wprt5OJvenfkuvfazTssNO0/f2uvffpp/b
+ 5j37Xzi4/sXrziJt4tZ7bDRwgVyvK4CC3pGrDh5PACI2aawQs7tETgBS0g9J5gSaDgFgzHxweH
+ cqRXe78+i3A/q1f53+YacOSVtNl9K3Bvsg0AUjDwY0NR7eUXuIiliQTAA86B30UmqGKLAASXc0
+ 70CATRffCI1EyinzuRvOxdhFM5fPkAo/CXyjwIPh71kM8M5Zjll+gFByISisZBnwPvH/MQMAlR
+ V0FGQcY3rV9GpQ6Wjnqkf0Yqucpq4HnPz7r1lpNKdykdYfPnuYP46oIlkBNbfGulA1qVoD2cAL
+ 7RsuWk6k9ffaVXd9cURRw/8G57e/foxcTxorCVlo0TmmL4dGkU4URtes7lmCfn4NqRk4zphXLz
+ yUCMtKooKPYHe/zBJxey2BPBJAap4yiGdm7g1qmYLQYoYSRV054VxePzrnO8JspAbZUzBBOvED
+ L1n1mCkCdrPNPXFbKCnhF7LxXRECKxPGe3b4rSZKyIyMeAO2KsTCpHbxCQ6NI63ei/phDmXzzd
+ 48vLz68Fzdyehtnz+HjKDyORnZz3bPB7VCe8tCbj4bcFPfPHthHH31CCSYKtJ198M8dXTTexv/
+ WyP4u2IOzLyN37aFyi/D36ZjhdK+94M2RgiyPSwZG0Kkyvd4uZja5emKj11/a8vaVVRdTprHhh
+ R+FLKTJAaiiFtSIgiIheE100GIaVQwvAdCPpytbeGR/O5vZ6/4ND3inVrODZtW63YadXF7Y57/
+ 5u/bhp79p3f1zfgYU2vrTsS2oumhQbokIn1bGMMOi5toHWBTNgBw44+AuD3u3cmaRx7lSnz5GK
+ NjpdS+e/y8J9uVZU7JLEe+3B3u8NbfPxRetVnPr93s2Gg04WBw0TnjWr5YCAFy88/mUIKi9upE
+ rKe2oJb0WAGagj6YpnlVvAgqhWnDJEWmmwiqLnAqK8KqoiUVGzdPLRsyE9C4tjv4Hj9CdqqUlN
+ y2dl3wxfmZm8W0foo7ahCZ0cTIZCvPVGrOaXu+NvXj5jOuAKXCXF5dU5qDOV2vKMgNgHFExL7a
+ ExwJU2klAeOA1DkyYgxnhuD+wJ0+/suFkYAeHR3b//rntdY+sjs53WmVrGDqpqwiAKdsNPr5sq
+ gqwV89A2DHsyC53oCdnW+zCdgM7zcJV3YGyzxjH+sd/8dP/oKxW6a2oJd5z7KhUmosvECe03eL
+ 2199LVYPfwmBX/7b8oRRQfLy+H4RKGsxbNkNJNC0tcJhmKbiAva5vFq84K+XUQiXJW+wmH3qN/
+ 5T3uIJXfY3eA/8OjpEAALc0iE+MYdt18HDwJMDHS98nGiucANz5lKxGeIOSqBPwyVAbYXr9Cpw
+ pXQVx6JYcNn7V63GaDRQ57CIdjOnWe3Z23z766Lv26NH7tn90RP4ZFqmp5wHdkOhmhd+Jq3EwJ
+ gmtEzHu3LfLrjYnWLGd913QPFz6oHEg7nD9svuRsHgLMJiNbPT6a5u8+dK2A1A4M4i8nes3Hiw
+ Vsla+2NAkA+zBh8JxcU0P9vEcUf3UJpOxZtDOYJewtMXEaZzZ1N4M+8yE2tWKHTaq1t6v2cHJq
+ X32o39l3/vBv7Gj4/dYdJzNxzaCuiTkeRj0jK5FeOEA7BHJocGGlL0uMFzw2GuhfaYlMo2uYqM
+ 6zQN7Tez/Yt022GPc//mHDlOBWp6p3lnud/6ns0r+9+6zpLeYGrWyAlkKEvzgfBlI8BxM8LEQr
+ QPsx6O+Mpk1aDNFxNCVG/1gNHB8uZi4VFggrxGI/tmd712tfX4prYLy546OVxBkcbDigqeJnYO
+ 8MEHvOZrO+Ll0gBWDeGcyLwl8FwY2+Bf6WvRlwOQN813XqOBb1RotXNDg7Jvcf+u1BACcv1vTi
+ MdmG6BftddXr+yfvvgn6/WuOCjoO9/5rp0cn3Etqhvx/7TyxnmlIEGPQu/X34NTkJCsqmbQMKj
+ 2+72ePX/2pU0WM7t379jOzi6s2z1goEHvQmQanHSm/VUWsZPqiN2nsbbCKrUka3G0vXxv7uwie
+ Vul98rHCxoHdJOeW+r+fcsUtW+7P3/17361Ar9agV+twK9W4P+jFfg/Q4gExUFfznoAAAAASUV
+ 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
new file mode 100644
index 00000000000..8f621588fdc
--- /dev/null
+++ b/apps/dav/lib/Exception/ServerMaintenanceMode.php
@@ -0,0 +1,14 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Exception;
+
+use Sabre\DAV\Exception\ServiceUnavailable;
+
+class ServerMaintenanceMode extends ServiceUnavailable {
+
+}
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 b3ce591bd4a..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;
@@ -70,12 +55,15 @@ class BrowserErrorPagePlugin extends ServerPlugin {
}
/**
- * @param \Exception $ex
+ * @param \Throwable $ex
*/
- public function logException(\Exception $ex) {
+ public function logException(\Throwable $ex): void {
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 7ee82779849..eb548bbd55c 100644
--- a/apps/dav/lib/Files/FileSearchBackend.php
+++ b/apps/dav/lib/Files/FileSearchBackend.php
@@ -1,27 +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 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;
@@ -29,19 +10,26 @@ use OC\Files\Search\SearchBinaryOperator;
use OC\Files\Search\SearchComparison;
use OC\Files\Search\SearchOrder;
use OC\Files\Search\SearchQuery;
+use OC\Files\Storage\Wrapper\Jail;
use OC\Files\View;
-use OC\Metadata\IMetadataManager;
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;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
+use OCP\Files\Search\ISearchBinaryOperator;
+use OCP\Files\Search\ISearchComparison;
use OCP\Files\Search\ISearchOperator;
use OCP\Files\Search\ISearchOrder;
use OCP\Files\Search\ISearchQuery;
+use OCP\FilesMetadata\IFilesMetadataManager;
+use OCP\FilesMetadata\IMetadataQuery;
+use OCP\FilesMetadata\Model\IMetadataValueWrapper;
use OCP\IUser;
use OCP\Share\IManager;
use Sabre\DAV\Exception\NotFound;
@@ -55,37 +43,17 @@ use SearchDAV\Query\Order;
use SearchDAV\Query\Query;
class FileSearchBackend implements ISearchBackend {
- /** @var CachingTree */
- private $tree;
-
- /** @var IUser */
- private $user;
-
- /** @var IRootFolder */
- private $rootFolder;
-
- /** @var IManager */
- private $shareManager;
-
- /** @var View */
- private $view;
-
- /**
- * FileSearchBackend constructor.
- *
- * @param CachingTree $tree
- * @param IUser $user
- * @param IRootFolder $rootFolder
- * @param IManager $shareManager
- * @param View $view
- * @internal param IRootFolder $rootFolder
- */
- public function __construct(CachingTree $tree, IUser $user, IRootFolder $rootFolder, IManager $shareManager, View $view) {
- $this->tree = $tree;
- $this->user = $user;
- $this->rootFolder = $rootFolder;
- $this->shareManager = $shareManager;
- $this->view = $view;
+ public const OPERATOR_LIMIT = 100;
+
+ public function __construct(
+ private Server $server,
+ private CachingTree $tree,
+ private IUser $user,
+ private IRootFolder $rootFolder,
+ private IManager $shareManager,
+ private View $view,
+ private IFilesMetadataManager $filesMetadataManager,
+ ) {
}
/**
@@ -113,7 +81,7 @@ class FileSearchBackend implements ISearchBackend {
// all valid scopes support the same schema
//todo dynamically load all propfind properties that are supported
- return [
+ $props = [
// queryable properties
new SearchPropertyDefinition('{DAV:}displayname', true, true, true),
new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true),
@@ -132,9 +100,35 @@ class FileSearchBackend implements ISearchBackend {
new SearchPropertyDefinition(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME, true, false, false),
new SearchPropertyDefinition(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME, true, false, false),
new SearchPropertyDefinition(FilesPlugin::HAS_PREVIEW_PROPERTYNAME, true, false, false, SearchPropertyDefinition::DATATYPE_BOOLEAN),
- new SearchPropertyDefinition(FilesPlugin::FILE_METADATA_SIZE, true, false, false, SearchPropertyDefinition::DATATYPE_STRING),
new SearchPropertyDefinition(FilesPlugin::FILEID_PROPERTYNAME, true, false, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
];
+
+ return array_merge($props, $this->getPropertyDefinitionsForMetadata());
+ }
+
+
+ private function getPropertyDefinitionsForMetadata(): array {
+ $metadataProps = [];
+ $metadata = $this->filesMetadataManager->getKnownMetadata();
+ $indexes = $metadata->getIndexes();
+ foreach ($metadata->getKeys() as $key) {
+ $isIndex = in_array($key, $indexes);
+ $type = match ($metadata->getType($key)) {
+ IMetadataValueWrapper::TYPE_INT => SearchPropertyDefinition::DATATYPE_INTEGER,
+ IMetadataValueWrapper::TYPE_FLOAT => SearchPropertyDefinition::DATATYPE_DECIMAL,
+ IMetadataValueWrapper::TYPE_BOOL => SearchPropertyDefinition::DATATYPE_BOOLEAN,
+ default => SearchPropertyDefinition::DATATYPE_STRING
+ };
+ $metadataProps[] = new SearchPropertyDefinition(
+ FilesPlugin::FILE_METADATA_PREFIX . $key,
+ true,
+ $isIndex,
+ $isIndex,
+ $type
+ );
+ }
+
+ return $metadataProps;
}
/**
@@ -142,58 +136,84 @@ class FileSearchBackend implements ISearchBackend {
* @param string[] $requestProperties
*/
public function preloadPropertyFor(array $nodes, array $requestProperties): void {
- if (in_array(FilesPlugin::FILE_METADATA_SIZE, $requestProperties, true)) {
- // Preloading of the metadata
- $fileIds = [];
- foreach ($nodes as $node) {
- /** @var \OCP\Files\Node|\OCA\DAV\Connector\Sabre\Node $node */
- if (str_starts_with($node->getFileInfo()->getMimeType(), 'image/')) {
- /** @var \OCA\DAV\Connector\Sabre\File $node */
- $fileIds[] = $node->getFileInfo()->getId();
- }
- }
- /** @var IMetaDataManager $metadataManager */
- $metadataManager = \OC::$server->get(IMetadataManager::class);
- $preloadedMetadata = $metadataManager->fetchMetadataFor('size', $fileIds);
- foreach ($nodes as $node) {
- /** @var \OCP\Files\Node|\OCA\DAV\Connector\Sabre\Node $node */
- if (str_starts_with($node->getFileInfo()->getMimeType(), 'image/')) {
- /** @var \OCA\DAV\Connector\Sabre\File $node */
- $node->setMetadata('size', $preloadedMetadata[$node->getFileInfo()->getId()]);
- }
- }
- }
+ $this->server->emit('preloadProperties', [$nodes, $requestProperties]);
}
- /**
- * @param Query $search
- * @return SearchResult[]
- */
- public function search(Query $search): array {
- if (count($search->from) !== 1) {
- throw new \InvalidArgumentException('Searching more than one folder is not supported');
- }
- $query = $this->transformQuery($search);
- $scope = $search->from[0];
- if ($scope->path === null) {
+ private function getFolderForPath(?string $path = null): Folder {
+ if ($path === null) {
throw new \InvalidArgumentException('Using uri\'s as scope is not supported, please use a path relative to the search arbiter instead');
}
- $node = $this->tree->getNodeForPath($scope->path);
+
+ $node = $this->tree->getNodeForPath($path);
+
if (!$node instanceof Directory) {
throw new \InvalidArgumentException('Search is only supported on directories');
}
$fileInfo = $node->getFileInfo();
- $folder = $this->rootFolder->get($fileInfo->getPath());
- /** @var Folder $folder $results */
- $results = $folder->search($query);
+
+ /** @var Folder */
+ return $this->rootFolder->get($fileInfo->getPath());
+ }
+
+ /**
+ * @param Query $search
+ * @return SearchResult[]
+ */
+ public function search(Query $search): array {
+ switch (count($search->from)) {
+ case 0:
+ throw new \InvalidArgumentException('You need to specify a scope for the search.');
+ break;
+ case 1:
+ $scope = $search->from[0];
+ $folder = $this->getFolderForPath($scope->path);
+ $query = $this->transformQuery($search);
+ $results = $folder->search($query);
+ break;
+ default:
+ $scopes = [];
+ foreach ($search->from as $scope) {
+ $folder = $this->getFolderForPath($scope->path);
+ $folderStorage = $folder->getStorage();
+ if ($folderStorage->instanceOfStorage(Jail::class)) {
+ /** @var Jail $folderStorage */
+ $internalPath = $folderStorage->getUnjailedPath($folder->getInternalPath());
+ } else {
+ $internalPath = $folder->getInternalPath();
+ }
+
+ $scopes[] = new SearchBinaryOperator(
+ ISearchBinaryOperator::OPERATOR_AND,
+ [
+ new SearchComparison(
+ ISearchComparison::COMPARE_EQUAL,
+ 'storage',
+ $folderStorage->getCache()->getNumericStorageId(),
+ ''
+ ),
+ new SearchComparison(
+ ISearchComparison::COMPARE_LIKE,
+ 'path',
+ $internalPath . '/%',
+ ''
+ ),
+ ]
+ );
+ }
+
+ $scopeOperators = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $scopes);
+ $query = $this->transformQuery($search, $scopeOperators);
+ $userFolder = $this->rootFolder->getUserFolder($this->user->getUID());
+ $results = $userFolder->search($query);
+ }
/** @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);
@@ -298,11 +318,20 @@ class FileSearchBackend implements ISearchBackend {
/**
* @param Query $query
+ *
* @return ISearchQuery
*/
- private function transformQuery(Query $query): ISearchQuery {
+ private function transformQuery(Query $query, ?SearchBinaryOperator $scopeOperators = null): ISearchQuery {
+ $orders = array_map(function (Order $order): ISearchOrder {
+ $direction = $order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING;
+ if (str_starts_with($order->property->name, FilesPlugin::FILE_METADATA_PREFIX)) {
+ return new SearchOrder($direction, substr($order->property->name, strlen(FilesPlugin::FILE_METADATA_PREFIX)), IMetadataQuery::EXTRA);
+ } else {
+ return new SearchOrder($direction, $this->mapPropertyNameToColumn($order->property));
+ }
+ }, $query->orderBy);
+
$limit = $query->limit;
- $orders = array_map([$this, 'mapSearchOrder'], $query->orderBy);
$offset = $limit->firstResult;
$limitHome = false;
@@ -315,8 +344,21 @@ class FileSearchBackend implements ISearchBackend {
}
}
+ $operatorCount = $this->countSearchOperators($query->where);
+ if ($operatorCount > self::OPERATOR_LIMIT) {
+ throw new \InvalidArgumentException('Invalid search query, maximum operator limit of ' . self::OPERATOR_LIMIT . ' exceeded, got ' . $operatorCount . ' operators');
+ }
+
+ /** @var SearchBinaryOperator|SearchComparison */
+ $queryOperators = $this->transformSearchOperation($query->where);
+ if ($scopeOperators === null) {
+ $operators = $queryOperators;
+ } else {
+ $operators = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [$queryOperators, $scopeOperators]);
+ }
+
return new SearchQuery(
- $this->transformSearchOperation($query->where),
+ $operators,
(int)$limit->maxResults,
$offset,
$orders,
@@ -325,12 +367,24 @@ class FileSearchBackend implements ISearchBackend {
);
}
- /**
- * @param Order $order
- * @return ISearchOrder
- */
- private function mapSearchOrder(Order $order) {
- return new SearchOrder($order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING, $this->mapPropertyNameToColumn($order->property));
+ private function countSearchOperators(Operator $operator): int {
+ switch ($operator->type) {
+ case Operator::OPERATION_AND:
+ case Operator::OPERATION_OR:
+ case Operator::OPERATION_NOT:
+ /** @var Operator[] $arguments */
+ $arguments = $operator->arguments;
+ return array_sum(array_map([$this, 'countSearchOperators'], $arguments));
+ case Operator::OPERATION_EQUAL:
+ case Operator::OPERATION_GREATER_OR_EQUAL_THAN:
+ case Operator::OPERATION_GREATER_THAN:
+ case Operator::OPERATION_LESS_OR_EQUAL_THAN:
+ case Operator::OPERATION_LESS_THAN:
+ case Operator::OPERATION_IS_LIKE:
+ case Operator::OPERATION_IS_COLLECTION:
+ default:
+ return 1;
+ }
}
/**
@@ -354,13 +408,37 @@ class FileSearchBackend implements ISearchBackend {
if (count($operator->arguments) !== 2) {
throw new \InvalidArgumentException('Invalid number of arguments for ' . $trimmedType . ' operation');
}
+ if (!($operator->arguments[1] instanceof Literal)) {
+ throw new \InvalidArgumentException('Invalid argument 2 for ' . $trimmedType . ' operation, expected literal');
+ }
+ $value = $operator->arguments[1]->value;
+ // no break
+ case Operator::OPERATION_IS_DEFINED:
if (!($operator->arguments[0] instanceof SearchPropertyDefinition)) {
throw new \InvalidArgumentException('Invalid argument 1 for ' . $trimmedType . ' operation, expected property');
}
- if (!($operator->arguments[1] instanceof Literal)) {
- throw new \InvalidArgumentException('Invalid argument 2 for ' . $trimmedType . ' operation, expected literal');
+ $property = $operator->arguments[0];
+
+ if (str_starts_with($property->name, FilesPlugin::FILE_METADATA_PREFIX)) {
+ $field = substr($property->name, strlen(FilesPlugin::FILE_METADATA_PREFIX));
+ $extra = IMetadataQuery::EXTRA;
+ } else {
+ $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, $this->mapPropertyNameToColumn($operator->arguments[0]), $this->castValue($operator->arguments[0], $operator->arguments[1]->value));
+
+ return new SearchComparison(
+ $trimmedType,
+ $field,
+ $castedValue,
+ $extra ?? ''
+ );
+
case Operator::OPERATION_IS_COLLECTION:
return new SearchComparison('eq', 'mimetype', ICacheEntry::DIRECTORY_MIMETYPE);
default:
@@ -394,6 +472,10 @@ class FileSearchBackend implements ISearchBackend {
}
private function castValue(SearchPropertyDefinition $property, $value) {
+ if ($value === '') {
+ return '';
+ }
+
switch ($property->dataType) {
case SearchPropertyDefinition::DATATYPE_BOOLEAN:
return $value === 'yes';
@@ -405,7 +487,7 @@ class FileSearchBackend implements ISearchBackend {
if (is_numeric($value)) {
return max(0, 0 + $value);
}
- $date = \DateTime::createFromFormat(\DateTimeInterface::ATOM, $value);
+ $date = \DateTime::createFromFormat(\DateTimeInterface::ATOM, (string)$value);
return ($date instanceof \DateTime && $date->getTimestamp() !== false) ? $date->getTimestamp() : 0;
default:
return $value;
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 c3b2f27d72a..6ba539ddd87 100644
--- a/apps/dav/lib/Files/LazySearchBackend.php
+++ b/apps/dav/lib/Files/LazySearchBackend.php
@@ -1,28 +1,11 @@
<?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;
-use Sabre\DAV\INode;
use SearchDAV\Backend\ISearchBackend;
use SearchDAV\Query\Query;
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 1287104fd10..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 Symfony\Component\EventDispatcher\EventDispatcherInterface;
-
-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;
-
- /** @var EventDispatcherInterface */
- private $eventDispatcher;
-
- public function __construct(IUserManager $userManager,
- SyncService $syncService,
- CalDavBackend $calDav,
- CardDavBackend $cardDav,
- Defaults $themingDefaults,
- EventDispatcherInterface $eventDispatcher) {
- $this->userManager = $userManager;
- $this->syncService = $syncService;
- $this->calDav = $calDav;
- $this->cardDav = $cardDav;
- $this->themingDefaults = $themingDefaults;
- $this->eventDispatcher = $eventDispatcher;
- }
-
- 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'];
- $this->syncService->updateUser($user);
- }
-
- 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 $ex) {
- \OC::$server->getLogger()->logException($ex);
- }
- }
- if ($this->cardDav->getAddressBooksForUserCount($principal) === 0) {
- try {
- $this->cardDav->createAddressBook($principal, CardDavBackend::PERSONAL_ADDRESSBOOK_URI, [
- '{DAV:}displayname' => CardDavBackend::PERSONAL_ADDRESSBOOK_NAME,
- ]);
- } catch (\Exception $ex) {
- \OC::$server->getLogger()->logException($ex);
- }
- }
- }
- }
-}
diff --git a/apps/dav/lib/Listener/ActivityUpdaterListener.php b/apps/dav/lib/Listener/ActivityUpdaterListener.php
index 371912ff035..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,31 +13,27 @@ 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\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;
use Throwable;
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 {
@@ -173,7 +152,26 @@ class ActivityUpdaterListener implements IEventListener {
);
$this->logger->debug(
- sprintf('Activity generated for deleted calendar object %d', $event->getCalendarId())
+ sprintf('Activity generated for updated calendar object in calendar %d', $event->getCalendarId())
+ );
+ } catch (Throwable $e) {
+ // Any error with activities shouldn't abort the calendar deletion, so we just log it
+ $this->logger->error('Error generating activity for a deleted calendar object: ' . $e->getMessage(), [
+ 'exception' => $e,
+ ]);
+ }
+ } elseif ($event instanceof CalendarObjectMovedEvent) {
+ try {
+ $this->activityBackend->onMovedCalendarObject(
+ $event->getSourceCalendarData(),
+ $event->getTargetCalendarData(),
+ $event->getSourceShares(),
+ $event->getTargetShares(),
+ $event->getObjectData()
+ );
+
+ $this->logger->debug(
+ sprintf('Activity generated for moved calendar object from calendar %d to calendar %d', $event->getSourceCalendarId(), $event->getTargetCalendarId())
);
} catch (Throwable $e) {
// Any error with activities shouldn't abort the calendar deletion, so we just log it
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 3c17d399d4e..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;
@@ -36,17 +19,12 @@ use Psr\Log\LoggerInterface;
use Throwable;
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
new file mode 100644
index 00000000000..3a464d668f9
--- /dev/null
+++ b/apps/dav/lib/Listener/BirthdayListener.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Listener;
+
+use OCA\DAV\CalDAV\BirthdayService;
+use OCA\DAV\Events\CardCreatedEvent;
+use OCA\DAV\Events\CardDeletedEvent;
+use OCA\DAV\Events\CardUpdatedEvent;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+
+/** @template-implements IEventListener<CardCreatedEvent|CardUpdatedEvent|CardDeletedEvent> */
+class BirthdayListener implements IEventListener {
+ public function __construct(
+ private BirthdayService $birthdayService,
+ ) {
+ }
+
+ public function handle(Event $event): void {
+ if ($event instanceof CardCreatedEvent || $event instanceof CardUpdatedEvent) {
+ $cardData = $event->getCardData();
+
+ $this->birthdayService->onCardChanged($event->getAddressBookId(), $cardData['uri'], $cardData['carddata']);
+ }
+
+ if ($event instanceof CardDeletedEvent) {
+ $cardData = $event->getCardData();
+ $this->birthdayService->onCardDeleted($event->getAddressBookId(), $cardData['uri']);
+ }
+ }
+}
diff --git a/apps/dav/lib/Listener/CalendarContactInteractionListener.php b/apps/dav/lib/Listener/CalendarContactInteractionListener.php
index 04c759d5c3c..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;
@@ -43,37 +26,19 @@ use Sabre\VObject\Property;
use Sabre\VObject\Reader;
use Throwable;
use function strlen;
-use function strpos;
use function substr;
+/** @template-implements IEventListener<CalendarObjectCreatedEvent|CalendarObjectUpdatedEvent|CalendarShareUpdatedEvent> */
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 {
@@ -130,7 +95,7 @@ class CalendarContactInteractionListener implements IEventListener {
// Invalid principal
return;
}
- if (strpos($principal, self::URI_USERS) !== 0) {
+ if (!str_starts_with($principal, self::URI_USERS)) {
// Not a user principal
return;
}
@@ -159,7 +124,7 @@ class CalendarContactInteractionListener implements IEventListener {
}
$mailTo = $attendee->getValue();
- if (strpos($mailTo, 'mailto:') !== 0) {
+ if (!str_starts_with($mailTo, 'mailto:')) {
// Doesn't look like an email
continue;
}
diff --git a/apps/dav/lib/Listener/CalendarDeletionDefaultUpdaterListener.php b/apps/dav/lib/Listener/CalendarDeletionDefaultUpdaterListener.php
index 7bb535383c0..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;
@@ -31,23 +14,16 @@ use OCP\EventDispatcher\IEventListener;
use OCP\IConfig;
use Psr\Log\LoggerInterface;
use Throwable;
-use function strpos;
/**
- * @template-implements IEventListener<\OCA\DAV\Events\CalendarDeletedEvent>
+ * @template-implements IEventListener<CalendarDeletedEvent>
*/
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,
+ ) {
}
/**
@@ -61,7 +37,7 @@ class CalendarDeletionDefaultUpdaterListener implements IEventListener {
try {
$principalUri = $event->getCalendarData()['principaluri'];
- if (strpos($principalUri, 'principals/users') !== 0) {
+ if (!str_starts_with($principalUri, 'principals/users')) {
$this->logger->debug('Default calendar needs no update because the deleted calendar does not belong to a user principal');
return;
}
diff --git a/apps/dav/lib/Listener/CalendarObjectReminderUpdaterListener.php b/apps/dav/lib/Listener/CalendarObjectReminderUpdaterListener.php
index 3c168f6105c..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,40 +13,27 @@ 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;
use Throwable;
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
new file mode 100644
index 00000000000..94a0a208d4e
--- /dev/null
+++ b/apps/dav/lib/Listener/CalendarPublicationListener.php
@@ -0,0 +1,46 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Listener;
+
+use OCA\DAV\CalDAV\Activity\Backend;
+use OCA\DAV\Events\CalendarPublishedEvent;
+use OCA\DAV\Events\CalendarUnpublishedEvent;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use Psr\Log\LoggerInterface;
+
+/** @template-implements IEventListener<CalendarPublishedEvent|CalendarUnpublishedEvent> */
+class CalendarPublicationListener implements IEventListener {
+ public function __construct(
+ private Backend $activityBackend,
+ private LoggerInterface $logger,
+ ) {
+ }
+
+ /**
+ * In case the user has set their default calendar to the deleted one
+ */
+ public function handle(Event $event): void {
+ if ($event instanceof CalendarPublishedEvent) {
+ $this->logger->debug('Creating activity for Calendar being published');
+
+ $this->activityBackend->onCalendarPublication(
+ $event->getCalendarData(),
+ true
+ );
+ } elseif ($event instanceof CalendarUnpublishedEvent) {
+ $this->logger->debug('Creating activity for Calendar being unpublished');
+
+ $this->activityBackend->onCalendarPublication(
+ $event->getCalendarData(),
+ false
+ );
+ }
+ }
+}
diff --git a/apps/dav/lib/Listener/CalendarShareUpdateListener.php b/apps/dav/lib/Listener/CalendarShareUpdateListener.php
new file mode 100644
index 00000000000..b673d5d2e42
--- /dev/null
+++ b/apps/dav/lib/Listener/CalendarShareUpdateListener.php
@@ -0,0 +1,45 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Listener;
+
+use OCA\DAV\CalDAV\Activity\Backend;
+use OCA\DAV\Events\CalendarShareUpdatedEvent;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use Psr\Log\LoggerInterface;
+
+/** @template-implements IEventListener<CalendarShareUpdatedEvent> */
+class CalendarShareUpdateListener implements IEventListener {
+ public function __construct(
+ private Backend $activityBackend,
+ private LoggerInterface $logger,
+ ) {
+ }
+
+ /**
+ * In case the user has set their default calendar to the deleted one
+ */
+ public function handle(Event $event): void {
+ if (!($event instanceof CalendarShareUpdatedEvent)) {
+ // Not what we subscribed to
+ return;
+ }
+
+ $this->logger->debug('Creating activity for Calendar having its shares updated');
+
+ $this->activityBackend->onCalendarUpdateShares(
+ $event->getCalendarData(),
+ $event->getOldShares(),
+ $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 0281127c858..b9fd1a7f64b 100644
--- a/apps/dav/lib/Listener/CardListener.php
+++ b/apps/dav/lib/Listener/CardListener.php
@@ -3,30 +3,13 @@
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;
-use OCA\DAV\CardDAV\Activity\Provider\Card;
use OCA\DAV\CardDAV\Activity\Backend as ActivityBackend;
+use OCA\DAV\CardDAV\Activity\Provider\Card;
use OCA\DAV\Events\CardCreatedEvent;
use OCA\DAV\Events\CardDeletedEvent;
use OCA\DAV\Events\CardUpdatedEvent;
@@ -36,17 +19,12 @@ use Psr\Log\LoggerInterface;
use Throwable;
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
new file mode 100644
index 00000000000..eb599d33871
--- /dev/null
+++ b/apps/dav/lib/Listener/ClearPhotoCacheListener.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Listener;
+
+use OCA\DAV\CardDAV\PhotoCache;
+use OCA\DAV\Events\CardDeletedEvent;
+use OCA\DAV\Events\CardUpdatedEvent;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+
+/** @template-implements IEventListener<CardUpdatedEvent|CardDeletedEvent> */
+class ClearPhotoCacheListener implements IEventListener {
+ public function __construct(
+ private PhotoCache $photoCache,
+ ) {
+ }
+
+ public function handle(Event $event): void {
+ if ($event instanceof CardUpdatedEvent || $event instanceof CardDeletedEvent) {
+ $cardData = $event->getCardData();
+
+ $this->photoCache->delete($event->getAddressBookId(), $cardData['uri']);
+ }
+ }
+}
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
new file mode 100644
index 00000000000..45728aa35d3
--- /dev/null
+++ b/apps/dav/lib/Listener/OutOfOfficeListener.php
@@ -0,0 +1,179 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Listener;
+
+use DateTimeImmutable;
+use DateTimeZone;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\Calendar;
+use OCA\DAV\CalDAV\CalendarHome;
+use OCA\DAV\CalDAV\TimezoneService;
+use OCA\DAV\ServerFactory;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\IConfig;
+use OCP\User\Events\OutOfOfficeChangedEvent;
+use OCP\User\Events\OutOfOfficeClearedEvent;
+use OCP\User\Events\OutOfOfficeScheduledEvent;
+use OCP\User\IOutOfOfficeData;
+use Psr\Log\LoggerInterface;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\VObject\Component\VCalendar;
+use function fclose;
+use function fopen;
+use function fwrite;
+use function rewind;
+
+/**
+ * @template-implements IEventListener<OutOfOfficeScheduledEvent|OutOfOfficeChangedEvent|OutOfOfficeClearedEvent>
+ */
+class OutOfOfficeListener implements IEventListener {
+ public function __construct(
+ private ServerFactory $serverFactory,
+ private IConfig $appConfig,
+ private TimezoneService $timezoneService,
+ private LoggerInterface $logger,
+ ) {
+ }
+
+ public function handle(Event $event): void {
+ if ($event instanceof OutOfOfficeScheduledEvent) {
+ $userId = $event->getData()->getUser()->getUID();
+ $principal = "principals/users/$userId";
+ $calendarNode = $this->getCalendarNode($principal, $userId);
+ if ($calendarNode === null) {
+ return;
+ }
+ $tzId = $this->timezoneService->getUserTimezone($userId) ?? $this->timezoneService->getDefaultTimezone();
+ $vCalendarEvent = $this->createVCalendarEvent($event->getData(), $tzId);
+ $stream = fopen('php://memory', 'rb+');
+ try {
+ fwrite($stream, $vCalendarEvent->serialize());
+ rewind($stream);
+ $calendarNode->createFile(
+ $this->getEventFileName($event->getData()->getId()),
+ $stream,
+ );
+ } finally {
+ fclose($stream);
+ }
+ } elseif ($event instanceof OutOfOfficeChangedEvent) {
+ $userId = $event->getData()->getUser()->getUID();
+ $principal = "principals/users/$userId";
+ $calendarNode = $this->getCalendarNode($principal, $userId);
+ if ($calendarNode === null) {
+ return;
+ }
+ $tzId = $this->timezoneService->getUserTimezone($userId) ?? $this->timezoneService->getDefaultTimezone();
+ $vCalendarEvent = $this->createVCalendarEvent($event->getData(), $tzId);
+ try {
+ $oldEvent = $calendarNode->getChild($this->getEventFileName($event->getData()->getId()));
+ $oldEvent->put($vCalendarEvent->serialize());
+ return;
+ } catch (NotFound) {
+ $stream = fopen('php://memory', 'rb+');
+ try {
+ fwrite($stream, $vCalendarEvent->serialize());
+ rewind($stream);
+ $calendarNode->createFile(
+ $this->getEventFileName($event->getData()->getId()),
+ $stream,
+ );
+ } finally {
+ fclose($stream);
+ }
+ }
+ } elseif ($event instanceof OutOfOfficeClearedEvent) {
+ $userId = $event->getData()->getUser()->getUID();
+ $principal = "principals/users/$userId";
+ $calendarNode = $this->getCalendarNode($principal, $userId);
+ if ($calendarNode === null) {
+ return;
+ }
+ try {
+ $oldEvent = $calendarNode->getChild($this->getEventFileName($event->getData()->getId()));
+ $oldEvent->delete();
+ } catch (NotFound) {
+ // The user must have deleted it or the default calendar changed -> ignore
+ return;
+ }
+ }
+ }
+
+ private function getCalendarNode(string $principal, string $userId): ?Calendar {
+ $invitationServer = $this->serverFactory->createInviationResponseServer(false);
+ $server = $invitationServer->getServer();
+
+ /** @var \OCA\DAV\CalDAV\Plugin $caldavPlugin */
+ $caldavPlugin = $server->getPlugin('caldav');
+ $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principal);
+ if ($calendarHomePath === null) {
+ $this->logger->debug('Principal has no calendar home path');
+ return null;
+ }
+ try {
+ /** @var CalendarHome $calendarHome */
+ $calendarHome = $server->tree->getNodeForPath($calendarHomePath);
+ } catch (NotFound $e) {
+ $this->logger->debug('Calendar home not found', [
+ 'exception' => $e,
+ ]);
+ return null;
+ }
+ $uri = $this->appConfig->getUserValue($userId, 'dav', 'defaultCalendar', CalDavBackend::PERSONAL_CALENDAR_URI);
+ try {
+ $calendarNode = $calendarHome->getChild($uri);
+ } catch (NotFound $e) {
+ $this->logger->debug('Personal calendar does not exist', [
+ 'exception' => $e,
+ 'uri' => $uri,
+ ]);
+ return null;
+ }
+ if (!($calendarNode instanceof Calendar)) {
+ $this->logger->warning('Personal calendar node is not a calendar');
+ return null;
+ }
+ if ($calendarNode->isDeleted()) {
+ $this->logger->warning('Personal calendar has been deleted');
+ return null;
+ }
+
+ return $calendarNode;
+ }
+
+ private function getEventFileName(string $id): string {
+ return "out_of_office_$id.ics";
+ }
+
+ private function createVCalendarEvent(IOutOfOfficeData $data, string $tzId): VCalendar {
+ $shortMessage = $data->getShortMessage();
+ $longMessage = $data->getMessage();
+ $start = (new DateTimeImmutable)
+ ->setTimezone(new DateTimeZone($tzId))
+ ->setTimestamp($data->getStartDate())
+ ->setTime(0, 0);
+ $end = (new DateTimeImmutable())
+ ->setTimezone(new DateTimeZone($tzId))
+ ->setTimestamp($data->getEndDate())
+ ->modify('+ 1 days')
+ ->setTime(0, 0);
+ $vCalendar = new VCalendar();
+ $vCalendar->add('VEVENT', [
+ 'SUMMARY' => $shortMessage,
+ 'DESCRIPTION' => $longMessage,
+ 'STATUS' => 'CONFIRMED',
+ 'DTSTART' => $start,
+ 'DTEND' => $end,
+ 'X-NEXTCLOUD-OUT-OF-OFFICE' => $data->getId(),
+ ]);
+ return $vCalendar;
+ }
+}
diff --git a/apps/dav/lib/Listener/SubscriptionListener.php b/apps/dav/lib/Listener/SubscriptionListener.php
new file mode 100644
index 00000000000..fc9dfcf122d
--- /dev/null
+++ b/apps/dav/lib/Listener/SubscriptionListener.php
@@ -0,0 +1,64 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Listener;
+
+use OCA\DAV\BackgroundJob\RefreshWebcalJob;
+use OCA\DAV\CalDAV\Reminder\Backend as ReminderBackend;
+use OCA\DAV\CalDAV\WebcalCaching\RefreshWebcalService;
+use OCA\DAV\Events\SubscriptionCreatedEvent;
+use OCA\DAV\Events\SubscriptionDeletedEvent;
+use OCP\BackgroundJob\IJobList;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use Psr\Log\LoggerInterface;
+
+/** @template-implements IEventListener<SubscriptionCreatedEvent|SubscriptionDeletedEvent> */
+class SubscriptionListener implements IEventListener {
+ public function __construct(
+ private IJobList $jobList,
+ private RefreshWebcalService $refreshWebcalService,
+ private ReminderBackend $reminderBackend,
+ private LoggerInterface $logger,
+ ) {
+ }
+
+ /**
+ * In case the user has set their default calendar to the deleted one
+ */
+ public function handle(Event $event): void {
+ if ($event instanceof SubscriptionCreatedEvent) {
+ $subscriptionId = $event->getSubscriptionId();
+ $subscriptionData = $event->getSubscriptionData();
+
+ $this->logger->debug('Refreshing webcal data for subscription ' . $subscriptionId);
+ $this->refreshWebcalService->refreshSubscription(
+ (string)$subscriptionData['principaluri'],
+ (string)$subscriptionData['uri']
+ );
+
+ $this->logger->debug('Scheduling webcal data refreshment for subscription ' . $subscriptionId);
+ $this->jobList->add(RefreshWebcalJob::class, [
+ 'principaluri' => $subscriptionData['principaluri'],
+ 'uri' => $subscriptionData['uri']
+ ]);
+ } elseif ($event instanceof SubscriptionDeletedEvent) {
+ $subscriptionId = $event->getSubscriptionId();
+ $subscriptionData = $event->getSubscriptionData();
+
+ $this->logger->debug('Removing refresh webcal job for subscription ' . $subscriptionId);
+ $this->jobList->remove(RefreshWebcalJob::class, [
+ 'principaluri' => $subscriptionData['principaluri'],
+ 'uri' => $subscriptionData['uri']
+ ]);
+
+ $this->logger->debug('Cleaning all reminders for subscription ' . $subscriptionId);
+ $this->reminderBackend->cleanRemindersForCalendar($subscriptionId);
+ }
+ }
+}
diff --git a/apps/dav/lib/Listener/TrustedServerRemovedListener.php b/apps/dav/lib/Listener/TrustedServerRemovedListener.php
new file mode 100644
index 00000000000..9adbcfc14c2
--- /dev/null
+++ b/apps/dav/lib/Listener/TrustedServerRemovedListener.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Listener;
+
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\Federation\Events\TrustedServerRemovedEvent;
+
+/** @template-implements IEventListener<TrustedServerRemovedEvent> */
+class TrustedServerRemovedListener implements IEventListener {
+ public function __construct(
+ private CardDavBackend $cardDavBackend,
+ ) {
+ }
+
+ public function handle(Event $event): void {
+ if (!$event instanceof TrustedServerRemovedEvent) {
+ return;
+ }
+ $addressBookUri = $event->getUrlHash();
+ $addressBook = $this->cardDavBackend->getAddressBooksByUri('principals/system/system', $addressBookUri);
+ if (!is_null($addressBook)) {
+ $this->cardDavBackend->deleteAddressBook($addressBook['id']);
+ }
+ }
+}
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
new file mode 100644
index 00000000000..5f5fed05348
--- /dev/null
+++ b/apps/dav/lib/Listener/UserPreferenceListener.php
@@ -0,0 +1,42 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Listener;
+
+use OCA\DAV\BackgroundJob\UserStatusAutomation;
+use OCP\BackgroundJob\IJobList;
+use OCP\Config\BeforePreferenceDeletedEvent;
+use OCP\Config\BeforePreferenceSetEvent;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+
+/** @template-implements IEventListener<BeforePreferenceSetEvent|BeforePreferenceDeletedEvent> */
+class UserPreferenceListener implements IEventListener {
+
+ public function __construct(
+ protected IJobList $jobList,
+ ) {
+ }
+
+ public function handle(Event $event): void {
+ if ($event instanceof BeforePreferenceSetEvent) {
+ if ($event->getAppId() === 'dav' && $event->getConfigKey() === 'user_status_automation' && $event->getConfigValue() === 'yes') {
+ $event->setValid(true);
+
+ // Not the cleanest way, but we just add the job in the before event.
+ // If something ever turns wrong the first execution will remove the job again.
+ // We also first delete the current job, so the next run time is reset.
+ $this->jobList->remove(UserStatusAutomation::class, ['userId' => $event->getUserId()]);
+ $this->jobList->add(UserStatusAutomation::class, ['userId' => $event->getUserId()]);
+ }
+ } elseif ($event instanceof BeforePreferenceDeletedEvent) {
+ if ($event->getAppId() === 'dav' && $event->getConfigKey() === 'user_status_automation') {
+ $event->setValid(true);
+ }
+ }
+ }
+}
diff --git a/apps/dav/lib/Migration/BuildCalendarSearchIndex.php b/apps/dav/lib/Migration/BuildCalendarSearchIndex.php
index c8a649f3449..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 6a315f2a150..da8f31e7d3d 100644
--- a/apps/dav/lib/Migration/BuildCalendarSearchIndexBackgroundJob.php
+++ b/apps/dav/lib/Migration/BuildCalendarSearchIndexBackgroundJob.php
@@ -1,79 +1,39 @@
<?php
+
+declare(strict_types=1);
+
/**
- * @copyright 2017 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: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
-use OC\BackgroundJob\QueuedJob;
use OCA\DAV\CalDAV\CalDavBackend;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
+use OCP\BackgroundJob\QueuedJob;
use OCP\IDBConnection;
-use OCP\ILogger;
+use Psr\Log\LoggerInterface;
class BuildCalendarSearchIndexBackgroundJob extends QueuedJob {
-
- /** @var IDBConnection */
- private $db;
-
- /** @var CalDavBackend */
- private $calDavBackend;
-
- /** @var ILogger */
- private $logger;
-
- /** @var IJobList */
- private $jobList;
-
- /** @var ITimeFactory */
- private $timeFactory;
-
- /**
- * @param IDBConnection $db
- * @param CalDavBackend $calDavBackend
- * @param ILogger $logger
- * @param IJobList $jobList
- * @param ITimeFactory $timeFactory
- */
- public function __construct(IDBConnection $db,
- CalDavBackend $calDavBackend,
- ILogger $logger,
- IJobList $jobList,
- ITimeFactory $timeFactory) {
- $this->db = $db;
- $this->calDavBackend = $calDavBackend;
- $this->logger = $logger;
- $this->jobList = $jobList;
- $this->timeFactory = $timeFactory;
+ public function __construct(
+ private IDBConnection $db,
+ private CalDavBackend $calDavBackend,
+ private LoggerInterface $logger,
+ private IJobList $jobList,
+ 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->timeFactory->getTime();
- while (($this->timeFactory->getTime() - $startTime) < 15) {
+ $startTime = $this->time->getTime();
+ while (($this->time->getTime() - $startTime) < 15) {
$offset = $this->buildIndex($offset, $stopAt);
if ($offset >= $stopAt) {
break;
@@ -105,7 +65,7 @@ class BuildCalendarSearchIndexBackgroundJob extends QueuedJob {
->orderBy('id', 'ASC')
->setMaxResults(500);
- $result = $query->execute();
+ $result = $query->executeQuery();
while ($row = $result->fetch(\PDO::FETCH_ASSOC)) {
$offset = $row['id'];
diff --git a/apps/dav/lib/Migration/BuildSocialSearchIndex.php b/apps/dav/lib/Migration/BuildSocialSearchIndex.php
index ae2eb084e2b..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 98afecc3b7d..fab61d56fd6 100644
--- a/apps/dav/lib/Migration/BuildSocialSearchIndexBackgroundJob.php
+++ b/apps/dav/lib/Migration/BuildSocialSearchIndexBackgroundJob.php
@@ -1,75 +1,37 @@
<?php
+
+declare(strict_types=1);
+
/**
- * @copyright 2020 Matthias Heinisch <nextcloud@matthiasheinisch.de>
- *
- * @author call-me-matt <nextcloud@matthiasheinisch.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\Migration;
-use OC\BackgroundJob\QueuedJob;
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 OCP\ILogger;
+use Psr\Log\LoggerInterface;
class BuildSocialSearchIndexBackgroundJob extends QueuedJob {
-
- /** @var IDBConnection */
- private $db;
-
- /** @var CardDavBackend */
- private $davBackend;
-
- /** @var ILogger */
- private $logger;
-
- /** @var IJobList */
- private $jobList;
-
- /** @var ITimeFactory */
- private $timeFactory;
-
- /**
- * @param IDBConnection $db
- * @param CardDavBackend $davBackend
- * @param ILogger $logger
- * @param IJobList $jobList
- * @param ITimeFactory $timeFactory
- */
- public function __construct(IDBConnection $db,
- CardDavBackend $davBackend,
- ILogger $logger,
- IJobList $jobList,
- ITimeFactory $timeFactory) {
- $this->db = $db;
- $this->davBackend = $davBackend;
- $this->logger = $logger;
- $this->jobList = $jobList;
- $this->timeFactory = $timeFactory;
+ public function __construct(
+ private IDBConnection $db,
+ private CardDavBackend $davBackend,
+ private LoggerInterface $logger,
+ private IJobList $jobList,
+ ITimeFactory $timeFactory,
+ ) {
+ parent::__construct($timeFactory);
}
public function run($arguments) {
$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);
@@ -90,7 +52,7 @@ class BuildSocialSearchIndexBackgroundJob extends QueuedJob {
* @return int
*/
private function buildIndex($offset, $stopAt) {
- $startTime = $this->timeFactory->getTime();
+ $startTime = $this->time->getTime();
// get contacts with social profiles
$query = $this->db->getQueryBuilder();
@@ -98,8 +60,9 @@ 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->execute()->fetchAll();
+ $social_cards = $query->executeQuery()->fetchAll();
if (empty($social_cards)) {
return $stopAt;
@@ -108,10 +71,14 @@ 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->timeFactory->getTime() - $startTime) > 15) {
+ if (($this->time->getTime() - $startTime) > 15) {
break;
}
}
diff --git a/apps/dav/lib/Migration/CalDAVRemoveEmptyValue.php b/apps/dav/lib/Migration/CalDAVRemoveEmptyValue.php
index 5e575347d95..24e182e46eb 100644
--- a/apps/dav/lib/Migration/CalDAVRemoveEmptyValue.php
+++ b/apps/dav/lib/Migration/CalDAVRemoveEmptyValue.php
@@ -1,58 +1,26 @@
<?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;
-use OCP\ILogger;
use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;
+use Psr\Log\LoggerInterface;
use Sabre\VObject\InvalidDataException;
class CalDAVRemoveEmptyValue implements IRepairStep {
- /** @var IDBConnection */
- private $db;
-
- /** @var CalDavBackend */
- private $calDavBackend;
-
- /** @var ILogger */
- private $logger;
-
- /**
- * @param IDBConnection $db
- * @param CalDavBackend $calDavBackend
- * @param ILogger $logger
- */
- public function __construct(IDBConnection $db, CalDavBackend $calDavBackend, ILogger $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() {
@@ -100,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();
@@ -118,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) {
@@ -143,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 918023552da..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 29547b09ff7..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 f488d85bde2..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
new file mode 100644
index 00000000000..f09293ae0bb
--- /dev/null
+++ b/apps/dav/lib/Migration/RemoveObjectProperties.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Migration;
+
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class RemoveObjectProperties implements IRepairStep {
+ private const RESOURCE_TYPE_PROPERTY = '{DAV:}resourcetype';
+ 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';
+
+ /**
+ * RemoveObjectProperties constructor.
+ *
+ * @param IDBConnection $connection
+ */
+ public function __construct(
+ private IDBConnection $connection,
+ ) {
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getName() {
+ return 'Remove invalid object properties';
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function run(IOutput $output) {
+ $query = $this->connection->getQueryBuilder();
+ $updated = $query->delete('properties')
+ ->where($query->expr()->in('propertyname', $query->createNamedParameter([self::RESOURCE_TYPE_PROPERTY, self::ME_CARD_PROPERTY, self::CALENDAR_TRANSP_PROPERTY], IQueryBuilder::PARAM_STR_ARRAY)))
+ ->andWhere($query->expr()->eq('propertyvalue', $query->createNamedParameter('Object'), IQueryBuilder::PARAM_STR))
+ ->executeStatement();
+
+ $output->info("$updated invalid object properties removed.");
+ }
+}
diff --git a/apps/dav/lib/Migration/RemoveOrphanEventsAndContacts.php b/apps/dav/lib/Migration/RemoveOrphanEventsAndContacts.php
index 4789a74d98a..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 623a92a12dd..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;
@@ -41,11 +24,15 @@ class Version1004Date20170924124212 extends SimpleMigrationStep {
$schema = $schemaClosure();
$table = $schema->getTable('cards');
- $table->addIndex(['addressbookid'], 'cards_abid');
+ // Dropped in Version1030Date20240205103243 because cards_abid is redundant with cards_abiduri
+ // $table->addIndex(['addressbookid'], 'cards_abid');
$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 1ee767ce547..db071c65d98 100644
--- a/apps/dav/lib/Migration/Version1005Date20180413093149.php
+++ b/apps/dav/lib/Migration/Version1005Date20180413093149.php
@@ -3,32 +3,13 @@
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;
-use OCP\DB\Types;
use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
@@ -48,7 +29,7 @@ class Version1005Date20180413093149 extends SimpleMigrationStep {
if (!$schema->hasTable('directlink')) {
$table = $schema->createTable('directlink');
- $table->addColumn('id',Types::BIGINT, [
+ $table->addColumn('id', Types::BIGINT, [
'autoincrement' => true,
'notnull' => true,
'length' => 11,
diff --git a/apps/dav/lib/Migration/Version1005Date20180530124431.php b/apps/dav/lib/Migration/Version1005Date20180530124431.php
index ae057f74599..b5f9ff26962 100644
--- a/apps/dav/lib/Migration/Version1005Date20180530124431.php
+++ b/apps/dav/lib/Migration/Version1005Date20180530124431.php
@@ -1,33 +1,13 @@
<?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;
-use OCP\DB\Types;
use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
diff --git a/apps/dav/lib/Migration/Version1006Date20180619154313.php b/apps/dav/lib/Migration/Version1006Date20180619154313.php
index c607a14c2a9..231861a68c4 100644
--- a/apps/dav/lib/Migration/Version1006Date20180619154313.php
+++ b/apps/dav/lib/Migration/Version1006Date20180619154313.php
@@ -1,33 +1,13 @@
<?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;
-use OCP\DB\Types;
use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
diff --git a/apps/dav/lib/Migration/Version1006Date20180628111625.php b/apps/dav/lib/Migration/Version1006Date20180628111625.php
index 7ce2edccc28..f4be26e6ad0 100644
--- a/apps/dav/lib/Migration/Version1006Date20180628111625.php
+++ b/apps/dav/lib/Migration/Version1006Date20180628111625.php
@@ -3,34 +3,13 @@
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;
-use OCP\DB\Types;
use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
@@ -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 694f096ff3e..d2354a185df 100644
--- a/apps/dav/lib/Migration/Version1008Date20181030113700.php
+++ b/apps/dav/lib/Migration/Version1008Date20181030113700.php
@@ -3,35 +3,15 @@
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;
use Closure;
-use OCP\DB\Types;
use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
diff --git a/apps/dav/lib/Migration/Version1008Date20181105104826.php b/apps/dav/lib/Migration/Version1008Date20181105104826.php
index 86ce4c33ce5..82612307cbb 100644
--- a/apps/dav/lib/Migration/Version1008Date20181105104826.php
+++ b/apps/dav/lib/Migration/Version1008Date20181105104826.php
@@ -3,50 +3,28 @@
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;
use Closure;
-use OCP\DB\Types;
use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
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 e275b2a8e1e..72e8dee1bf8 100644
--- a/apps/dav/lib/Migration/Version1008Date20181105110300.php
+++ b/apps/dav/lib/Migration/Version1008Date20181105110300.php
@@ -3,50 +3,28 @@
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;
use Closure;
-use OCP\DB\Types;
use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
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 1191f0e8878..4524dca9c83 100644
--- a/apps/dav/lib/Migration/Version1011Date20190725113607.php
+++ b/apps/dav/lib/Migration/Version1011Date20190725113607.php
@@ -3,33 +3,13 @@
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;
-use OCP\DB\Types;
use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
diff --git a/apps/dav/lib/Migration/Version1011Date20190806104428.php b/apps/dav/lib/Migration/Version1011Date20190806104428.php
index a6f8772ec08..183dcd4bf1e 100644
--- a/apps/dav/lib/Migration/Version1011Date20190806104428.php
+++ b/apps/dav/lib/Migration/Version1011Date20190806104428.php
@@ -3,34 +3,14 @@
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;
use Closure;
-use OCP\DB\Types;
use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
diff --git a/apps/dav/lib/Migration/Version1012Date20190808122342.php b/apps/dav/lib/Migration/Version1012Date20190808122342.php
index 7bb39db6bf1..717bcbc1119 100644
--- a/apps/dav/lib/Migration/Version1012Date20190808122342.php
+++ b/apps/dav/lib/Migration/Version1012Date20190808122342.php
@@ -3,34 +3,13 @@
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;
-use OCP\DB\Types;
use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
@@ -47,8 +26,8 @@ class Version1012Date20190808122342 extends SimpleMigrationStep {
* @since 17.0.0
*/
public function changeSchema(IOutput $output,
- \Closure $schemaClosure,
- array $options):?ISchemaWrapper {
+ \Closure $schemaClosure,
+ array $options):?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
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
new file mode 100644
index 00000000000..656a50809cd
--- /dev/null
+++ b/apps/dav/lib/Migration/Version1024Date20211221144219.php
@@ -0,0 +1,61 @@
+<?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;
+use Doctrine\DBAL\Schema\SchemaException;
+use OCA\DAV\DAV\CustomPropertiesBackend;
+use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+/**
+ * Auto-generated migration step: Please modify to your needs!
+ */
+class Version1024Date20211221144219 extends SimpleMigrationStep {
+
+ /**
+ * @param IOutput $output
+ * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
+ * @param array $options
+ */
+ public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
+ }
+
+ /**
+ * @param IOutput $output
+ * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
+ * @param array $options
+ * @return null|ISchemaWrapper
+ * @throws SchemaException
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+ $propertiesTable = $schema->getTable('properties');
+
+ if ($propertiesTable->hasColumn('valuetype')) {
+ return null;
+ }
+ $propertiesTable->addColumn('valuetype', Types::SMALLINT, [
+ 'notnull' => false,
+ 'default' => CustomPropertiesBackend::PROPERTY_TYPE_STRING
+ ]);
+
+ return $schema;
+ }
+
+ /**
+ * @param IOutput $output
+ * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
+ * @param array $options
+ */
+ public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
+ }
+}
diff --git a/apps/dav/lib/Migration/Version1025Date20240308063933.php b/apps/dav/lib/Migration/Version1025Date20240308063933.php
new file mode 100644
index 00000000000..d84acf8fea9
--- /dev/null
+++ b/apps/dav/lib/Migration/Version1025Date20240308063933.php
@@ -0,0 +1,91 @@
+<?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\AppFramework\Services\IAppConfig;
+use OCP\DB\ISchemaWrapper;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\DB\Types;
+use OCP\IDBConnection;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+class Version1025Date20240308063933 extends SimpleMigrationStep {
+
+ public function __construct(
+ private IAppConfig $appConfig,
+ private IDBConnection $db,
+ ) {
+ }
+
+ /**
+ * @param IOutput $output
+ * @param Closure(): ISchemaWrapper $schemaClosure
+ * @param array $options
+ * @return null|ISchemaWrapper
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+
+ foreach (['addressbookchanges', 'calendarchanges'] as $tableName) {
+ $table = $schema->getTable($tableName);
+ if (!$table->hasColumn('created_at')) {
+ $table->addColumn('created_at', Types::INTEGER, [
+ 'notnull' => true,
+ 'length' => 4,
+ 'default' => 0,
+ ]);
+ }
+ }
+
+ return $schema;
+ }
+
+ 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)
+ ->set('created_at', $qb->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
+ ->where(
+ $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
new file mode 100644
index 00000000000..89c192a8419
--- /dev/null
+++ b/apps/dav/lib/Migration/Version1027Date20230504122946.php
@@ -0,0 +1,53 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Migration;
+
+use Closure;
+use OCA\DAV\CardDAV\SyncService;
+use OCP\DB\ISchemaWrapper;
+use OCP\IConfig;
+use OCP\IUserManager;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+use Psr\Log\LoggerInterface;
+use Throwable;
+
+class Version1027Date20230504122946 extends SimpleMigrationStep {
+ public function __construct(
+ private SyncService $syncService,
+ private LoggerInterface $logger,
+ private IUserManager $userManager,
+ private IConfig $config,
+ ) {
+ }
+ /**
+ * @param IOutput $output
+ * @param Closure(): ISchemaWrapper $schemaClosure
+ * @param array $options
+ */
+ public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
+ 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;
+ }
+
+ try {
+ $this->syncService->syncInstance();
+ $this->config->setAppValue('dav', 'needs_system_address_book_sync', 'no');
+ } catch (Throwable $e) {
+ $this->config->setAppValue('dav', 'needs_system_address_book_sync', 'yes');
+ $this->logger->error('Could not sync system address books during update', [
+ 'exception' => $e,
+ ]);
+ $output->warning('System address book sync failed. See logs for details');
+ }
+ }
+}
diff --git a/apps/dav/lib/Migration/Version1029Date20221114151721.php b/apps/dav/lib/Migration/Version1029Date20221114151721.php
new file mode 100644
index 00000000000..dba5e0b1a48
--- /dev/null
+++ b/apps/dav/lib/Migration/Version1029Date20221114151721.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Migration;
+
+use Closure;
+use Doctrine\DBAL\Schema\SchemaException;
+use OCP\DB\ISchemaWrapper;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+class Version1029Date20221114151721 extends SimpleMigrationStep {
+
+ /**
+ * @param IOutput $output
+ * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
+ * @param array $options
+ * @return null|ISchemaWrapper
+ * @throws SchemaException
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+ $calendarObjectsTable = $schema->getTable('calendarobjects');
+ if (!$calendarObjectsTable->hasIndex('calobj_clssfction_index')) {
+ $calendarObjectsTable->addIndex(['classification'], 'calobj_clssfction_index');
+ return $schema;
+ }
+ return null;
+ }
+
+}
diff --git a/apps/dav/lib/Migration/Version1029Date20231004091403.php b/apps/dav/lib/Migration/Version1029Date20231004091403.php
new file mode 100644
index 00000000000..1dcbf9c5dfc
--- /dev/null
+++ b/apps/dav/lib/Migration/Version1029Date20231004091403.php
@@ -0,0 +1,66 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 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\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+class Version1029Date20231004091403 extends SimpleMigrationStep {
+ /**
+ * @param IOutput $output
+ * @param Closure(): ISchemaWrapper $schemaClosure
+ * @param array $options
+ * @return null|ISchemaWrapper
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+
+ if (!$schema->hasTable('dav_absence')) {
+ $table = $schema->createTable('dav_absence');
+ $table->addColumn('id', Types::INTEGER, [
+ 'autoincrement' => true,
+ 'notnull' => true,
+ 'length' => 4,
+ ]);
+ $table->addColumn('user_id', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 64,
+ ]);
+ $table->addColumn('first_day', Types::STRING, [
+ 'length' => 10,
+ 'notnull' => true,
+ ]);
+ $table->addColumn('last_day', Types::STRING, [
+ 'length' => 10,
+ 'notnull' => true,
+ ]);
+ $table->addColumn('status', Types::STRING, [
+ 'length' => 100,
+ 'notnull' => true,
+ ]);
+ $table->addColumn('message', Types::TEXT, [
+ 'notnull' => true,
+ ]);
+ $table->addUniqueIndex(['user_id'], 'dav_absence_uid_idx');
+ } else {
+ $table = $schema->getTable('dav_absence');
+ }
+
+ if ($table->getPrimaryKey() === null) {
+ $table->setPrimaryKey(['id'], 'dav_absence_id_idx');
+ }
+
+ return $schema;
+ }
+}
diff --git a/apps/dav/lib/Migration/Version1030Date20240205103243.php b/apps/dav/lib/Migration/Version1030Date20240205103243.php
new file mode 100644
index 00000000000..72cbcafc444
--- /dev/null
+++ b/apps/dav/lib/Migration/Version1030Date20240205103243.php
@@ -0,0 +1,37 @@
+<?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\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+class Version1030Date20240205103243 extends SimpleMigrationStep {
+
+ /**
+ * @param IOutput $output
+ * @param Closure(): ISchemaWrapper $schemaClosure
+ * @param array $options
+ * @return null|ISchemaWrapper
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+
+ $tableCards = $schema->getTable('cards');
+
+ if ($tableCards->hasIndex('cards_abiduri') && $tableCards->hasIndex('cards_abid')) {
+ $tableCards->dropIndex('cards_abid');
+ }
+
+ return $schema;
+ }
+}
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 672ca4010b7..455760fc2bf 100644
--- a/apps/dav/lib/Profiler/ProfilerPlugin.php
+++ b/apps/dav/lib/Profiler/ProfilerPlugin.php
@@ -1,24 +1,9 @@
-<?php declare(strict_types = 1);
+<?php
+
+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;
@@ -29,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 f92bc4f3a0d..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;
@@ -36,61 +17,28 @@ use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class AppleProvisioningPlugin extends ServerPlugin {
-
/**
* @var Server
*/
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.
- *
- * @param IUserSession $userSession
- * @param IURLGenerator $urlGenerator
- * @param \OC_Defaults $themingDefaults
- * @param IRequest $request
- * @param IL10N $l10n
- * @param \closure $uuidClosure
*/
- public function __construct(IUserSession $userSession, IURLGenerator $urlGenerator,
- \OC_Defaults $themingDefaults, IRequest $request,
- IL10N $l10n, \closure $uuidClosure) {
- $this->userSession = $userSession;
- $this->urlGenerator = $urlGenerator;
+ public function __construct(
+ protected IUserSession $userSession,
+ protected IURLGenerator $urlGenerator,
+ \OC_Defaults $themingDefaults,
+ protected IRequest $request,
+ protected IL10N $l10n,
+ protected \Closure $uuidClosure,
+ ) {
$this->themingDefaults = $themingDefaults;
- $this->request = $request;
- $this->l10n = $l10n;
- $this->uuidClosure = $uuidClosure;
}
/**
@@ -120,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()]));
@@ -129,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();
@@ -157,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,
@@ -182,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
new file mode 100644
index 00000000000..3deafad6704
--- /dev/null
+++ b/apps/dav/lib/ResponseDefinitions.php
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * 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{
+ * id: int,
+ * firstDay: string,
+ * lastDay: string,
+ * status: string,
+ * }
+ *
+ * @todo this is a copy of \OCP\User\IOutOfOfficeData
+ * @psalm-type DAVCurrentOutOfOfficeData = DAVOutOfOfficeDataCommon&array{
+ * id: string,
+ * startDate: int,
+ * 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 2e5952f6efd..870aa0d4540 100644
--- a/apps/dav/lib/RootCollection.php
+++ b/apps/dav/lib/RootCollection.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 Georg Ehrke <oc.list@georgehrke.com>
- * @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;
@@ -36,47 +16,63 @@ use OCA\DAV\CalDAV\Proxy\ProxyMapper;
use OCA\DAV\CalDAV\PublicCalendarRoot;
use OCA\DAV\CalDAV\ResourceBooking\ResourcePrincipalBackend;
use OCA\DAV\CalDAV\ResourceBooking\RoomPrincipalBackend;
+use OCA\DAV\CalDAV\Sharing\Backend;
use OCA\DAV\CardDAV\AddressBookRoot;
use OCA\DAV\CardDAV\CardDavBackend;
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->getLogger();
- $psrLogger = \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);
- $legacyDispatcher = \OC::$server->getEventDispatcher();
- $config = \OC::$server->get(IConfig::class);
- $proxyMapper = \OC::$server->query(ProxyMapper::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,
+ 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()
);
+
$groupPrincipalBackend = new GroupPrincipalBackend($groupManager, $userSession, $shareManager, $config);
$calendarResourcePrincipalBackend = new ResourcePrincipalBackend($db, $userSession, $groupManager, $logger, $proxyMapper);
$calendarRoomPrincipalBackend = new RoomPrincipalBackend($db, $userSession, $groupManager, $logger, $proxyMapper);
@@ -91,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 = Server::get(Backend::class);
$filesCollection = new Files\RootCollection($userPrincipalBackend, 'principals/users');
$filesCollection->disableListing = $disableListing;
@@ -102,64 +96,82 @@ class RootCollection extends SimpleCollection {
$db,
$userPrincipalBackend,
$userManager,
- $groupManager,
$random,
$logger,
$dispatcher,
- $legacyDispatcher,
- $config
+ $config,
+ $calendarSharingBackend,
+ false,
);
- $userCalendarRoot = new CalendarRoot($userPrincipalBackend, $caldavBackend, 'principals/users', $psrLogger);
+ $userCalendarRoot = new CalendarRoot($userPrincipalBackend, $caldavBackend, 'principals/users', $logger);
$userCalendarRoot->disableListing = $disableListing;
- $resourceCalendarRoot = new CalendarRoot($calendarResourcePrincipalBackend, $caldavBackend, 'principals/calendar-resources', $psrLogger);
+ $resourceCalendarRoot = new CalendarRoot($calendarResourcePrincipalBackend, $caldavBackend, 'principals/calendar-resources', $logger);
$resourceCalendarRoot->disableListing = $disableListing;
- $roomCalendarRoot = new CalendarRoot($calendarRoomPrincipalBackend, $caldavBackend, 'principals/calendar-rooms', $psrLogger);
+ $roomCalendarRoot = new CalendarRoot($calendarRoomPrincipalBackend, $caldavBackend, 'principals/calendar-rooms', $logger);
$roomCalendarRoot->disableListing = $disableListing;
- $publicCalendarRoot = new PublicCalendarRoot($caldavBackend, $l10n, $config, $psrLogger);
- $publicCalendarRoot->disableListing = $disableListing;
+ $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,
- \OC::$server->getEventDispatcher()
+ $dispatcher,
+ $rootFolder,
);
+ $systemTagInUseCollection = Server::get(SystemTagsInUseCollection::class);
$commentsCollection = new Comments\RootCollection(
- \OC::$server->getCommentsManager(),
+ Server::get(ICommentsManager::class),
$userManager,
- \OC::$server->getUserSession(),
- \OC::$server->getEventDispatcher(),
- \OC::$server->getLogger()
+ Server::get(IUserSession::class),
+ $dispatcher,
+ $logger
);
- $pluginManager = new PluginManager(\OC::$server, \OC::$server->query(IAppManager::class));
- $usersCardDavBackend = new CardDavBackend($db, $userPrincipalBackend, $userManager, $groupManager, $dispatcher, $legacyDispatcher);
- $usersAddressBookRoot = new AddressBookRoot($userPrincipalBackend, $usersCardDavBackend, $pluginManager, 'principals/users');
+ $contactsSharingBackend = Server::get(\OCA\DAV\CardDAV\Sharing\Backend::class);
+ $config = Server::get(IConfig::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;
- $systemCardDavBackend = new CardDavBackend($db, $userPrincipalBackend, $userManager, $groupManager, $dispatcher, $legacyDispatcher);
- $systemAddressBookRoot = new AddressBookRoot(new SystemPrincipalBackend(), $systemCardDavBackend, $pluginManager, 'principals/system');
+ $systemCardDavBackend = new CardDavBackend(
+ $db,
+ $userPrincipalBackend,
+ $userManager,
+ $dispatcher,
+ $contactsSharingBackend,
+ $config
+ );
+ $systemAddressBookRoot = new AddressBookRoot(new SystemPrincipalBackend(), $systemCardDavBackend, $pluginManager, $userSession->getUser(), $groupManager, 'principals/system');
$systemAddressBookRoot->disableListing = $disableListing;
$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', [
@@ -180,6 +192,7 @@ class RootCollection extends SimpleCollection {
$systemAddressBookRoot]),
$systemTagCollection,
$systemTagRelationsCollection,
+ $systemTagInUseCollection,
$commentsCollection,
$uploadCollection,
$avatarCollection,
diff --git a/apps/dav/lib/Search/ACalendarSearchProvider.php b/apps/dav/lib/Search/ACalendarSearchProvider.php
index 91904d5e8d6..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 a7c2969016b..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;
@@ -32,31 +13,24 @@ use OCP\App\IAppManager;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUser;
-use OCP\Search\IProvider;
+use OCP\Search\FilterDefinition;
+use OCP\Search\IFilter;
+use OCP\Search\IFilteringProvider;
use OCP\Search\ISearchQuery;
use OCP\Search\SearchResult;
use OCP\Search\SearchResultEntry;
use Sabre\VObject\Component\VCard;
use Sabre\VObject\Reader;
-class ContactsSearchProvider implements IProvider {
-
- /** @var IAppManager */
- private $appManager;
-
- /** @var IL10N */
- private $l10n;
-
- /** @var IURLGenerator */
- private $urlGenerator;
-
- /** @var CardDavBackend */
- private $backend;
+class ContactsSearchProvider implements IFilteringProvider {
+ private static array $searchPropertiesRestricted = [
+ 'N',
+ 'FN',
+ 'NICKNAME',
+ 'EMAIL',
+ ];
- /**
- * @var string[]
- */
- private static $searchProperties = [
+ private static array $searchProperties = [
'N',
'FN',
'NICKNAME',
@@ -68,22 +42,12 @@ class ContactsSearchProvider implements IProvider {
'NOTE',
];
- /**
- * ContactsSearchProvider constructor.
- *
- * @param IAppManager $appManager
- * @param IL10N $l10n
- * @param IURLGenerator $urlGenerator
- * @param CardDavBackend $backend
- */
- public function __construct(IAppManager $appManager,
- IL10N $l10n,
- IURLGenerator $urlGenerator,
- CardDavBackend $backend) {
- $this->appManager = $appManager;
- $this->l10n = $l10n;
- $this->urlGenerator = $urlGenerator;
- $this->backend = $backend;
+ public function __construct(
+ private IAppManager $appManager,
+ private IL10N $l10n,
+ private IURLGenerator $urlGenerator,
+ private CardDavBackend $backend,
+ ) {
}
/**
@@ -100,19 +64,14 @@ class ContactsSearchProvider implements IProvider {
return $this->l10n->t('Contacts');
}
- /**
- * @inheritDoc
- */
- public function getOrder(string $route, array $routeParameters): int {
- if ($route === 'contacts.Page.index') {
- return -1;
+ public function getOrder(string $route, array $routeParameters): ?int {
+ if ($this->appManager->isEnabledForUser('contacts')) {
+ return $route === 'contacts.Page.index' ? -1 : 25;
}
- return 25;
+
+ return null;
}
- /**
- * @inheritDoc
- */
public function search(IUser $user, ISearchQuery $query): SearchResult {
if (!$this->appManager->isEnabledForUser('contacts', $user)) {
return SearchResult::complete($this->getName(), []);
@@ -122,17 +81,21 @@ class ContactsSearchProvider implements IProvider {
$addressBooks = $this->backend->getAddressBooksForUser($principalUri);
$addressBooksById = [];
foreach ($addressBooks as $addressBook) {
- $addressBooksById[(int) $addressBook['id']] = $addressBook;
+ $addressBooksById[(int)$addressBook['id']] = $addressBook;
}
$searchResults = $this->backend->searchPrincipalUri(
$principalUri,
- $query->getTerm(),
- self::$searchProperties,
+ $query->getFilter('term')?->get() ?? '',
+ $query->getFilter('title-only')?->get() ? self::$searchPropertiesRestricted : self::$searchProperties,
[
'limit' => $query->getLimit(),
'offset' => $query->getCursor(),
- ]
+ 'since' => $query->getFilter('since'),
+ 'until' => $query->getFilter('until'),
+ 'person' => $this->getPersonDisplayName($query->getFilter('person')),
+ 'company' => $query->getFilter('company'),
+ ],
);
$formattedResults = \array_map(function (array $contactRow) use ($addressBooksById):SearchResultEntry {
$addressBook = $addressBooksById[$contactRow['addressbookid']];
@@ -146,9 +109,14 @@ class ContactsSearchProvider implements IProvider {
$title = (string)$vCard->FN;
$subline = $this->generateSubline($vCard);
- $resourceUrl = $this->getDeepLinkToContactsApp($addressBook['uri'], (string) $vCard->UID);
+ $resourceUrl = $this->getDeepLinkToContactsApp($addressBook['uri'], (string)$vCard->UID);
+
+ $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 new SearchResultEntry($thumbnailUrl, $title, $subline, $resourceUrl, 'icon-contacts-dark', true);
+ return $result;
}, $searchResults);
return SearchResult::paginated(
@@ -157,16 +125,19 @@ class ContactsSearchProvider implements IProvider {
$query->getCursor() + count($formattedResults)
);
}
+ private function getPersonDisplayName(?IFilter $person): ?string {
+ $user = $person?->get();
+ if ($user instanceof IUser) {
+ return $user->getDisplayName();
+ }
+ return null;
+ }
- /**
- * @param string $principalUri
- * @param string $addressBookUri
- * @param string $contactsUri
- * @return string
- */
- protected function getDavUrlForContact(string $principalUri,
- string $addressBookUri,
- string $contactsUri): string {
+ protected function getDavUrlForContact(
+ string $principalUri,
+ string $addressBookUri,
+ string $contactsUri,
+ ): string {
[, $principalType, $principalId] = explode('/', $principalUri, 3);
return $this->urlGenerator->getAbsoluteURL(
@@ -178,13 +149,10 @@ class ContactsSearchProvider implements IProvider {
);
}
- /**
- * @param string $addressBookUri
- * @param string $contactUid
- * @return string
- */
- protected function getDeepLinkToContactsApp(string $addressBookUri,
- string $contactUid): string {
+ protected function getDeepLinkToContactsApp(
+ string $addressBookUri,
+ string $contactUid,
+ ): string {
return $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->linkToRoute('contacts.contacts.direct', [
'contact' => $contactUid . '~' . $addressBookUri
@@ -192,10 +160,6 @@ class ContactsSearchProvider implements IProvider {
);
}
- /**
- * @param VCard $vCard
- * @return string
- */
protected function generateSubline(VCard $vCard): string {
$emailAddresses = $vCard->select('EMAIL');
if (!is_array($emailAddresses) || empty($emailAddresses)) {
@@ -204,4 +168,24 @@ class ContactsSearchProvider implements IProvider {
return (string)$emailAddresses[0];
}
+
+ public function getSupportedFilters(): array {
+ return [
+ 'term',
+ 'since',
+ 'until',
+ 'person',
+ 'title-only',
+ ];
+ }
+
+ public function getAlternateIds(): array {
+ return [];
+ }
+
+ public function getCustomFilters(): array {
+ return [
+ new FilterDefinition('company'),
+ ];
+ }
}
diff --git a/apps/dav/lib/Search/EventsSearchProvider.php b/apps/dav/lib/Search/EventsSearchProvider.php
index 07fc90397ed..55fba40918a 100644
--- a/apps/dav/lib/Search/EventsSearchProvider.php
+++ b/apps/dav/lib/Search/EventsSearchProvider.php
@@ -3,46 +3,32 @@
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;
use OCA\DAV\CalDAV\CalDavBackend;
use OCP\IUser;
+use OCP\Search\IFilteringProvider;
use OCP\Search\ISearchQuery;
use OCP\Search\SearchResult;
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;
+use function array_map;
/**
* Class EventsSearchProvider
*
* @package OCA\DAV\Search
*/
-class EventsSearchProvider extends ACalendarSearchProvider {
-
+class EventsSearchProvider extends ACalendarSearchProvider implements IFilteringProvider {
/**
* @var string[]
*/
@@ -85,18 +71,21 @@ class EventsSearchProvider extends ACalendarSearchProvider {
/**
* @inheritDoc
*/
- public function getOrder(string $route, array $routeParameters): int {
- if ($route === 'calendar.View.index') {
- return -1;
+ public function getOrder(string $route, array $routeParameters): ?int {
+ if ($this->appManager->isEnabledForUser('calendar')) {
+ return $route === 'calendar.View.index' ? -1 : 30;
}
- return 30;
+
+ return null;
}
/**
* @inheritDoc
*/
- public function search(IUser $user,
- ISearchQuery $query): SearchResult {
+ public function search(
+ IUser $user,
+ ISearchQuery $query,
+ ): SearchResult {
if (!$this->appManager->isEnabledForUser('calendar', $user)) {
return SearchResult::complete($this->getName(), []);
}
@@ -105,18 +94,60 @@ class EventsSearchProvider extends ACalendarSearchProvider {
$calendarsById = $this->getSortedCalendars($principalUri);
$subscriptionsById = $this->getSortedSubscriptions($principalUri);
- $searchResults = $this->backend->searchPrincipalUri(
- $principalUri,
- $query->getTerm(),
- [self::$componentType],
- self::$searchProperties,
- self::$searchParameters,
- [
- 'limit' => $query->getLimit(),
- 'offset' => $query->getCursor(),
- ]
- );
- $formattedResults = \array_map(function (array $eventRow) use ($calendarsById, $subscriptionsById):SearchResultEntry {
+ /** @var string|null $term */
+ $term = $query->getFilter('term')?->get();
+ if ($term === null) {
+ $searchResults = [];
+ } else {
+ $searchResults = $this->backend->searchPrincipalUri(
+ $principalUri,
+ $term,
+ [self::$componentType],
+ self::$searchProperties,
+ self::$searchParameters,
+ [
+ 'limit' => $query->getLimit(),
+ 'offset' => $query->getCursor(),
+ 'timerange' => [
+ 'start' => $query->getFilter('since')?->get(),
+ 'end' => $query->getFilter('until')?->get(),
+ ],
+ ]
+ );
+ }
+ /** @var IUser|null $person */
+ $person = $query->getFilter('person')?->get();
+ $personDisplayName = $person?->getDisplayName();
+ if ($personDisplayName !== null) {
+ $attendeeSearchResults = $this->backend->searchPrincipalUri(
+ $principalUri,
+ $personDisplayName,
+ [self::$componentType],
+ ['ATTENDEE'],
+ self::$searchParameters,
+ [
+ 'limit' => $query->getLimit(),
+ 'offset' => $query->getCursor(),
+ 'timerange' => [
+ 'start' => $query->getFilter('since')?->get(),
+ 'end' => $query->getFilter('until')?->get(),
+ ],
+ ],
+ );
+
+ $searchResultIndex = array_combine(
+ array_map(fn ($event) => $event['calendarid'] . '-' . $event['uri'], $searchResults),
+ array_fill(0, count($searchResults), null),
+ );
+ foreach ($attendeeSearchResults as $attendeeResult) {
+ if (array_key_exists($attendeeResult['calendarid'] . '-' . $attendeeResult['uri'], $searchResultIndex)) {
+ // Duplicate
+ continue;
+ }
+ $searchResults[] = $attendeeResult;
+ }
+ }
+ $formattedResults = \array_map(function (array $eventRow) use ($calendarsById, $subscriptionsById): SearchResultEntry {
$component = $this->getPrimaryComponent($eventRow['calendardata'], self::$componentType);
$title = (string)($component->SUMMARY ?? $this->l10n->t('Untitled event'));
$subline = $this->generateSubline($component);
@@ -127,8 +158,16 @@ class EventsSearchProvider extends ACalendarSearchProvider {
$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;
+
+ if ($dtStart instanceof DateTime) {
+ $startDateTime = $dtStart->getDateTime()->format('U');
+ $result->addAttribute('createdAt', $startDateTime);
+ }
- return new SearchResultEntry('', $title, $subline, $resourceUrl, 'icon-calendar-dark', false);
+ return $result;
}, $searchResults);
return SearchResult::paginated(
@@ -138,15 +177,11 @@ class EventsSearchProvider extends ACalendarSearchProvider {
);
}
- /**
- * @param string $principalUri
- * @param string $calendarUri
- * @param string $calendarObjectUri
- * @return string
- */
- protected function getDeepLinkToCalendarApp(string $principalUri,
- string $calendarUri,
- string $calendarObjectUri): string {
+ protected function getDeepLinkToCalendarApp(
+ string $principalUri,
+ string $calendarUri,
+ string $calendarObjectUri,
+ ): string {
$davUrl = $this->getDavUrlForCalendarObject($principalUri, $calendarUri, $calendarObjectUri);
// This route will automatically figure out what recurrence-id to open
return $this->urlGenerator->getAbsoluteURL(
@@ -156,15 +191,11 @@ class EventsSearchProvider extends ACalendarSearchProvider {
);
}
- /**
- * @param string $principalUri
- * @param string $calendarUri
- * @param string $calendarObjectUri
- * @return string
- */
- protected function getDavUrlForCalendarObject(string $principalUri,
- string $calendarUri,
- string $calendarObjectUri): string {
+ protected function getDavUrlForCalendarObject(
+ string $principalUri,
+ string $calendarUri,
+ string $calendarObjectUri,
+ ): string {
[,, $principalId] = explode('/', $principalUri, 3);
return $this->urlGenerator->linkTo('', 'remote.php') . '/dav/calendars/'
@@ -173,10 +204,6 @@ class EventsSearchProvider extends ACalendarSearchProvider {
. $calendarObjectUri;
}
- /**
- * @param Component $eventComponent
- * @return string
- */
protected function generateSubline(Component $eventComponent): string {
$dtStart = $eventComponent->DTSTART;
$dtEnd = $this->getDTEndForEvent($eventComponent);
@@ -207,10 +234,6 @@ class EventsSearchProvider extends ACalendarSearchProvider {
return "$formattedStartDate $formattedStartTime - $formattedEndDate $formattedEndTime";
}
- /**
- * @param Component $eventComponent
- * @return Property
- */
protected function getDTEndForEvent(Component $eventComponent):Property {
if (isset($eventComponent->DTEND)) {
$end = $eventComponent->DTEND;
@@ -233,13 +256,27 @@ class EventsSearchProvider extends ACalendarSearchProvider {
return $end;
}
- /**
- * @param \DateTime $dtStart
- * @param \DateTime $dtEnd
- * @return bool
- */
- protected function isDayEqual(\DateTime $dtStart,
- \DateTime $dtEnd) {
+ protected function isDayEqual(
+ \DateTime $dtStart,
+ \DateTime $dtEnd,
+ ): bool {
return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
}
+
+ public function getSupportedFilters(): array {
+ return [
+ 'term',
+ 'person',
+ 'since',
+ 'until',
+ ];
+ }
+
+ public function getAlternateIds(): array {
+ return [];
+ }
+
+ public function getCustomFilters(): array {
+ return [];
+ }
}
diff --git a/apps/dav/lib/Search/TasksSearchProvider.php b/apps/dav/lib/Search/TasksSearchProvider.php
index 763720ee4ae..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;
@@ -41,7 +21,6 @@ use Sabre\VObject\Component;
* @package OCA\DAV\Search
*/
class TasksSearchProvider extends ACalendarSearchProvider {
-
/**
* @var string[]
*/
@@ -78,18 +57,21 @@ class TasksSearchProvider extends ACalendarSearchProvider {
/**
* @inheritDoc
*/
- public function getOrder(string $route, array $routeParameters): int {
- if ($route === 'tasks.Page.index') {
- return -1;
+ public function getOrder(string $route, array $routeParameters): ?int {
+ if ($this->appManager->isEnabledForUser('tasks')) {
+ return $route === 'tasks.Page.index' ? -1 : 35;
}
- return 35;
+
+ return null;
}
/**
* @inheritDoc
*/
- public function search(IUser $user,
- ISearchQuery $query): SearchResult {
+ public function search(
+ IUser $user,
+ ISearchQuery $query,
+ ): SearchResult {
if (!$this->appManager->isEnabledForUser('tasks', $user)) {
return SearchResult::complete($this->getName(), []);
}
@@ -100,13 +82,15 @@ class TasksSearchProvider extends ACalendarSearchProvider {
$searchResults = $this->backend->searchPrincipalUri(
$principalUri,
- $query->getTerm(),
+ $query->getFilter('term')?->get() ?? '',
[self::$componentType],
self::$searchProperties,
self::$searchParameters,
[
'limit' => $query->getLimit(),
'offset' => $query->getCursor(),
+ 'since' => $query->getFilter('since'),
+ 'until' => $query->getFilter('until'),
]
);
$formattedResults = \array_map(function (array $taskRow) use ($calendarsById, $subscriptionsById):SearchResultEntry {
@@ -131,26 +115,19 @@ class TasksSearchProvider extends ACalendarSearchProvider {
);
}
- /**
- * @param string $calendarUri
- * @param string $taskUri
- * @return string
- */
- protected function getDeepLinkToTasksApp(string $calendarUri,
- string $taskUri): string {
+ protected function getDeepLinkToTasksApp(
+ string $calendarUri,
+ string $taskUri,
+ ): string {
return $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->linkToRoute('tasks.page.index')
- . '#/calendars/'
+ . 'calendars/'
. $calendarUri
. '/tasks/'
. $taskUri
);
}
- /**
- * @param Component $taskComponent
- * @return string
- */
protected function generateSubline(Component $taskComponent): string {
if ($taskComponent->COMPLETED) {
$completedDateTime = new \DateTime($taskComponent->COMPLETED->getDateTime()->format(\DateTimeInterface::ATOM));
diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php
index 589e6c2bd6c..a92e162f1b0 100644
--- a/apps/dav/lib/Server.php
+++ b/apps/dav/lib/Server.php
@@ -1,53 +1,34 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @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 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 OCA\DAV\Connector\Sabre\RequestIdHeaderPlugin;
-use OCP\Diagnostics\IEventLogger;
-use OCP\Profiler\IProfiler;
-use OCA\DAV\Profiler\ProfilerPlugin;
-use OCP\AppFramework\Http\Response;
-use Psr\Log\LoggerInterface;
+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;
@@ -57,64 +38,104 @@ 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\BulkUpload\BulkUploadPlugin;
+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;
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->getLogger();
- $dispatcher = \OC::$server->getEventDispatcher();
- /** @var IEventDispatcher $newDispatcher */
- $newDispatcher = \OC::$server->query(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 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
@@ -122,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());
@@ -130,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);
- $newDispatcher->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
@@ -167,61 +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(), \OC::$server->getLogger()));
- $this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(\OC::$server->getConfig()));
- 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));
- $this->server->addPlugin(new \Sabre\CalDAV\Subscriptions\Plugin());
+ $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'),
- \OC::$server->getLogger())
- ));
+ $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(new SystemTagPlugin(
- \OC::$server->getSystemTagManager(),
- \OC::$server->getGroupManager(),
- \OC::$server->getUserSession()
- ));
+ $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);
+ $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
@@ -237,21 +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) {
+ $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());
@@ -259,9 +302,11 @@ class Server {
$this->server->addPlugin(
new \Sabre\DAV\PropertyStorage\Plugin(
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),
)
)
);
@@ -271,55 +316,75 @@ 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(),
- $view
+ \OCP\Server::get(IRootFolder::class),
+ $shareManager,
+ $view,
+ \OCP\Server::get(IFilesMetadataManager::class)
));
- $logger = \OC::$server->get(LoggerInterface::class);
$this->server->addPlugin(
- new BulkUploadPlugin($userFolder, $logger)
+ new BulkUploadPlugin(
+ $userFolder,
+ $logger
+ )
);
}
- $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();
@@ -330,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);
@@ -347,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);
}
}
@@ -361,10 +426,11 @@ class Server {
private function requestIsForSubtree(array $subTrees): bool {
foreach ($subTrees as $subTree) {
$subTree = trim($subTree, ' /');
- if (strpos($this->server->getRequestUri(), $subTree.'/') === 0) {
+ if (str_starts_with($this->server->getRequestUri(), $subTree . '/')) {
return true;
}
}
return false;
}
+
}
diff --git a/apps/dav/lib/ServerFactory.php b/apps/dav/lib/ServerFactory.php
new file mode 100644
index 00000000000..f632ee6015d
--- /dev/null
+++ b/apps/dav/lib/ServerFactory.php
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * 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
new file mode 100644
index 00000000000..7cbc0386d43
--- /dev/null
+++ b/apps/dav/lib/Service/AbsenceService.php
@@ -0,0 +1,155 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Service;
+
+use InvalidArgumentException;
+use OCA\DAV\BackgroundJob\OutOfOfficeEventDispatcherJob;
+use OCA\DAV\CalDAV\TimezoneService;
+use OCA\DAV\Db\Absence;
+use OCA\DAV\Db\AbsenceMapper;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IUser;
+use OCP\User\Events\OutOfOfficeChangedEvent;
+use OCP\User\Events\OutOfOfficeClearedEvent;
+use OCP\User\Events\OutOfOfficeScheduledEvent;
+use OCP\User\IOutOfOfficeData;
+
+class AbsenceService {
+ public function __construct(
+ private AbsenceMapper $absenceMapper,
+ private IEventDispatcher $eventDispatcher,
+ private IJobList $jobList,
+ private TimezoneService $timezoneService,
+ private ITimeFactory $timeFactory,
+ ) {
+ }
+
+ /**
+ * @param string $firstDay The first day (inclusive) of the absence formatted as YYYY-MM-DD.
+ * @param string $lastDay The last day (inclusive) of the absence formatted as YYYY-MM-DD.
+ *
+ * @throws \OCP\DB\Exception
+ * @throws InvalidArgumentException If no user with the given user id exists.
+ */
+ public function createOrUpdateAbsence(
+ IUser $user,
+ string $firstDay,
+ string $lastDay,
+ string $status,
+ string $message,
+ ?string $replacementUserId = null,
+ ?string $replacementUserDisplayName = null,
+ ): Absence {
+ try {
+ $absence = $this->absenceMapper->findByUserId($user->getUID());
+ } catch (DoesNotExistException) {
+ $absence = new Absence();
+ }
+
+ $absence->setUserId($user->getUID());
+ $absence->setFirstDay($firstDay);
+ $absence->setLastDay($lastDay);
+ $absence->setStatus($status);
+ $absence->setMessage($message);
+ $absence->setReplacementUserId($replacementUserId);
+ $absence->setReplacementUserDisplayName($replacementUserDisplayName);
+
+ if ($absence->getId() === null) {
+ $absence = $this->absenceMapper->insert($absence);
+ $eventData = $absence->toOutOufOfficeData(
+ $user,
+ $this->timezoneService->getUserTimezone($user->getUID()) ?? $this->timezoneService->getDefaultTimezone(),
+ );
+ $this->eventDispatcher->dispatchTyped(new OutOfOfficeScheduledEvent($eventData));
+ } else {
+ $absence = $this->absenceMapper->update($absence);
+ $eventData = $absence->toOutOufOfficeData(
+ $user,
+ $this->timezoneService->getUserTimezone($user->getUID()) ?? $this->timezoneService->getDefaultTimezone(),
+ );
+ $this->eventDispatcher->dispatchTyped(new OutOfOfficeChangedEvent($eventData));
+ }
+
+ $now = $this->timeFactory->getTime();
+ if ($eventData->getStartDate() > $now) {
+ $this->jobList->scheduleAfter(
+ OutOfOfficeEventDispatcherJob::class,
+ $eventData->getStartDate(),
+ [
+ 'id' => $absence->getId(),
+ 'event' => OutOfOfficeEventDispatcherJob::EVENT_START,
+ ],
+ );
+ }
+ if ($eventData->getEndDate() > $now) {
+ $this->jobList->scheduleAfter(
+ OutOfOfficeEventDispatcherJob::class,
+ $eventData->getEndDate(),
+ [
+ 'id' => $absence->getId(),
+ 'event' => OutOfOfficeEventDispatcherJob::EVENT_END,
+ ],
+ );
+ }
+
+ return $absence;
+ }
+
+ /**
+ * @throws \OCP\DB\Exception
+ */
+ public function clearAbsence(IUser $user): void {
+ try {
+ $absence = $this->absenceMapper->findByUserId($user->getUID());
+ } catch (DoesNotExistException $e) {
+ // Nothing to clear
+ return;
+ }
+ $this->absenceMapper->delete($absence);
+ $this->jobList->remove(OutOfOfficeEventDispatcherJob::class);
+ $eventData = $absence->toOutOufOfficeData(
+ $user,
+ $this->timezoneService->getUserTimezone($user->getUID()) ?? $this->timezoneService->getDefaultTimezone(),
+ );
+ $this->eventDispatcher->dispatchTyped(new OutOfOfficeClearedEvent($eventData));
+ }
+
+ public function getAbsence(string $userId): ?Absence {
+ try {
+ return $this->absenceMapper->findByUserId($userId);
+ } catch (DoesNotExistException $e) {
+ return null;
+ }
+ }
+
+ public function getCurrentAbsence(IUser $user): ?IOutOfOfficeData {
+ try {
+ $absence = $this->absenceMapper->findByUserId($user->getUID());
+ $oooData = $absence->toOutOufOfficeData(
+ $user,
+ $this->timezoneService->getUserTimezone($user->getUID()) ?? $this->timezoneService->getDefaultTimezone(),
+ );
+ if ($this->isInEffect($oooData)) {
+ return $oooData;
+ }
+ } catch (DoesNotExistException) {
+ // Nothing there to process
+ }
+ return null;
+ }
+
+ public function isInEffect(IOutOfOfficeData $absence): bool {
+ $now = $this->timeFactory->getTime();
+ return $absence->getStartDate() <= $now && $absence->getEndDate() >= $now;
+ }
+}
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 9a163e21edb..a1ada96b255 100644
--- a/apps/dav/lib/Settings/AvailabilitySettings.php
+++ b/apps/dav/lib/Settings/AvailabilitySettings.php
@@ -2,40 +2,65 @@
declare(strict_types=1);
-/*
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2021 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\Settings;
use OCA\DAV\AppInfo\Application;
+use OCA\DAV\Db\AbsenceMapper;
+use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http\TemplateResponse;
+use OCP\AppFramework\Services\IInitialState;
+use OCP\IConfig;
use OCP\Settings\ISettings;
+use OCP\User\IAvailabilityCoordinator;
+use Psr\Log\LoggerInterface;
class AvailabilitySettings implements ISettings {
+ public function __construct(
+ protected IConfig $config,
+ protected IInitialState $initialState,
+ protected ?string $userId,
+ private LoggerInterface $logger,
+ private IAvailabilityCoordinator $coordinator,
+ private AbsenceMapper $absenceMapper,
+ ) {
+ }
+
public function getForm(): TemplateResponse {
+ $this->initialState->provideInitialState(
+ 'user_status_automation',
+ $this->config->getUserValue(
+ $this->userId,
+ 'dav',
+ 'user_status_automation',
+ 'no'
+ )
+ );
+ $hideAbsenceSettings = !$this->coordinator->isEnabled();
+ $this->initialState->provideInitialState('hide_absence_settings', $hideAbsenceSettings);
+ if (!$hideAbsenceSettings) {
+ try {
+ $absence = $this->absenceMapper->findByUserId($this->userId);
+ $this->initialState->provideInitialState('absence', $absence);
+ } catch (DoesNotExistException) {
+ // The user has not yet set up an absence period.
+ // Logging this error is not necessary.
+ } catch (\OCP\DB\Exception $e) {
+ $this->logger->error("Could not find absence data for user $this->userId: " . $e->getMessage(), [
+ 'exception' => $e,
+ ]);
+ }
+ }
+
return new TemplateResponse(Application::APP_ID, 'settings-personal-availability');
}
public function getSection(): string {
- return 'groupware';
+ return 'availability';
}
public function getPriority(): int {
diff --git a/apps/dav/lib/Settings/CalDAVSettings.php b/apps/dav/lib/Settings/CalDAVSettings.php
index 6d60b2611e0..5e19539a899 100644
--- a/apps/dav/lib/Settings/CalDAVSettings.php
+++ b/apps/dav/lib/Settings/CalDAVSettings.php
@@ -1,53 +1,27 @@
<?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\IConfig;
use OCP\AppFramework\Services\IInitialState;
+use OCP\IConfig;
use OCP\IURLGenerator;
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',
'sendEventReminders' => 'yes',
- 'sendEventRemindersToSharedGroupMembers' => 'yes',
- 'sendEventRemindersPush' => 'no',
+ 'sendEventRemindersToSharedUsers' => 'yes',
+ 'sendEventRemindersPush' => '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
new file mode 100644
index 00000000000..c3f0742a640
--- /dev/null
+++ b/apps/dav/lib/SetupChecks/NeedsSystemAddressBookSync.php
@@ -0,0 +1,39 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\SetupChecks;
+
+use OCP\IConfig;
+use OCP\IL10N;
+use OCP\SetupCheck\ISetupCheck;
+use OCP\SetupCheck\SetupResult;
+
+class NeedsSystemAddressBookSync implements ISetupCheck {
+ public function __construct(
+ private IConfig $config,
+ private IL10N $l10n,
+ ) {
+ }
+
+ public function getName(): string {
+ return $this->l10n->t('DAV system address book');
+ }
+
+ public function getCategory(): string {
+ return 'dav';
+ }
+
+ public function run(): SetupResult {
+ if ($this->config->getAppValue('dav', 'needs_system_address_book_sync', 'no') === 'no') {
+ return SetupResult::success($this->l10n->t('No outstanding DAV system address book sync.'));
+ } else {
+ return SetupResult::warning($this->l10n->t('The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling "occ dav:sync-system-addressbook".'));
+ }
+ }
+}
diff --git a/apps/dav/lib/SetupChecks/WebdavEndpoint.php b/apps/dav/lib/SetupChecks/WebdavEndpoint.php
new file mode 100644
index 00000000000..c2574202fcd
--- /dev/null
+++ b/apps/dav/lib/SetupChecks/WebdavEndpoint.php
@@ -0,0 +1,75 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\SetupChecks;
+
+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;
+
+class WebdavEndpoint implements ISetupCheck {
+
+ use CheckServerResponseTrait;
+
+ public function __construct(
+ protected IL10N $l10n,
+ protected IConfig $config,
+ protected IURLGenerator $urlGenerator,
+ protected IClientService $clientService,
+ protected LoggerInterface $logger,
+ ) {
+ }
+
+ public function getCategory(): string {
+ return 'network';
+ }
+
+ public function getName(): string {
+ return $this->l10n->t('WebDAV endpoint');
+ }
+
+ public function run(): SetupResult {
+ $urls = [
+ ['propfind', '/remote.php/webdav', [207, 401]],
+ ];
+
+ foreach ($urls as [$verb,$url,$validStatuses]) {
+ $works = null;
+ foreach ($this->runRequest($verb, $url, ['httpErrors' => false]) as $response) {
+ // Check that the response status matches
+ $works = in_array($response->getStatusCode(), $validStatuses);
+ // Skip the other requests if one works
+ if ($works === true) {
+ break;
+ }
+ }
+ // If 'works' is null then we could not connect to the server
+ if ($works === null) {
+ return SetupResult::info(
+ $this->l10n->t('Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually.') . "\n" . $this->serverConfigHelp(),
+ $this->urlGenerator->linkToDocs('admin-setup-well-known-URL'),
+ );
+ }
+ // Otherwise if we fail we can abort here
+ if ($works === false) {
+ return SetupResult::error(
+ $this->l10n->t('Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken.') . "\n" . $this->serverConfigHelp(),
+ );
+ }
+ }
+ return SetupResult::success(
+ $this->l10n->t('Your web server is properly set up to allow file synchronization over WebDAV.')
+ );
+ }
+}
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
new file mode 100644
index 00000000000..b55f10164d7
--- /dev/null
+++ b/apps/dav/lib/SystemTag/SystemTagList.php
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\SystemTag;
+
+use OCP\IUser;
+use OCP\SystemTag\ISystemTag;
+use OCP\SystemTag\ISystemTagManager;
+use Sabre\Xml\Element;
+use Sabre\Xml\Reader;
+use Sabre\Xml\Writer;
+
+/**
+ * TagList property
+ *
+ * This property contains multiple "tag" elements, each containing a tag name.
+ */
+class SystemTagList implements Element {
+ public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
+ private array $canAssignTagMap = [];
+
+ /**
+ * @param ISystemTag[] $tags
+ */
+ public function __construct(
+ private array $tags,
+ ISystemTagManager $tagManager,
+ ?IUser $user,
+ ) {
+ $this->tags = $tags;
+ foreach ($this->tags as $tag) {
+ $this->canAssignTagMap[$tag->getId()] = $tagManager->canUserAssignTag($tag, $user);
+ }
+ }
+
+ /**
+ * @return ISystemTag[]
+ */
+ public function getTags(): array {
+ return $this->tags;
+ }
+
+ public static function xmlDeserialize(Reader $reader): void {
+ // unsupported/unused
+ }
+
+ public function xmlSerialize(Writer $writer): void {
+ foreach ($this->tags as $tag) {
+ $writer->startElement('{' . self::NS_NEXTCLOUD . '}system-tag');
+ $writer->writeAttributes([
+ 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 344ff1dbc70..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;
@@ -37,62 +21,15 @@ use Sabre\DAV\Exception\NotFound;
* Mapping node for system tag to object id
*/
class SystemTagMappingNode implements \Sabre\DAV\INode {
- /**
- * @var ISystemTag
- */
- protected $tag;
-
- /**
- * @var string
- */
- private $objectId;
-
- /**
- * @var string
- */
- private $objectType;
-
- /**
- * User
- *
- * @var IUser
- */
- protected $user;
-
- /**
- * @var ISystemTagManager
- */
- protected $tagManager;
-
- /**
- * @var ISystemTagObjectMapper
- */
- private $tagMapper;
-
- /**
- * Sets up the node, expects a full path name
- *
- * @param ISystemTag $tag system tag
- * @param string $objectId
- * @param string $objectType
- * @param IUser $user user
- * @param ISystemTagManager $tagManager
- * @param ISystemTagObjectMapper $tagMapper
- */
public function __construct(
- ISystemTag $tag,
- $objectId,
- $objectType,
- IUser $user,
- ISystemTagManager $tagManager,
- ISystemTagObjectMapper $tagMapper
+ private ISystemTag $tag,
+ private string $objectId,
+ private string $objectType,
+ private IUser $user,
+ private ISystemTagManager $tagManager,
+ private ISystemTagObjectMapper $tagMapper,
+ private \Closure $childWriteAccessFunction,
) {
- $this->tag = $tag;
- $this->objectId = $objectId;
- $this->objectType = $objectType;
- $this->user = $user;
- $this->tagManager = $tagManager;
- $this->tagMapper = $tagMapper;
}
/**
@@ -137,6 +74,8 @@ class SystemTagMappingNode implements \Sabre\DAV\INode {
* @param string $name The new name
*
* @throws MethodNotAllowed not allowed to rename node
+ *
+ * @return never
*/
public function setName($name) {
throw new MethodNotAllowed();
@@ -145,6 +84,7 @@ class SystemTagMappingNode implements \Sabre\DAV\INode {
/**
* Returns null, not supported
*
+ * @return null
*/
public function getLastModified() {
return null;
@@ -152,6 +92,8 @@ class SystemTagMappingNode implements \Sabre\DAV\INode {
/**
* Delete tag to object association
+ *
+ * @return void
*/
public function delete() {
try {
@@ -161,6 +103,10 @@ class SystemTagMappingNode implements \Sabre\DAV\INode {
if (!$this->tagManager->canUserAssignTag($this->tag, $this->user)) {
throw new Forbidden('No permission to unassign tag ' . $this->tag->getId());
}
+ $writeAccessFunction = $this->childWriteAccessFunction;
+ if (!$writeAccessFunction($this->objectId)) {
+ throw new Forbidden('No permission to unassign tag to ' . $this->objectId);
+ }
$this->tagMapper->unassignTags($this->objectId, $this->objectType, $this->tag->getId());
} catch (TagNotFoundException $e) {
// can happen if concurrent deletion occurred
diff --git a/apps/dav/lib/SystemTag/SystemTagNode.php b/apps/dav/lib/SystemTag/SystemTagNode.php
index a31deb59a93..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,10 @@ 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;
+class SystemTagNode implements \Sabre\DAV\ICollection {
- /**
- * @var ISystemTagManager
- */
- protected $tagManager;
-
- /**
- * User
- *
- * @var IUser
- */
- protected $user;
-
- /**
- * Whether to allow permissions for admins
- *
- * @var bool
- */
- protected $isAdmin;
+ protected int $numberOfFiles = -1;
+ protected int $referenceFileId = -1;
/**
* Sets up the node, expects a full path name
@@ -72,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,
+ ) {
}
/**
@@ -103,6 +74,8 @@ class SystemTagNode implements \Sabre\DAV\INode {
* @param string $name The new name
*
* @throws MethodNotAllowed not allowed to rename node
+ *
+ * @return never
*/
public function setName($name) {
throw new MethodNotAllowed();
@@ -114,11 +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) {
+ 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');
@@ -137,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'
);
}
}
@@ -151,11 +131,15 @@ class SystemTagNode implements \Sabre\DAV\INode {
/**
* Returns null, not supported
*
+ * @return null
*/
public function getLastModified() {
return null;
}
+ /**
+ * @return void
+ */
public function delete() {
try {
if (!$this->isAdmin) {
@@ -172,4 +156,47 @@ class SystemTagNode implements \Sabre\DAV\INode {
throw new NotFound('Tag with id ' . $this->tag->getId() . ' not found', 0, $e);
}
}
+
+ public function getNumberOfFiles(): int {
+ return $this->numberOfFiles;
+ }
+
+ public function setNumberOfFiles(int $numberOfFiles): void {
+ $this->numberOfFiles = $numberOfFiles;
+ }
+
+ public function getReferenceFileId(): int {
+ return $this->referenceFileId;
+ }
+
+ 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 b6bd7d3b7cd..4d4499c7559 100644
--- a/apps/dav/lib/SystemTag/SystemTagPlugin.php
+++ b/apps/dav/lib/SystemTag/SystemTagPlugin.php
@@ -1,35 +1,28 @@
<?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;
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;
use Sabre\DAV\Exception\Forbidden;
@@ -50,44 +43,36 @@ 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';
public const USERASSIGNABLE_PROPERTYNAME = '{http://owncloud.org/ns}user-assignable';
public const GROUPS_PROPERTYNAME = '{http://owncloud.org/ns}groups';
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 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;
-
- /**
- * @param ISystemTagManager $tagManager tag manager
- * @param IGroupManager $groupManager
- * @param IUserSession $userSession
- */
- public function __construct(ISystemTagManager $tagManager,
- IGroupManager $groupManager,
- IUserSession $userSession) {
- $this->tagManager = $tagManager;
- $this->userSession = $userSession;
- $this->groupManager = $groupManager;
+ /** @var array<int, string[]> */
+ private array $cachedTagMappings = [];
+ /** @var array<string, ISystemTag> */
+ private array $cachedTags = [];
+
+ public function __construct(
+ protected ISystemTagManager $tagManager,
+ protected IGroupManager $groupManager,
+ protected IUserSession $userSession,
+ protected IRootFolder $rootFolder,
+ protected ISystemTagObjectMapper $tagMapper,
+ ) {
}
/**
@@ -103,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;
@@ -145,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;
}
}
@@ -163,7 +151,7 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
*/
private function createTag($data, $contentType = 'application/json') {
if (explode(';', $contentType)[0] === 'application/json') {
- $data = json_decode($data, true);
+ $data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
} else {
throw new UnsupportedMediaType();
}
@@ -206,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);
}
}
@@ -215,15 +205,31 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
*
* @param PropFind $propFind
* @param \Sabre\DAV\INode $node
+ *
+ * @return void
*/
public function handleGetProperties(
PropFind $propFind,
- \Sabre\DAV\INode $node
+ \Sabre\DAV\INode $node,
) {
- if (!($node instanceof SystemTagNode) && !($node instanceof SystemTagMappingNode)) {
+ if ($node instanceof Node) {
+ $this->propfindForFile($propFind, $node);
+ return;
+ }
+
+ if (!$node instanceof SystemTagNode && !$node instanceof SystemTagMappingNode && !$node instanceof SystemTagObjectType) {
return;
}
+ // child nodes from systemtags-assigned should point to normal tag endpoint
+ if (preg_match('/^systemtags-assigned\/[0-9]+/', $propFind->getPath())) {
+ $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();
});
@@ -246,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
@@ -258,6 +268,106 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
}
return implode('|', $groups);
});
+
+ if ($node instanceof SystemTagNode) {
+ $propFind->handle(self::NUM_FILES_PROPERTYNAME, function () use ($node): int {
+ return $node->getNumberOfFiles();
+ });
+
+ $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()));
+ });
+ }
+ }
+
+ private function propfindForFile(PropFind $propFind, Node $node): void {
+ if ($node instanceof Directory
+ && $propFind->getDepth() !== 0
+ && !is_null($propFind->getStatus(self::SYSTEM_TAGS_PROPERTYNAME))) {
+ $fileIds = [$node->getId()];
+
+ // note: pre-fetching only supported for depth <= 1
+ $folderContent = $node->getChildren();
+ foreach ($folderContent as $info) {
+ if ($info instanceof Node) {
+ $fileIds[] = $info->getId();
+ }
+ }
+
+ $tags = $this->tagMapper->getTagIdsForObjects($fileIds, 'files');
+
+ $this->cachedTagMappings = $this->cachedTagMappings + $tags;
+ $emptyFileIds = array_diff($fileIds, array_keys($tags));
+
+ // also cache the ones that were not found
+ foreach ($emptyFileIds as $fileId) {
+ $this->cachedTagMappings[$fileId] = [];
+ }
+ }
+
+ $propFind->handle(self::SYSTEM_TAGS_PROPERTYNAME, function () use ($node) {
+ $user = $this->userSession->getUser();
+
+ $tags = $this->getTagsForFile($node->getId(), $user);
+ usort($tags, function (ISystemTag $tagA, ISystemTag $tagB): int {
+ return Util::naturalSortCompare($tagA->getName(), $tagB->getName());
+ });
+ return new SystemTagList($tags, $this->tagManager, $user);
+ });
+ }
+
+ /**
+ * @param int $fileId
+ * @return ISystemTag[]
+ */
+ private function getTagsForFile(int $fileId, ?IUser $user): array {
+ if (isset($this->cachedTagMappings[$fileId])) {
+ $tagIds = $this->cachedTagMappings[$fileId];
+ } else {
+ $tags = $this->tagMapper->getTagIdsForObjects([$fileId], 'files');
+ $fileTags = current($tags);
+ if ($fileTags) {
+ $tagIds = $fileTags;
+ } else {
+ $tagIds = [];
+ }
+ }
+
+ $tags = array_filter(array_map(function (string $tagId) {
+ return $this->cachedTags[$tagId] ?? null;
+ }, $tagIds));
+
+ $uncachedTagIds = array_filter($tagIds, function (string $tagId): bool {
+ return !isset($this->cachedTags[$tagId]);
+ });
+
+ if (count($uncachedTagIds)) {
+ $retrievedTags = $this->tagManager->getTagsByIds($uncachedTagIds);
+ foreach ($retrievedTags as $tag) {
+ $this->cachedTags[$tag->getId()] = $tag;
+ }
+ $tags += $retrievedTags;
+ }
+
+ return array_filter($tags, function (ISystemTag $tag) use ($user) {
+ return $this->tagManager->canUserSeeTag($tag, $user);
+ });
}
/**
@@ -270,20 +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::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;
@@ -304,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
@@ -315,11 +500,40 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
$this->tagManager->setTagGroups($tag, $groupIds);
}
+ 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 1256c58921e..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;
}
/**
@@ -84,7 +51,10 @@ class SystemTagsByIdCollection implements ICollection {
/**
* @param string $name
* @param resource|string $data Initial payload
+ *
* @throws Forbidden
+ *
+ * @return never
*/
public function createFile($name, $data = null) {
throw new Forbidden('Cannot create tags by id');
@@ -92,6 +62,8 @@ class SystemTagsByIdCollection implements ICollection {
/**
* @param string $name
+ *
+ * @return never
*/
public function createDirectory($name) {
throw new Forbidden('Permission denied to create collections');
@@ -99,6 +71,8 @@ class SystemTagsByIdCollection implements ICollection {
/**
* @param string $name
+ *
+ * @return SystemTagNode
*/
public function getChild($name) {
try {
@@ -115,6 +89,11 @@ class SystemTagsByIdCollection implements ICollection {
}
}
+ /**
+ * @return SystemTagNode[]
+ *
+ * @psalm-return array<SystemTagNode>
+ */
public function getChildren() {
$visibilityFilter = true;
if ($this->isAdmin()) {
@@ -145,14 +124,25 @@ class SystemTagsByIdCollection implements ICollection {
}
}
+ /**
+ * @return never
+ */
public function delete() {
throw new Forbidden('Permission denied to delete this collection');
}
+ /**
+ * @return string
+ *
+ * @psalm-return 'systemtags'
+ */
public function getName() {
return 'systemtags';
}
+ /**
+ * @return never
+ */
public function setName($name) {
throw new Forbidden('Permission denied to rename this collection');
}
@@ -160,7 +150,7 @@ class SystemTagsByIdCollection implements ICollection {
/**
* Returns the last modification time, as a unix timestamp
*
- * @return int
+ * @return null
*/
public function getLastModified() {
return null;
@@ -174,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
new file mode 100644
index 00000000000..f11482b04ee
--- /dev/null
+++ b/apps/dav/lib/SystemTag/SystemTagsInUseCollection.php
@@ -0,0 +1,85 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\SystemTag;
+
+use OC\SystemTag\SystemTag;
+use OC\SystemTag\SystemTagsInFilesDetector;
+use OC\User\NoUserException;
+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 SystemTagsInFilesDetector $systemTagsInFilesDetector;
+
+ /** @noinspection PhpMissingParentConstructorInspection */
+ public function __construct(
+ protected IUserSession $userSession,
+ protected IRootFolder $rootFolder,
+ protected ISystemTagManager $systemTagManager,
+ protected ISystemTagObjectMapper $tagMapper,
+ SystemTagsInFilesDetector $systemTagsInFilesDetector,
+ protected string $mediaType = '',
+ ) {
+ $this->systemTagsInFilesDetector = $systemTagsInFilesDetector;
+ $this->name = 'systemtags-assigned';
+ if ($this->mediaType != '') {
+ $this->name .= '/' . $this->mediaType;
+ }
+ }
+
+ public function setName($name): void {
+ throw new Forbidden('Permission denied to rename this collection');
+ }
+
+ public function getChild($name): self {
+ if ($this->mediaType !== '') {
+ throw new NotFound('Invalid media type');
+ }
+ return new self($this->userSession, $this->rootFolder, $this->systemTagManager, $this->tagMapper, $this->systemTagsInFilesDetector, $name);
+ }
+
+ /**
+ * @return SystemTagNode[]
+ * @throws NotPermittedException
+ * @throws Forbidden
+ */
+ public function getChildren(): array {
+ $user = $this->userSession->getUser();
+ $userFolder = null;
+ try {
+ if ($user) {
+ $userFolder = $this->rootFolder->getUserFolder($user->getUID());
+ }
+ } catch (NoUserException) {
+ // will throw a Sabre exception in the next step.
+ }
+ if ($user === null || $userFolder === null) {
+ throw new Forbidden('Permission denied to read this collection');
+ }
+
+ $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'], $tagData['etag'], $tagData['color']);
+ // read only, so we can submit the isAdmin parameter as false generally
+ $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 8bb34182b0c..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;
@@ -40,58 +22,19 @@ use Sabre\DAV\ICollection;
* Collection containing tags by object id
*/
class SystemTagsObjectMappingCollection implements ICollection {
-
- /**
- * @var string
- */
- private $objectId;
-
- /**
- * @var string
- */
- private $objectType;
-
- /**
- * @var ISystemTagManager
- */
- private $tagManager;
-
- /**
- * @var ISystemTagObjectMapper
- */
- private $tagMapper;
-
- /**
- * User
- *
- * @var IUser
- */
- private $user;
-
-
- /**
- * Constructor
- *
- * @param string $objectId object id
- * @param string $objectType object type
- * @param IUser $user user
- * @param ISystemTagManager $tagManager tag manager
- * @param ISystemTagObjectMapper $tagMapper tag mapper
- */
public function __construct(
- $objectId,
- $objectType,
- IUser $user,
- ISystemTagManager $tagManager,
- ISystemTagObjectMapper $tagMapper
+ private string $objectId,
+ private string $objectType,
+ private IUser $user,
+ private ISystemTagManager $tagManager,
+ private ISystemTagObjectMapper $tagMapper,
+ protected \Closure $childWriteAccessFunction,
) {
- $this->tagManager = $tagManager;
- $this->tagMapper = $tagMapper;
- $this->objectId = $objectId;
- $this->objectType = $objectType;
- $this->user = $user;
}
+ /**
+ * @return void
+ */
public function createFile($name, $data = null) {
$tagId = $name;
try {
@@ -103,17 +46,26 @@ class SystemTagsObjectMappingCollection implements ICollection {
if (!$this->tagManager->canUserAssignTag($tag, $this->user)) {
throw new Forbidden('No permission to assign tag ' . $tagId);
}
-
+ $writeAccessFunction = $this->childWriteAccessFunction;
+ if (!$writeAccessFunction($this->objectId)) {
+ throw new Forbidden('No permission to assign tag to ' . $this->objectId);
+ }
$this->tagMapper->assignTags($this->objectId, $this->objectType, $tagId);
} catch (TagNotFoundException $e) {
throw new PreconditionFailed('Tag with id ' . $tagId . ' does not exist, cannot assign');
}
}
+ /**
+ * @return never
+ */
public function createDirectory($name) {
throw new Forbidden('Permission denied to create collections');
}
+ /**
+ * @return SystemTagMappingNode
+ */
public function getChild($tagName) {
try {
if ($this->tagMapper->haveTag([$this->objectId], $this->objectType, $tagName, true)) {
@@ -131,6 +83,11 @@ class SystemTagsObjectMappingCollection implements ICollection {
}
}
+ /**
+ * @return SystemTagMappingNode[]
+ *
+ * @psalm-return list<SystemTagMappingNode>
+ */
public function getChildren() {
$tagIds = current($this->tagMapper->getTagIdsForObjects([$this->objectId], $this->objectType));
if (empty($tagIds)) {
@@ -168,6 +125,9 @@ class SystemTagsObjectMappingCollection implements ICollection {
}
}
+ /**
+ * @return never
+ */
public function delete() {
throw new Forbidden('Permission denied to delete this collection');
}
@@ -176,6 +136,9 @@ class SystemTagsObjectMappingCollection implements ICollection {
return $this->objectId;
}
+ /**
+ * @return never
+ */
public function setName($name) {
throw new Forbidden('Permission denied to rename this collection');
}
@@ -183,7 +146,7 @@ class SystemTagsObjectMappingCollection implements ICollection {
/**
* Returns the last modification time, as a unix timestamp
*
- * @return int
+ * @return null
*/
public function getLastModified() {
return null;
@@ -204,7 +167,8 @@ class SystemTagsObjectMappingCollection implements ICollection {
$this->objectType,
$this->user,
$this->tagManager,
- $this->tagMapper
+ $this->tagMapper,
+ $this->childWriteAccessFunction,
);
}
}
diff --git a/apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php b/apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php
index 1ca45c32ce4..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;
@@ -38,67 +20,23 @@ use Sabre\DAV\ICollection;
* Collection containing object ids by object type
*/
class SystemTagsObjectTypeCollection implements ICollection {
-
- /**
- * @var string
- */
- private $objectType;
-
- /**
- * @var ISystemTagManager
- */
- private $tagManager;
-
- /**
- * @var ISystemTagObjectMapper
- */
- private $tagMapper;
-
- /**
- * @var IGroupManager
- */
- private $groupManager;
-
- /**
- * @var IUserSession
- */
- private $userSession;
-
- /**
- * @var \Closure
- **/
- protected $childExistsFunction;
-
- /**
- * Constructor
- *
- * @param string $objectType object type
- * @param ISystemTagManager $tagManager
- * @param ISystemTagObjectMapper $tagMapper
- * @param IUserSession $userSession
- * @param IGroupManager $groupManager
- * @param \Closure $childExistsFunction
- */
public function __construct(
- $objectType,
- ISystemTagManager $tagManager,
- ISystemTagObjectMapper $tagMapper,
- IUserSession $userSession,
- IGroupManager $groupManager,
- \Closure $childExistsFunction
+ private string $objectType,
+ private ISystemTagManager $tagManager,
+ private ISystemTagObjectMapper $tagMapper,
+ private IUserSession $userSession,
+ private IGroupManager $groupManager,
+ protected \Closure $childExistsFunction,
+ protected \Closure $childWriteAccessFunction,
) {
- $this->tagManager = $tagManager;
- $this->tagMapper = $tagMapper;
- $this->objectType = $objectType;
- $this->userSession = $userSession;
- $this->groupManager = $groupManager;
- $this->childExistsFunction = $childExistsFunction;
}
/**
* @param string $name
* @param resource|string $data Initial payload
- * @return null|string
+ *
+ * @return never
+ *
* @throws Forbidden
*/
public function createFile($name, $data = null) {
@@ -107,7 +45,10 @@ class SystemTagsObjectTypeCollection implements ICollection {
/**
* @param string $name
+ *
* @throws Forbidden
+ *
+ * @return never
*/
public function createDirectory($name) {
throw new Forbidden('Permission denied to create collections');
@@ -129,10 +70,14 @@ class SystemTagsObjectTypeCollection implements ICollection {
$this->objectType,
$this->userSession->getUser(),
$this->tagManager,
- $this->tagMapper
+ $this->tagMapper,
+ $this->childWriteAccessFunction,
);
}
+ /**
+ * @return never
+ */
public function getChildren() {
// do not list object ids
throw new MethodNotAllowed();
@@ -148,6 +93,9 @@ class SystemTagsObjectTypeCollection implements ICollection {
return call_user_func($this->childExistsFunction, $name);
}
+ /**
+ * @return never
+ */
public function delete() {
throw new Forbidden('Permission denied to delete this collection');
}
@@ -158,7 +106,10 @@ class SystemTagsObjectTypeCollection implements ICollection {
/**
* @param string $name
+ *
* @throws Forbidden
+ *
+ * @return never
*/
public function setName($name) {
throw new Forbidden('Permission denied to rename this collection');
@@ -167,7 +118,7 @@ class SystemTagsObjectTypeCollection implements ICollection {
/**
* Returns the last modification time, as a unix timestamp
*
- * @return int
+ * @return null
*/
public function getLastModified() {
return null;
diff --git a/apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php b/apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php
index 4c179d5f0f6..0839a5bc995 100644
--- a/apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php
+++ b/apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php
@@ -1,31 +1,15 @@
<?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;
+use OCP\Constants;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\IRootFolder;
use OCP\IGroupManager;
use OCP\IUserSession;
use OCP\SystemTag\ISystemTagManager;
@@ -33,42 +17,54 @@ use OCP\SystemTag\ISystemTagObjectMapper;
use OCP\SystemTag\SystemTagsEntityEvent;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\SimpleCollection;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class SystemTagsRelationsCollection extends SimpleCollection {
-
- /**
- * SystemTagsRelationsCollection constructor.
- *
- * @param ISystemTagManager $tagManager
- * @param ISystemTagObjectMapper $tagMapper
- * @param IUserSession $userSession
- * @param IGroupManager $groupManager
- * @param EventDispatcherInterface $dispatcher
- */
public function __construct(
ISystemTagManager $tagManager,
ISystemTagObjectMapper $tagMapper,
IUserSession $userSession,
IGroupManager $groupManager,
- EventDispatcherInterface $dispatcher
+ IEventDispatcher $dispatcher,
+ IRootFolder $rootFolder,
) {
$children = [
+ // Only files are supported at the moment
+ // Also see SystemTagPlugin::OBJECTIDS_PROPERTYNAME supported types
new SystemTagsObjectTypeCollection(
'files',
$tagManager,
$tagMapper,
$userSession,
$groupManager,
- function ($name) {
- $nodes = \OC::$server->getUserFolder()->getById((int)$name);
- return !empty($nodes);
- }
+ function (string $name) use ($rootFolder, $userSession): bool {
+ $user = $userSession->getUser();
+ if ($user) {
+ $node = $rootFolder->getUserFolder($user->getUID())->getFirstNodeById((int)$name);
+ return $node !== null;
+ } else {
+ return false;
+ }
+ },
+ function (string $name) use ($rootFolder, $userSession): bool {
+ $user = $userSession->getUser();
+ if ($user) {
+ $nodes = $rootFolder->getUserFolder($user->getUID())->getById((int)$name);
+ foreach ($nodes as $node) {
+ if (($node->getPermissions() & Constants::PERMISSION_UPDATE) === Constants::PERMISSION_UPDATE) {
+ return true;
+ }
+ }
+ return false;
+ } else {
+ return false;
+ }
+ },
),
];
- $event = new SystemTagsEntityEvent(SystemTagsEntityEvent::EVENT_ENTITY);
+ $event = new SystemTagsEntityEvent();
$dispatcher->dispatch(SystemTagsEntityEvent::EVENT_ENTITY, $event);
+ $dispatcher->dispatchTyped($event);
foreach ($event->getEntityCollections() as $entity => $entityExistsFunction) {
$children[] = new SystemTagsObjectTypeCollection(
@@ -77,7 +73,8 @@ class SystemTagsRelationsCollection extends SimpleCollection {
$tagMapper,
$userSession,
$groupManager,
- $entityExistsFunction
+ $entityExistsFunction,
+ fn ($name) => true,
);
}
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
new file mode 100644
index 00000000000..07452dc0593
--- /dev/null
+++ b/apps/dav/lib/Upload/ChunkingV2Plugin.php
@@ -0,0 +1,385 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Upload;
+
+use Exception;
+use InvalidArgumentException;
+use OC\Files\Filesystem;
+use OC\Files\ObjectStore\ObjectStoreStorage;
+use OC\Files\View;
+use OC\Memcache\Memcached;
+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;
+use OCP\Files\Storage\IChunkedFileWrite;
+use OCP\Files\StorageInvalidException;
+use OCP\ICache;
+use OCP\ICacheFactory;
+use OCP\IConfig;
+use OCP\Lock\ILockingProvider;
+use Sabre\DAV\Exception\BadRequest;
+use Sabre\DAV\Exception\InsufficientStorage;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\Exception\PreconditionFailed;
+use Sabre\DAV\ICollection;
+use Sabre\DAV\INode;
+use Sabre\DAV\Server;
+use Sabre\DAV\ServerPlugin;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Sabre\Uri;
+
+class ChunkingV2Plugin extends ServerPlugin {
+ /** @var Server */
+ private $server;
+ /** @var UploadFolder */
+ private $uploadFolder;
+ /** @var ICache */
+ private $cache;
+
+ private ?string $uploadId = null;
+ private ?string $uploadPath = null;
+
+ private const TEMP_TARGET = '.target';
+
+ public const CACHE_KEY = 'chunking-v2';
+ public const UPLOAD_TARGET_PATH = 'upload-target-path';
+ public const UPLOAD_TARGET_ID = 'upload-target-id';
+ public const UPLOAD_ID = 'upload-id';
+
+ private const DESTINATION_HEADER = 'Destination';
+
+ public function __construct(ICacheFactory $cacheFactory) {
+ $this->cache = $cacheFactory->createDistributed(self::CACHE_KEY);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function initialize(Server $server) {
+ $server->on('afterMethod:MKCOL', [$this, 'afterMkcol']);
+ $server->on('beforeMethod:PUT', [$this, 'beforePut']);
+ $server->on('beforeMethod:DELETE', [$this, 'beforeDelete']);
+ $server->on('beforeMove', [$this, 'beforeMove'], 90);
+
+ $this->server = $server;
+ }
+
+ /**
+ * @param string $path
+ * @param bool $createIfNotExists
+ * @return FutureFile|UploadFile|ICollection|INode
+ */
+ private function getUploadFile(string $path, bool $createIfNotExists = false) {
+ try {
+ $actualFile = $this->server->tree->getNodeForPath($path);
+ // Only directly upload to the target file if it is on the same storage
+ // There may be further potential to optimize here by also uploading
+ // to other storages directly. This would require to also carefully pick
+ // the storage/path used in getStorage()
+ if ($actualFile instanceof File && $this->uploadFolder->getStorage()->getId() === $actualFile->getNode()->getStorage()->getId()) {
+ return $actualFile;
+ }
+ } catch (NotFound $e) {
+ // If there is no target file we upload to the upload folder first
+ }
+
+ // Use file in the upload directory that will be copied or moved afterwards
+ if ($createIfNotExists) {
+ $this->uploadFolder->createFile(self::TEMP_TARGET);
+ }
+
+ /** @var UploadFile $uploadFile */
+ $uploadFile = $this->uploadFolder->getChild(self::TEMP_TARGET);
+ return $uploadFile->getFile();
+ }
+
+ public function afterMkcol(RequestInterface $request, ResponseInterface $response): bool {
+ try {
+ $this->prepareUpload($request->getPath());
+ $this->checkPrerequisites(false);
+ } catch (BadRequest|StorageInvalidException|NotFound $e) {
+ return true;
+ }
+
+ $this->uploadPath = $this->server->calculateUri($this->server->httpRequest->getHeader(self::DESTINATION_HEADER));
+ $targetFile = $this->getUploadFile($this->uploadPath, true);
+ [$storage, $storagePath] = $this->getUploadStorage($this->uploadPath);
+
+ $this->uploadId = $storage->startChunkedWrite($storagePath);
+
+ $this->cache->set($this->uploadFolder->getName(), [
+ self::UPLOAD_ID => $this->uploadId,
+ self::UPLOAD_TARGET_PATH => $this->uploadPath,
+ self::UPLOAD_TARGET_ID => $targetFile->getId(),
+ ], 86400);
+
+ $response->setStatus(Http::STATUS_CREATED);
+ return true;
+ }
+
+ public function beforePut(RequestInterface $request, ResponseInterface $response): bool {
+ try {
+ $this->prepareUpload(dirname($request->getPath()));
+ $this->checkPrerequisites();
+ } catch (StorageInvalidException|BadRequest|NotFound $e) {
+ return true;
+ }
+
+ [$storage, $storagePath] = $this->getUploadStorage($this->uploadPath);
+
+ $chunkName = basename($request->getPath());
+ $partId = is_numeric($chunkName) ? (int)$chunkName : -1;
+ if (!($partId >= 1 && $partId <= 10000)) {
+ throw new BadRequest('Invalid chunk name, must be numeric between 1 and 10000');
+ }
+
+ $uploadFile = $this->getUploadFile($this->uploadPath);
+ $tempTargetFile = null;
+
+ $additionalSize = (int)$request->getHeader('Content-Length');
+ if ($this->uploadFolder->childExists(self::TEMP_TARGET) && $this->uploadPath) {
+ /** @var UploadFile $tempTargetFile */
+ $tempTargetFile = $this->uploadFolder->getChild(self::TEMP_TARGET);
+ [$destinationDir, $destinationName] = Uri\split($this->uploadPath);
+ /** @var Directory $destinationParent */
+ $destinationParent = $this->server->tree->getNodeForPath($destinationDir);
+ $free = $destinationParent->getNode()->getFreeSpace();
+ $newSize = $tempTargetFile->getSize() + $additionalSize;
+ if ($free >= 0 && ($tempTargetFile->getSize() > $free || $newSize > $free)) {
+ throw new InsufficientStorage("Insufficient space in $this->uploadPath");
+ }
+ }
+
+ $stream = $request->getBodyAsStream();
+ $storage->putChunkedWritePart($storagePath, $this->uploadId, (string)$partId, $stream, $additionalSize);
+
+ $storage->getCache()->update($uploadFile->getId(), ['size' => $uploadFile->getSize() + $additionalSize]);
+ if ($tempTargetFile) {
+ $storage->getPropagator()->propagateChange($tempTargetFile->getInternalPath(), time(), $additionalSize);
+ }
+
+ $response->setStatus(201);
+ return false;
+ }
+
+ public function beforeMove($sourcePath, $destination): bool {
+ try {
+ $this->prepareUpload(dirname($sourcePath));
+ $this->checkPrerequisites();
+ } catch (StorageInvalidException|BadRequest|NotFound|PreconditionFailed $e) {
+ return true;
+ }
+ [$storage, $storagePath] = $this->getUploadStorage($this->uploadPath);
+
+ $targetFile = $this->getUploadFile($this->uploadPath);
+
+ [$destinationDir, $destinationName] = Uri\split($destination);
+ /** @var Directory $destinationParent */
+ $destinationParent = $this->server->tree->getNodeForPath($destinationDir);
+ $destinationExists = $destinationParent->childExists($destinationName);
+
+
+ // allow sync clients to send the modification and creation time along in a header
+ $updateFileInfo = [];
+ if ($this->server->httpRequest->getHeader('X-OC-MTime') !== null) {
+ $updateFileInfo['mtime'] = $this->sanitizeMtime($this->server->httpRequest->getHeader('X-OC-MTime'));
+ $this->server->httpResponse->setHeader('X-OC-MTime', 'accepted');
+ }
+ if ($this->server->httpRequest->getHeader('X-OC-CTime') !== null) {
+ $updateFileInfo['creation_time'] = $this->sanitizeMtime($this->server->httpRequest->getHeader('X-OC-CTime'));
+ $this->server->httpResponse->setHeader('X-OC-CTime', 'accepted');
+ }
+ $updateFileInfo['mimetype'] = \OCP\Server::get(IMimeTypeDetector::class)->detectPath($destinationName);
+
+ if ($storage->instanceOfStorage(ObjectStoreStorage::class) && $storage->getObjectStore() instanceof IObjectStoreMultiPartUpload) {
+ /** @var ObjectStoreStorage $storage */
+ /** @var IObjectStoreMultiPartUpload $objectStore */
+ $objectStore = $storage->getObjectStore();
+ $parts = $objectStore->getMultipartUploads($storage->getURN($targetFile->getId()), $this->uploadId);
+ $size = 0;
+ foreach ($parts as $part) {
+ $size += $part['Size'];
+ }
+ $free = $destinationParent->getNode()->getFreeSpace();
+ if ($free >= 0 && ($size > $free)) {
+ throw new InsufficientStorage("Insufficient space in $this->uploadPath");
+ }
+ }
+
+ $destinationInView = $destinationParent->getFileInfo()->getPath() . '/' . $destinationName;
+ $this->completeChunkedWrite($destinationInView);
+
+ $rootView = new View();
+ $rootView->putFileInfo($destinationInView, $updateFileInfo);
+
+ $sourceNode = $this->server->tree->getNodeForPath($sourcePath);
+ if ($sourceNode instanceof FutureFile) {
+ $this->uploadFolder->delete();
+ }
+
+ $this->server->emit('afterMove', [$sourcePath, $destination]);
+ $this->server->emit('afterUnbind', [$sourcePath]);
+ $this->server->emit('afterBind', [$destination]);
+
+ $response = $this->server->httpResponse;
+ $response->setHeader('Content-Type', 'application/xml; charset=utf-8');
+ $response->setHeader('Content-Length', '0');
+ $response->setStatus($destinationExists ? Http::STATUS_NO_CONTENT : Http::STATUS_CREATED);
+ return false;
+ }
+
+ public function beforeDelete(RequestInterface $request, ResponseInterface $response) {
+ try {
+ $this->prepareUpload(dirname($request->getPath()));
+ $this->checkPrerequisites();
+ } catch (StorageInvalidException|BadRequest|NotFound $e) {
+ return true;
+ }
+
+ [$storage, $storagePath] = $this->getUploadStorage($this->uploadPath);
+ $storage->cancelChunkedWrite($storagePath, $this->uploadId);
+ return true;
+ }
+
+ /**
+ * @throws BadRequest
+ * @throws PreconditionFailed
+ * @throws StorageInvalidException
+ */
+ private function checkPrerequisites(bool $checkUploadMetadata = true): void {
+ $distributedCacheConfig = \OCP\Server::get(IConfig::class)->getSystemValue('memcache.distributed', null);
+
+ if ($distributedCacheConfig === null || (!$this->cache instanceof Redis && !$this->cache instanceof Memcached)) {
+ throw new BadRequest('Skipping chunking v2 since no proper distributed cache is available');
+ }
+ if (!$this->uploadFolder instanceof UploadFolder || empty($this->server->httpRequest->getHeader(self::DESTINATION_HEADER))) {
+ throw new BadRequest('Skipping chunked file writing as the destination header was not passed');
+ }
+ if (!$this->uploadFolder->getStorage()->instanceOfStorage(IChunkedFileWrite::class)) {
+ throw new StorageInvalidException('Storage does not support chunked file writing');
+ }
+ if ($this->uploadFolder->getStorage()->instanceOfStorage(ObjectStoreStorage::class) && !$this->uploadFolder->getStorage()->getObjectStore() instanceof IObjectStoreMultiPartUpload) {
+ throw new StorageInvalidException('Storage does not support multi part uploads');
+ }
+
+ if ($checkUploadMetadata) {
+ if ($this->uploadId === null || $this->uploadPath === null) {
+ throw new PreconditionFailed('Missing metadata for chunked upload. The distributed cache does not hold the information of previous requests.');
+ }
+ }
+ }
+
+ /**
+ * @return array [IStorage, string]
+ */
+ private function getUploadStorage(string $targetPath): array {
+ $storage = $this->uploadFolder->getStorage();
+ $targetFile = $this->getUploadFile($targetPath);
+ return [$storage, $targetFile->getInternalPath()];
+ }
+
+ protected function sanitizeMtime(string $mtimeFromRequest): int {
+ if (!is_numeric($mtimeFromRequest)) {
+ throw new InvalidArgumentException('X-OC-MTime header must be an integer (unix timestamp).');
+ }
+
+ return (int)$mtimeFromRequest;
+ }
+
+ /**
+ * @throws NotFound
+ */
+ public function prepareUpload($path): void {
+ $this->uploadFolder = $this->server->tree->getNodeForPath($path);
+ $uploadMetadata = $this->cache->get($this->uploadFolder->getName());
+ $this->uploadId = $uploadMetadata[self::UPLOAD_ID] ?? null;
+ $this->uploadPath = $uploadMetadata[self::UPLOAD_TARGET_PATH] ?? null;
+ }
+
+ private function completeChunkedWrite(string $targetAbsolutePath): void {
+ $uploadFile = $this->getUploadFile($this->uploadPath)->getNode();
+ [$storage, $storagePath] = $this->getUploadStorage($this->uploadPath);
+
+ $rootFolder = \OCP\Server::get(IRootFolder::class);
+ $exists = $rootFolder->nodeExists($targetAbsolutePath);
+
+ $uploadFile->lock(ILockingProvider::LOCK_SHARED);
+ $this->emitPreHooks($targetAbsolutePath, $exists);
+ try {
+ $uploadFile->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
+ $storage->completeChunkedWrite($storagePath, $this->uploadId);
+ $uploadFile->changeLock(ILockingProvider::LOCK_SHARED);
+ } catch (Exception $e) {
+ $uploadFile->unlock(ILockingProvider::LOCK_EXCLUSIVE);
+ throw $e;
+ }
+
+ // If the file was not uploaded to the user storage directly we need to copy/move it
+ try {
+ $uploadFileAbsolutePath = $uploadFile->getFileInfo()->getPath();
+ if ($uploadFileAbsolutePath !== $targetAbsolutePath) {
+ $uploadFile = $rootFolder->get($uploadFile->getFileInfo()->getPath());
+ if ($exists) {
+ $uploadFile->copy($targetAbsolutePath);
+ } else {
+ $uploadFile->move($targetAbsolutePath);
+ }
+ }
+ $this->emitPostHooks($targetAbsolutePath, $exists);
+ } catch (Exception $e) {
+ $uploadFile->unlock(ILockingProvider::LOCK_SHARED);
+ throw $e;
+ }
+ }
+
+ private function emitPreHooks(string $target, bool $exists): void {
+ $hookPath = $this->getHookPath($target);
+ if (!$exists) {
+ OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_create, [
+ Filesystem::signal_param_path => $hookPath,
+ ]);
+ } else {
+ OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_update, [
+ Filesystem::signal_param_path => $hookPath,
+ ]);
+ }
+ OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_write, [
+ Filesystem::signal_param_path => $hookPath,
+ ]);
+ }
+
+ private function emitPostHooks(string $target, bool $exists): void {
+ $hookPath = $this->getHookPath($target);
+ if (!$exists) {
+ OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_create, [
+ Filesystem::signal_param_path => $hookPath,
+ ]);
+ } else {
+ OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_update, [
+ Filesystem::signal_param_path => $hookPath,
+ ]);
+ }
+ OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_write, [
+ Filesystem::signal_param_path => $hookPath,
+ ]);
+ }
+
+ private function getHookPath(string $path): ?string {
+ if (!Filesystem::getView()) {
+ return $path;
+ }
+ return Filesystem::getView()->getRelativePath($path);
+ }
+}
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 eba550a62da..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,19 +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,
+ ) {
}
/**
@@ -66,6 +45,10 @@ class FutureFile implements \Sabre\DAV\IFile {
return AssemblyStream::wrap($nodes);
}
+ public function getPath() {
+ return $this->root->getFileInfo()->getInternalPath() . '/.file';
+ }
+
/**
* @inheritdoc
*/
diff --git a/apps/dav/lib/Upload/PartFile.php b/apps/dav/lib/Upload/PartFile.php
new file mode 100644
index 00000000000..11900997a90
--- /dev/null
+++ b/apps/dav/lib/Upload/PartFile.php
@@ -0,0 +1,91 @@
+<?php
+
+/**
+ * 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;
+
+use OCA\DAV\Connector\Sabre\Directory;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\IFile;
+
+/**
+ * This class represents an Upload part which is not present on the storage itself
+ * but handled directly by external storage services like S3 with Multipart Upload
+ */
+class PartFile implements IFile {
+ public function __construct(
+ private Directory $root,
+ private array $partInfo,
+ ) {
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function put($data) {
+ throw new Forbidden('Permission denied to put into this file');
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function get() {
+ throw new Forbidden('Permission denied to get this file');
+ }
+
+ public function getPath() {
+ return $this->root->getFileInfo()->getInternalPath() . '/' . $this->partInfo['PartNumber'];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getContentType() {
+ return 'application/octet-stream';
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getETag() {
+ return $this->partInfo['ETag'];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getSize() {
+ return $this->partInfo['Size'];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function delete() {
+ $this->root->delete();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getName() {
+ return $this->partInfo['PartNumber'];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setName($name) {
+ throw new Forbidden('Permission denied to rename this file');
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getLastModified() {
+ return $this->partInfo['LastModified'];
+ }
+}
diff --git a/apps/dav/lib/Upload/RootCollection.php b/apps/dav/lib/Upload/RootCollection.php
index e3ae22af5e9..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,
- string $principalPrefix,
- CleanupService $cleanupService) {
+ public function __construct(
+ PrincipalBackend\BackendInterface $principalBackend,
+ string $principalPrefix,
+ 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 49a2fadecf6..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,12 +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) {
@@ -45,6 +25,10 @@ class UploadFile implements IFile {
return $this->file->get();
}
+ public function getId() {
+ return $this->file->getId();
+ }
+
public function getContentType() {
return $this->file->getContentType();
}
@@ -53,6 +37,10 @@ class UploadFile implements IFile {
return $this->file->getETag();
}
+ /**
+ * @psalm-suppress ImplementedReturnTypeMismatch \Sabre\DAV\IFile::getSize signature does not support 32bit
+ * @return int|float
+ */
public function getSize() {
return $this->file->getSize();
}
@@ -72,4 +60,16 @@ class UploadFile implements IFile {
public function getLastModified() {
return $this->file->getLastModified();
}
+
+ public function getInternalPath(): string {
+ return $this->file->getInternalPath();
+ }
+
+ public function getFile(): File {
+ return $this->file;
+ }
+
+ public function getNode() {
+ return $this->file->getNode();
+ }
}
diff --git a/apps/dav/lib/Upload/UploadFolder.php b/apps/dav/lib/Upload/UploadFolder.php
index bb7c494cee3..8890d472f87 100644
--- a/apps/dav/lib/Upload/UploadFolder.php
+++ b/apps/dav/lib/Upload/UploadFolder.php
@@ -1,48 +1,41 @@
<?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;
+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;
-
- public function __construct(Directory $node, CleanupService $cleanupService) {
- $this->node = $node;
- $this->cleanupService = $cleanupService;
+ public function __construct(
+ private Directory $node,
+ private CleanupService $cleanupService,
+ private IStorage $storage,
+ private string $uid,
+ ) {
}
public function createFile($name, $data = null) {
// TODO: verify name - should be a simple number
- $this->node->createFile($name, $data);
+ try {
+ $this->node->createFile($name, $data);
+ } catch (\Exception $e) {
+ if ($this->node->childExists($name)) {
+ $child = $this->node->getChild($name);
+ $child->delete();
+ }
+ throw $e;
+ }
}
public function createDirectory($name) {
@@ -66,6 +59,23 @@ class UploadFolder implements ICollection {
$children[] = new UploadFile($child);
}
+ if ($this->storage->instanceOfStorage(ObjectStoreStorage::class)) {
+ /** @var ObjectStoreStorage $storage */
+ $objectStore = $this->storage->getObjectStore();
+ if ($objectStore instanceof IObjectStoreMultiPartUpload) {
+ $cache = Server::get(ICacheFactory::class)->createDistributed(ChunkingV2Plugin::CACHE_KEY);
+ $uploadSession = $cache->get($this->getName());
+ if ($uploadSession) {
+ $uploadId = $uploadSession[ChunkingV2Plugin::UPLOAD_ID];
+ $id = $uploadSession[ChunkingV2Plugin::UPLOAD_TARGET_ID];
+ $parts = $objectStore->getMultipartUploads($this->storage->getURN($id), $uploadId);
+ foreach ($parts as $part) {
+ $children[] = new PartFile($this->node, $part);
+ }
+ }
+ }
+ }
+
return $children;
}
@@ -80,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() {
@@ -94,4 +104,8 @@ class UploadFolder implements ICollection {
public function getLastModified() {
return $this->node->getLastModified();
}
+
+ public function getStorage() {
+ return $this->storage;
+ }
}
diff --git a/apps/dav/lib/Upload/UploadHome.php b/apps/dav/lib/Upload/UploadHome.php
index 35d47b6a82a..4042f1c4101 100644
--- a/apps/dav/lib/Upload/UploadHome.php
+++ b/apps/dav/lib/Upload/UploadHome.php
@@ -1,46 +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 {
+ private string $uid;
+ private ?Folder $uploadFolder = null;
- /** @var array */
- private $principalInfo;
- /** @var CleanupService */
- private $cleanupService;
+ 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');
+ }
- public function __construct(array $principalInfo, CleanupService $cleanupService) {
- $this->principalInfo = $principalInfo;
- $this->cleanupService = $cleanupService;
+ $this->uid = $user->getUID();
+ }
}
public function createFile($name, $data = null) {
@@ -51,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);
+ 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);
+ return new UploadFolder(
+ $node,
+ $this->cleanupService,
+ $this->getStorage(),
+ $this->uid,
+ );
}, $this->impl()->getChildren());
}
@@ -85,18 +92,29 @@ class UploadHome implements ICollection {
return $this->impl()->getLastModified();
}
- /**
- * @return Directory
- */
- private function impl() {
- $rootView = new View();
- $user = \OC::$server->getUserSession()->getUser();
- Filesystem::initMountPoints($user->getUID());
- if (!$rootView->file_exists('/' . $user->getUID() . '/uploads')) {
- $rootView->mkdir('/' . $user->getUID() . '/uploads');
+ 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);
+ }
}
- $view = new View('/' . $user->getUID() . '/uploads');
- $rootInfo = $view->getFileInfo('');
- return new Directory($view, $rootInfo);
+ return $this->uploadFolder;
+ }
+
+ private function impl(): Directory {
+ $folder = $this->getUploadFolder();
+ $view = new View($folder->getPath());
+ return new Directory($view, $folder);
+ }
+
+ private function getStorage() {
+ return $this->getUploadFolder()->getStorage();
}
}
diff --git a/apps/dav/lib/UserMigration/CalendarMigrator.php b/apps/dav/lib/UserMigration/CalendarMigrator.php
index 015ce6faa86..73e9c375490 100644
--- a/apps/dav/lib/UserMigration/CalendarMigrator.php
+++ b/apps/dav/lib/UserMigration/CalendarMigrator.php
@@ -3,30 +3,12 @@
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;
-use function Safe\substr;
use OCA\DAV\AppInfo\Application;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\ICSExportPlugin\ICSExportPlugin;
@@ -42,6 +24,7 @@ use OCP\IUser;
use OCP\UserMigration\IExportDestination;
use OCP\UserMigration\IImportSource;
use OCP\UserMigration\IMigrator;
+use OCP\UserMigration\ISizeEstimationMigrator;
use OCP\UserMigration\TMigratorBasicVersionHandling;
use Sabre\VObject\Component as VObjectComponent;
use Sabre\VObject\Component\VCalendar;
@@ -49,25 +32,15 @@ use Sabre\VObject\Component\VTimeZone;
use Sabre\VObject\Property\ICalendar\DateTime;
use Sabre\VObject\Reader as VObjectReader;
use Sabre\VObject\UUIDUtil;
-use Safe\Exceptions\StringsException;
+use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
+use function substr;
-class CalendarMigrator implements IMigrator {
+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/';
@@ -79,18 +52,12 @@ class CalendarMigrator implements IMigrator {
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());
@@ -181,14 +148,18 @@ class CalendarMigrator implements IMigrator {
)));
}
+ /**
+ * @throws InvalidCalendarException
+ */
private function getUniqueCalendarUri(IUser $user, string $initialCalendarUri): string {
$principalUri = $this->getPrincipalUri($user);
- try {
- $initialCalendarUri = substr($initialCalendarUri, 0, strlen(CalendarMigrator::MIGRATED_URI_PREFIX)) === CalendarMigrator::MIGRATED_URI_PREFIX
- ? $initialCalendarUri
- : CalendarMigrator::MIGRATED_URI_PREFIX . $initialCalendarUri;
- } catch (StringsException $e) {
- throw new CalendarMigratorException('Failed to get unique calendar URI', 0, $e);
+
+ $initialCalendarUri = substr($initialCalendarUri, 0, strlen(CalendarMigrator::MIGRATED_URI_PREFIX)) === CalendarMigrator::MIGRATED_URI_PREFIX
+ ? $initialCalendarUri
+ : CalendarMigrator::MIGRATED_URI_PREFIX . $initialCalendarUri;
+
+ if ($initialCalendarUri === '') {
+ throw new InvalidCalendarException();
}
$existingCalendarUris = array_map(
@@ -209,6 +180,31 @@ class CalendarMigrator implements IMigrator {
/**
* {@inheritDoc}
*/
+ public function getEstimatedExportSize(IUser $user): int|float {
+ $calendarExports = $this->getCalendarExports($user, new NullOutput());
+ $calendarCount = count($calendarExports);
+
+ // 150B for top-level properties
+ $size = ($calendarCount * 150) / 1024;
+
+ $componentCount = array_sum(array_map(
+ function (array $data): int {
+ /** @var VCalendar $vCalendar */
+ $vCalendar = $data['vCalendar'];
+ return count($vCalendar->getComponents());
+ },
+ $calendarExports,
+ ));
+
+ // 450B for each component (events, todos, alarms, etc.)
+ $size += ($componentCount * 450) / 1024;
+
+ return ceil($size);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
public function export(IUser $user, IExportDestination $exportDestination, OutputInterface $output): void {
$output->writeln('Exporting calendars into ' . CalendarMigrator::EXPORT_ROOT . '…');
@@ -430,17 +426,20 @@ class CalendarMigrator implements IMigrator {
VObjectReader::OPTION_FORGIVING,
);
} catch (Throwable $e) {
- throw new CalendarMigratorException("Failed to read file \"$importPath\"", 0, $e);
+ $output->writeln("Failed to read file \"$importPath\", skipping…");
+ continue;
}
$problems = $vCalendar->validate();
if (!empty($problems)) {
- throw new CalendarMigratorException("Invalid calendar data contained in \"$importPath\"");
+ $output->writeln("Invalid calendar data contained in \"$importPath\", skipping…");
+ continue;
}
$splitFilename = explode('.', $filename, 2);
if (count($splitFilename) !== 2) {
- throw new CalendarMigratorException("Invalid filename \"$filename\", expected filename of the format \"<calendar_name>" . CalendarMigrator::FILENAME_EXT . '"');
+ $output->writeln("Invalid filename \"$filename\", expected filename of the format \"<calendar_name>" . CalendarMigrator::FILENAME_EXT . '", skipping…');
+ continue;
}
[$initialCalendarUri, $ext] = $splitFilename;
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 aed41e5c82f..96d623938a3 100644
--- a/apps/dav/lib/UserMigration/ContactsMigrator.php
+++ b/apps/dav/lib/UserMigration/ContactsMigrator.php
@@ -3,31 +3,12 @@
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;
-use function Safe\sort;
-use function Safe\substr;
use OCA\DAV\AppInfo\Application;
use OCA\DAV\CardDAV\CardDavBackend;
use OCA\DAV\CardDAV\Plugin as CardDAVPlugin;
@@ -39,25 +20,23 @@ use OCP\IUser;
use OCP\UserMigration\IExportDestination;
use OCP\UserMigration\IImportSource;
use OCP\UserMigration\IMigrator;
+use OCP\UserMigration\ISizeEstimationMigrator;
use OCP\UserMigration\TMigratorBasicVersionHandling;
use Sabre\VObject\Component\VCard;
use Sabre\VObject\Parser\Parser as VObjectParser;
use Sabre\VObject\Reader as VObjectReader;
use Sabre\VObject\Splitter\VCard as VCardSplitter;
use Sabre\VObject\UUIDUtil;
-use Safe\Exceptions\ArrayException;
-use Safe\Exceptions\StringsException;
+use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
+use function sort;
+use function substr;
-class ContactsMigrator implements IMigrator {
+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 {
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());
@@ -129,6 +105,10 @@ class ContactsMigrator implements IMigrator {
}
}
+ if (count($vCards) === 0) {
+ throw new InvalidAddressBookException();
+ }
+
return [
'name' => $addressBookNode->getName(),
'displayName' => $addressBookInfo['{DAV:}displayname'],
@@ -156,15 +136,18 @@ class ContactsMigrator implements IMigrator {
)));
}
+ /**
+ * @throws InvalidAddressBookException
+ */
private function getUniqueAddressBookUri(IUser $user, string $initialAddressBookUri): string {
$principalUri = $this->getPrincipalUri($user);
- try {
- $initialAddressBookUri = substr($initialAddressBookUri, 0, strlen(ContactsMigrator::MIGRATED_URI_PREFIX)) === ContactsMigrator::MIGRATED_URI_PREFIX
- ? $initialAddressBookUri
- : ContactsMigrator::MIGRATED_URI_PREFIX . $initialAddressBookUri;
- } catch (StringsException $e) {
- throw new ContactsMigratorException('Failed to get unique address book URI', 0, $e);
+ $initialAddressBookUri = substr($initialAddressBookUri, 0, strlen(ContactsMigrator::MIGRATED_URI_PREFIX)) === ContactsMigrator::MIGRATED_URI_PREFIX
+ ? $initialAddressBookUri
+ : ContactsMigrator::MIGRATED_URI_PREFIX . $initialAddressBookUri;
+
+ if ($initialAddressBookUri === '') {
+ throw new InvalidAddressBookException();
}
$existingAddressBookUris = array_map(
@@ -196,6 +179,27 @@ class ContactsMigrator implements IMigrator {
/**
* {@inheritDoc}
*/
+ public function getEstimatedExportSize(IUser $user): int|float {
+ $addressBookExports = $this->getAddressBookExports($user, new NullOutput());
+ $addressBookCount = count($addressBookExports);
+
+ // 50B for each metadata JSON
+ $size = ($addressBookCount * 50) / 1024;
+
+ $contactsCount = array_sum(array_map(
+ fn (array $data): int => count($data['vCards']),
+ $addressBookExports,
+ ));
+
+ // 350B for each contact
+ $size += ($contactsCount * 350) / 1024;
+
+ return ceil($size);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
public function export(IUser $user, IExportDestination $exportDestination, OutputInterface $output): void {
$output->writeln('Exporting contacts into ' . ContactsMigrator::PATH_ROOT . '…');
@@ -221,7 +225,7 @@ class ContactsMigrator implements IMigrator {
$exportDestination->addFileContents($exportPath, $this->serializeCards($vCards));
$metadata = array_filter(['displayName' => $displayName, 'description' => $description]);
- $exportDestination->addFileContents($metadataExportPath, json_encode($metadata));
+ $exportDestination->addFileContents($metadataExportPath, json_encode($metadata, JSON_THROW_ON_ERROR));
}
} catch (Throwable $e) {
throw new CalendarMigratorException('Could not export address book', 0, $e);
@@ -240,13 +244,15 @@ class ContactsMigrator implements IMigrator {
$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…");
}
}
/**
* @param array{displayName: string, description?: string} $metadata
* @param VCard[] $vCards
+ *
+ * @throws InvalidAddressBookException
*/
private function importAddressBook(IUser $user, string $filename, string $initialAddressBookUri, array $metadata, array $vCards, OutputInterface $output): void {
$principalUri = $this->getPrincipalUri($user);
@@ -276,11 +282,10 @@ class ContactsMigrator implements IMigrator {
fn (string $filename) => pathinfo($filename, PATHINFO_EXTENSION) === ContactsMigrator::METADATA_EXT,
);
- try {
- sort($addressBookImports);
- sort($metadataImports);
- } catch (ArrayException $e) {
- throw new ContactsMigratorException('Failed to sort address book files in ' . ContactsMigrator::PATH_ROOT, 0, $e);
+ $addressBookSort = sort($addressBookImports);
+ $metadataSort = sort($metadataImports);
+ if ($addressBookSort === false || $metadataSort === false) {
+ throw new ContactsMigratorException('Failed to sort address book files in ' . ContactsMigrator::PATH_ROOT);
}
if (count($addressBookImports) !== count($metadataImports)) {
@@ -342,24 +347,29 @@ class ContactsMigrator implements IMigrator {
$splitFilename = explode('.', $addressBookFilename, 2);
if (count($splitFilename) !== 2) {
- throw new ContactsMigratorException("Invalid filename \"$addressBookFilename\", expected filename of the format \"<address_book_name>." . ContactsMigrator::FILENAME_EXT . '"');
+ $output->writeln("Invalid filename \"$addressBookFilename\", expected filename of the format \"<address_book_name>." . ContactsMigrator::FILENAME_EXT . '", skipping…');
+ continue;
}
[$initialAddressBookUri, $ext] = $splitFilename;
/** @var array{displayName: string, description?: string} $metadata */
$metadata = json_decode($importSource->getFileContents($metadataImportPath), true, 512, JSON_THROW_ON_ERROR);
- $this->importAddressBook(
- $user,
- $addressBookFilename,
- $initialAddressBookUri,
- $metadata,
- $vCards,
- $output,
- );
-
- foreach ($vCards as $vCard) {
- $vCard->destroy();
+ try {
+ $this->importAddressBook(
+ $user,
+ $addressBookFilename,
+ $initialAddressBookUri,
+ $metadata,
+ $vCards,
+ $output,
+ );
+ } catch (InvalidAddressBookException $e) {
+ // Allow this exception to skip a failed import
+ } finally {
+ foreach ($vCards as $vCard) {
+ $vCard->destroy();
+ }
}
}
}
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;
diff --git a/apps/dav/openapi.json b/apps/dav/openapi.json
new file mode 100644
index 00000000000..27904636def
--- /dev/null
+++ b/apps/dav/openapi.json
@@ -0,0 +1,999 @@
+{
+ "openapi": "3.0.3",
+ "info": {
+ "title": "dav",
+ "version": "0.0.1",
+ "description": "WebDAV endpoint",
+ "license": {
+ "name": "agpl"
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "basic_auth": {
+ "type": "http",
+ "scheme": "basic"
+ },
+ "bearer_auth": {
+ "type": "http",
+ "scheme": "bearer"
+ }
+ },
+ "schemas": {
+ "Capabilities": {
+ "type": "object",
+ "required": [
+ "dav"
+ ],
+ "properties": {
+ "dav": {
+ "type": "object",
+ "required": [
+ "chunking",
+ "public_shares_chunking"
+ ],
+ "properties": {
+ "chunking": {
+ "type": "string"
+ },
+ "public_shares_chunking": {
+ "type": "boolean"
+ },
+ "bulkupload": {
+ "type": "string"
+ },
+ "absence-supported": {
+ "type": "boolean"
+ },
+ "absence-replacement": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "CurrentOutOfOfficeData": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/OutOfOfficeDataCommon"
+ },
+ {
+ "type": "object",
+ "required": [
+ "id",
+ "startDate",
+ "endDate",
+ "shortMessage"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "startDate": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "endDate": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "shortMessage": {
+ "type": "string"
+ }
+ }
+ }
+ ]
+ },
+ "OCSMeta": {
+ "type": "object",
+ "required": [
+ "status",
+ "statuscode"
+ ],
+ "properties": {
+ "status": {
+ "type": "string"
+ },
+ "statuscode": {
+ "type": "integer"
+ },
+ "message": {
+ "type": "string"
+ },
+ "totalitems": {
+ "type": "string"
+ },
+ "itemsperpage": {
+ "type": "string"
+ }
+ }
+ },
+ "OutOfOfficeData": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/OutOfOfficeDataCommon"
+ },
+ {
+ "type": "object",
+ "required": [
+ "id",
+ "firstDay",
+ "lastDay",
+ "status"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "firstDay": {
+ "type": "string"
+ },
+ "lastDay": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string"
+ }
+ }
+ }
+ ]
+ },
+ "OutOfOfficeDataCommon": {
+ "type": "object",
+ "required": [
+ "userId",
+ "message",
+ "replacementUserId",
+ "replacementUserDisplayName"
+ ],
+ "properties": {
+ "userId": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "replacementUserId": {
+ "type": "string",
+ "nullable": true
+ },
+ "replacementUserDisplayName": {
+ "type": "string",
+ "nullable": true
+ }
+ }
+ },
+ "UpcomingEvent": {
+ "type": "object",
+ "required": [
+ "uri",
+ "calendarUri",
+ "start",
+ "summary",
+ "location"
+ ],
+ "properties": {
+ "uri": {
+ "type": "string"
+ },
+ "calendarUri": {
+ "type": "string"
+ },
+ "start": {
+ "type": "integer",
+ "format": "int64",
+ "nullable": true
+ },
+ "summary": {
+ "type": "string",
+ "nullable": true
+ },
+ "location": {
+ "type": "string",
+ "nullable": true
+ }
+ }
+ }
+ }
+ },
+ "paths": {
+ "/ocs/v2.php/apps/dav/api/v1/direct": {
+ "post": {
+ "operationId": "direct-get-url",
+ "summary": "Get a direct link to a file",
+ "tags": [
+ "direct"
+ ],
+ "security": [
+ {
+ "bearer_auth": []
+ },
+ {
+ "basic_auth": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "fileId",
+ "expirationTime"
+ ],
+ "properties": {
+ "fileId": {
+ "type": "integer",
+ "format": "int64",
+ "description": "ID of the file"
+ },
+ "expirationTime": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Duration until the link expires"
+ }
+ }
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "OCS-APIRequest",
+ "in": "header",
+ "description": "Required to be true for the API request to pass",
+ "required": true,
+ "schema": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Direct link returned",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "ocs"
+ ],
+ "properties": {
+ "ocs": {
+ "type": "object",
+ "required": [
+ "meta",
+ "data"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/components/schemas/OCSMeta"
+ },
+ "data": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "url": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "File not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "ocs"
+ ],
+ "properties": {
+ "ocs": {
+ "type": "object",
+ "required": [
+ "meta",
+ "data"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/components/schemas/OCSMeta"
+ },
+ "data": {}
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Getting direct link is not possible",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "ocs"
+ ],
+ "properties": {
+ "ocs": {
+ "type": "object",
+ "required": [
+ "meta",
+ "data"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/components/schemas/OCSMeta"
+ },
+ "data": {}
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Missing permissions to get direct link",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "ocs"
+ ],
+ "properties": {
+ "ocs": {
+ "type": "object",
+ "required": [
+ "meta",
+ "data"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/components/schemas/OCSMeta"
+ },
+ "data": {}
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/ocs/v2.php/apps/dav/api/v1/events/upcoming": {
+ "get": {
+ "operationId": "upcoming_events-get-events",
+ "summary": "Get information about upcoming events",
+ "tags": [
+ "upcoming_events"
+ ],
+ "security": [
+ {
+ "bearer_auth": []
+ },
+ {
+ "basic_auth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "location",
+ "in": "query",
+ "description": "location/URL to filter by",
+ "schema": {
+ "type": "string",
+ "nullable": true,
+ "default": null
+ }
+ },
+ {
+ "name": "OCS-APIRequest",
+ "in": "header",
+ "description": "Required to be true for the API request to pass",
+ "required": true,
+ "schema": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Upcoming events",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "ocs"
+ ],
+ "properties": {
+ "ocs": {
+ "type": "object",
+ "required": [
+ "meta",
+ "data"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/components/schemas/OCSMeta"
+ },
+ "data": {
+ "type": "object",
+ "required": [
+ "events"
+ ],
+ "properties": {
+ "events": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/UpcomingEvent"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "When not authenticated",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "ocs"
+ ],
+ "properties": {
+ "ocs": {
+ "type": "object",
+ "required": [
+ "meta",
+ "data"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/components/schemas/OCSMeta"
+ },
+ "data": {
+ "nullable": true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/ocs/v2.php/apps/dav/api/v1/outOfOffice/{userId}/now": {
+ "get": {
+ "operationId": "out_of_office-get-current-out-of-office-data",
+ "summary": "Get the currently configured out-of-office data of a user",
+ "tags": [
+ "out_of_office"
+ ],
+ "security": [
+ {
+ "bearer_auth": []
+ },
+ {
+ "basic_auth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "userId",
+ "in": "path",
+ "description": "The user id to get out-of-office data for.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "OCS-APIRequest",
+ "in": "header",
+ "description": "Required to be true for the API request to pass",
+ "required": true,
+ "schema": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Out-of-office data",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "ocs"
+ ],
+ "properties": {
+ "ocs": {
+ "type": "object",
+ "required": [
+ "meta",
+ "data"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/components/schemas/OCSMeta"
+ },
+ "data": {
+ "$ref": "#/components/schemas/CurrentOutOfOfficeData"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "No out-of-office data was found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "ocs"
+ ],
+ "properties": {
+ "ocs": {
+ "type": "object",
+ "required": [
+ "meta",
+ "data"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/components/schemas/OCSMeta"
+ },
+ "data": {
+ "nullable": true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/ocs/v2.php/apps/dav/api/v1/outOfOffice/{userId}": {
+ "get": {
+ "operationId": "out_of_office-get-out-of-office",
+ "summary": "Get the configured out-of-office data of a user.",
+ "tags": [
+ "out_of_office"
+ ],
+ "security": [
+ {
+ "bearer_auth": []
+ },
+ {
+ "basic_auth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "userId",
+ "in": "path",
+ "description": "The user id to get out-of-office data for.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "OCS-APIRequest",
+ "in": "header",
+ "description": "Required to be true for the API request to pass",
+ "required": true,
+ "schema": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Out-of-office data",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "ocs"
+ ],
+ "properties": {
+ "ocs": {
+ "type": "object",
+ "required": [
+ "meta",
+ "data"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/components/schemas/OCSMeta"
+ },
+ "data": {
+ "$ref": "#/components/schemas/OutOfOfficeData"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "No out-of-office data was found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "ocs"
+ ],
+ "properties": {
+ "ocs": {
+ "type": "object",
+ "required": [
+ "meta",
+ "data"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/components/schemas/OCSMeta"
+ },
+ "data": {
+ "nullable": true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "operationId": "out_of_office-set-out-of-office",
+ "summary": "Set out-of-office absence",
+ "tags": [
+ "out_of_office"
+ ],
+ "security": [
+ {
+ "bearer_auth": []
+ },
+ {
+ "basic_auth": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "firstDay",
+ "lastDay",
+ "status",
+ "message"
+ ],
+ "properties": {
+ "firstDay": {
+ "type": "string",
+ "description": "First day of the absence in format `YYYY-MM-DD`"
+ },
+ "lastDay": {
+ "type": "string",
+ "description": "Last day of the absence in format `YYYY-MM-DD`"
+ },
+ "status": {
+ "type": "string",
+ "description": "Short text that is set as user status during the absence"
+ },
+ "message": {
+ "type": "string",
+ "description": "Longer multiline message that is shown to others during the absence"
+ },
+ "replacementUserId": {
+ "type": "string",
+ "nullable": true,
+ "description": "User id of the replacement user"
+ }
+ }
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "userId",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "OCS-APIRequest",
+ "in": "header",
+ "description": "Required to be true for the API request to pass",
+ "required": true,
+ "schema": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Absence data",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "ocs"
+ ],
+ "properties": {
+ "ocs": {
+ "type": "object",
+ "required": [
+ "meta",
+ "data"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/components/schemas/OCSMeta"
+ },
+ "data": {
+ "$ref": "#/components/schemas/OutOfOfficeData"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "When validation fails, e.g. data range error or the first day is not before the last day",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "ocs"
+ ],
+ "properties": {
+ "ocs": {
+ "type": "object",
+ "required": [
+ "meta",
+ "data"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/components/schemas/OCSMeta"
+ },
+ "data": {
+ "type": "object",
+ "required": [
+ "error"
+ ],
+ "properties": {
+ "error": {
+ "type": "string",
+ "enum": [
+ "firstDay",
+ "statusLength"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "When the user is not logged in",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "ocs"
+ ],
+ "properties": {
+ "ocs": {
+ "type": "object",
+ "required": [
+ "meta",
+ "data"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/components/schemas/OCSMeta"
+ },
+ "data": {
+ "nullable": true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "When the replacementUserId was provided but replacement user was not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "ocs"
+ ],
+ "properties": {
+ "ocs": {
+ "type": "object",
+ "required": [
+ "meta",
+ "data"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/components/schemas/OCSMeta"
+ },
+ "data": {
+ "nullable": true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "delete": {
+ "operationId": "out_of_office-clear-out-of-office",
+ "summary": "Clear the out-of-office",
+ "tags": [
+ "out_of_office"
+ ],
+ "security": [
+ {
+ "bearer_auth": []
+ },
+ {
+ "basic_auth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "userId",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "OCS-APIRequest",
+ "in": "header",
+ "description": "Required to be true for the API request to pass",
+ "required": true,
+ "schema": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "When the absence was cleared successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "ocs"
+ ],
+ "properties": {
+ "ocs": {
+ "type": "object",
+ "required": [
+ "meta",
+ "data"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/components/schemas/OCSMeta"
+ },
+ "data": {
+ "nullable": true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "When the user is not logged in",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "ocs"
+ ],
+ "properties": {
+ "ocs": {
+ "type": "object",
+ "required": [
+ "meta",
+ "data"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/components/schemas/OCSMeta"
+ },
+ "data": {
+ "nullable": true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "tags": []
+}
diff --git a/apps/dav/openapi.json.license b/apps/dav/openapi.json.license
new file mode 100644
index 00000000000..83559daa9dc
--- /dev/null
+++ b/apps/dav/openapi.json.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+SPDX-License-Identifier: AGPL-3.0-or-later \ No newline at end of file
diff --git a/apps/dav/src/components/AbsenceForm.vue b/apps/dav/src/components/AbsenceForm.vue
new file mode 100644
index 00000000000..5350c04a565
--- /dev/null
+++ b/apps/dav/src/components/AbsenceForm.vue
@@ -0,0 +1,274 @@
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <form class="absence" @submit.prevent="saveForm">
+ <div class="absence__dates">
+ <NcDateTimePickerNative id="absence-first-day"
+ v-model="firstDay"
+ :label="$t('dav', 'First day')"
+ class="absence__dates__picker"
+ :required="true" />
+ <NcDateTimePickerNative id="absence-last-day"
+ v-model="lastDay"
+ :label="$t('dav', 'Last day (inclusive)')"
+ class="absence__dates__picker"
+ :required="true" />
+ </div>
+ <label for="replacement-search-input">{{ $t('dav', 'Out of office replacement (optional)') }}</label>
+ <NcSelect ref="select"
+ v-model="replacementUser"
+ input-id="replacement-search-input"
+ :loading="searchLoading"
+ :placeholder="$t('dav', 'Name of the replacement')"
+ :clear-search-on-blur="() => false"
+ :user-select="true"
+ :options="options"
+ @search="asyncFind">
+ <template #no-options="{ search }">
+ {{ search ?$t('dav', 'No results.') : $t('dav', 'Start typing.') }}
+ </template>
+ </NcSelect>
+ <NcTextField :value.sync="status" :label="$t('dav', 'Short absence status')" :required="true" />
+ <NcTextArea :value.sync="message" :label="$t('dav', 'Long absence Message')" :required="true" />
+
+ <div class="absence__buttons">
+ <NcButton :disabled="loading || !valid"
+ type="primary"
+ native-type="submit">
+ {{ $t('dav', 'Save') }}
+ </NcButton>
+ <NcButton :disabled="loading || !valid"
+ type="error"
+ @click="clearAbsence">
+ {{ $t('dav', 'Disable absence') }}
+ </NcButton>
+ </div>
+ </form>
+</template>
+
+<script>
+import { getCurrentUser } from '@nextcloud/auth'
+import { showError, showSuccess } from '@nextcloud/dialogs'
+import { loadState } from '@nextcloud/initial-state'
+import { generateOcsUrl } from '@nextcloud/router'
+import { ShareType } from '@nextcloud/sharing'
+import { formatDateAsYMD } from '../utils/date.js'
+import axios from '@nextcloud/axios'
+import debounce from 'debounce'
+import logger from '../service/logger.js'
+
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
+import NcTextArea from '@nextcloud/vue/components/NcTextArea'
+import NcSelect from '@nextcloud/vue/components/NcSelect'
+import NcDateTimePickerNative from '@nextcloud/vue/components/NcDateTimePickerNative'
+
+export default {
+ name: 'AbsenceForm',
+ components: {
+ NcButton,
+ NcTextField,
+ NcTextArea,
+ NcDateTimePickerNative,
+ NcSelect,
+ },
+ data() {
+ const { firstDay, lastDay, status, message, replacementUserId, replacementUserDisplayName } = loadState('dav', 'absence', {})
+ return {
+ loading: false,
+ status: status ?? '',
+ message: message ?? '',
+ firstDay: firstDay ? new Date(firstDay) : new Date(),
+ lastDay: lastDay ? new Date(lastDay) : null,
+ replacementUserId,
+ replacementUser: replacementUserId ? { user: replacementUserId, displayName: replacementUserDisplayName } : null,
+ searchLoading: false,
+ options: [],
+ }
+ },
+ computed: {
+ /**
+ * @return {boolean}
+ */
+ valid() {
+ // Translate the two date objects to midnight for an accurate comparison
+ const firstDay = new Date(this.firstDay?.getTime())
+ const lastDay = new Date(this.lastDay?.getTime())
+ firstDay?.setHours(0, 0, 0, 0)
+ lastDay?.setHours(0, 0, 0, 0)
+
+ return !!this.firstDay
+ && !!this.lastDay
+ && !!this.status
+ && !!this.message
+ && lastDay >= firstDay
+ },
+ },
+ methods: {
+ resetForm() {
+ this.status = ''
+ this.message = ''
+ this.firstDay = new Date()
+ this.lastDay = null
+ },
+
+ /**
+ * Format shares for the multiselect options
+ *
+ * @param {object} result select entry item
+ * @return {object}
+ */
+ formatForMultiselect(result) {
+ return {
+ user: result.uuid || result.value.shareWith,
+ displayName: result.name || result.label,
+ subtitle: result.dsc | '',
+ }
+ },
+
+ async asyncFind(query) {
+ this.searchLoading = true
+ await this.debounceGetSuggestions(query.trim())
+ },
+ /**
+ * Get suggestions
+ *
+ * @param {string} search the search query
+ */
+ async getSuggestions(search) {
+
+ const shareType = [
+ ShareType.User,
+ ]
+
+ let request = null
+ try {
+ request = await axios.get(generateOcsUrl('apps/files_sharing/api/v1/sharees'), {
+ params: {
+ format: 'json',
+ itemType: 'file',
+ search,
+ shareType,
+ },
+ })
+ } catch (error) {
+ console.error('Error fetching suggestions', error)
+ return
+ }
+
+ const data = request.data.ocs.data
+ const exact = request.data.ocs.data.exact
+ data.exact = [] // removing exact from general results
+ const rawExactSuggestions = exact.users
+ const rawSuggestions = data.users
+ console.info('rawExactSuggestions', rawExactSuggestions)
+ console.info('rawSuggestions', rawSuggestions)
+ // remove invalid data and format to user-select layout
+ const exactSuggestions = rawExactSuggestions
+ .map(share => this.formatForMultiselect(share))
+ const suggestions = rawSuggestions
+ .map(share => this.formatForMultiselect(share))
+
+ const allSuggestions = exactSuggestions.concat(suggestions)
+
+ // Count occurrences of display names in order to provide a distinguishable description if needed
+ const nameCounts = allSuggestions.reduce((nameCounts, result) => {
+ if (!result.displayName) {
+ return nameCounts
+ }
+ if (!nameCounts[result.displayName]) {
+ nameCounts[result.displayName] = 0
+ }
+ nameCounts[result.displayName]++
+ return nameCounts
+ }, {})
+
+ this.options = allSuggestions.map(item => {
+ // Make sure that items with duplicate displayName get the shareWith applied as a description
+ if (nameCounts[item.displayName] > 1 && !item.desc) {
+ return { ...item, desc: item.shareWithDisplayNameUnique }
+ }
+ return item
+ })
+
+ this.searchLoading = false
+ console.info('suggestions', this.options)
+ },
+
+ /**
+ * Debounce getSuggestions
+ *
+ * @param {...*} args the arguments
+ */
+ debounceGetSuggestions: debounce(function(...args) {
+ this.getSuggestions(...args)
+ }, 300),
+
+ async saveForm() {
+ if (!this.valid) {
+ return
+ }
+
+ this.loading = true
+ try {
+ await axios.post(generateOcsUrl('/apps/dav/api/v1/outOfOffice/{userId}', { userId: getCurrentUser().uid }), {
+ firstDay: formatDateAsYMD(this.firstDay),
+ lastDay: formatDateAsYMD(this.lastDay),
+ status: this.status,
+ message: this.message,
+ replacementUserId: this.replacementUser?.user ?? null,
+ })
+ showSuccess(this.$t('dav', 'Absence saved'))
+ } catch (error) {
+ showError(this.$t('dav', 'Failed to save your absence settings'))
+ logger.error('Could not save absence', { error })
+ } finally {
+ this.loading = false
+ }
+ },
+ async clearAbsence() {
+ this.loading = true
+ try {
+ await axios.delete(generateOcsUrl('/apps/dav/api/v1/outOfOffice/{userId}', { userId: getCurrentUser().uid }))
+ this.resetForm()
+ showSuccess(this.$t('dav', 'Absence cleared'))
+ } catch (error) {
+ showError(this.$t('dav', 'Failed to clear your absence settings'))
+ logger.error('Could not clear absence', { error })
+ } finally {
+ this.loading = false
+ }
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+.absence {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+
+ &__dates {
+ display: flex;
+ gap: 10px;
+ width: 100%;
+
+ &__picker {
+ flex: 1 auto;
+
+ :deep(.native-datetime-picker--input) {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ &__buttons {
+ display: flex;
+ gap: 5px;
+ }
+}
+</style>
diff --git a/apps/dav/src/components/AvailabilityForm.vue b/apps/dav/src/components/AvailabilityForm.vue
new file mode 100644
index 00000000000..d53c092be9d
--- /dev/null
+++ b/apps/dav/src/components/AvailabilityForm.vue
@@ -0,0 +1,223 @@
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+ <div>
+ <div class="time-zone">
+ <label :for="`vs${timeZonePickerId}__combobox`" class="time-zone__heading">
+ {{ $t('dav', 'Time zone:') }}
+ </label>
+ <span class="time-zone-text">
+ <NcTimezonePicker v-model="timezone" :uid="timeZonePickerId" />
+ </span>
+ </div>
+
+ <CalendarAvailability :slots.sync="slots"
+ :loading="loading"
+ :l10n-to="$t('dav', 'to')"
+ :l10n-delete-slot="$t('dav', 'Delete slot')"
+ :l10n-empty-day="$t('dav', 'No working hours set')"
+ :l10n-add-slot="$t('dav', 'Add slot')"
+ :l10n-week-day-list-label="$t('dav', 'Weekdays')"
+ :l10n-monday="$t('dav', 'Monday')"
+ :l10n-tuesday="$t('dav', 'Tuesday')"
+ :l10n-wednesday="$t('dav', 'Wednesday')"
+ :l10n-thursday="$t('dav', 'Thursday')"
+ :l10n-friday="$t('dav', 'Friday')"
+ :l10n-saturday="$t('dav', 'Saturday')"
+ :l10n-sunday="$t('dav', 'Sunday')"
+ :l10n-start-picker-label="(dayName) => $t('dav', 'Pick a start time for {dayName}', { dayName })"
+ :l10n-end-picker-label="(dayName) => $t('dav', 'Pick a end time for {dayName}', { dayName })" />
+
+ <NcCheckboxRadioSwitch :checked.sync="automated">
+ {{ $t('dav', 'Automatically set user status to "Do not disturb" outside of availability to mute all notifications.') }}
+ </NcCheckboxRadioSwitch>
+
+ <NcButton :disabled="loading || saving"
+ type="primary"
+ @click="save">
+ {{ $t('dav', 'Save') }}
+ </NcButton>
+ </div>
+</template>
+
+<script>
+import { CalendarAvailability } from '@nextcloud/calendar-availability-vue'
+import { loadState } from '@nextcloud/initial-state'
+import {
+ showError,
+ showSuccess,
+} from '@nextcloud/dialogs'
+import {
+ findScheduleInboxAvailability,
+ getEmptySlots,
+ saveScheduleInboxAvailability,
+} from '../service/CalendarService.js'
+import {
+ enableUserStatusAutomation,
+ disableUserStatusAutomation,
+} from '../service/PreferenceService.js'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
+import NcTimezonePicker from '@nextcloud/vue/components/NcTimezonePicker'
+
+export default {
+ name: 'AvailabilityForm',
+ components: {
+ NcButton,
+ NcCheckboxRadioSwitch,
+ CalendarAvailability,
+ NcTimezonePicker,
+ },
+ data() {
+ // Try to determine the current timezone, and fall back to UTC otherwise
+ const defaultTimezoneId = (new Intl.DateTimeFormat())?.resolvedOptions()?.timeZone ?? 'UTC'
+
+ return {
+ loading: true,
+ saving: false,
+ timezone: defaultTimezoneId,
+ slots: getEmptySlots(),
+ automated: loadState('dav', 'user_status_automation') === 'yes',
+ }
+ },
+ computed: {
+ timeZonePickerId() {
+ return `tz-${(Math.random() + 1).toString(36).substring(7)}`
+ },
+ },
+ async mounted() {
+ try {
+ const slotData = await findScheduleInboxAvailability()
+ if (!slotData) {
+ console.info('no availability is set')
+ this.slots = getEmptySlots()
+ } else {
+ const { slots, timezoneId } = slotData
+ this.slots = slots
+ if (timezoneId) {
+ this.timezone = timezoneId
+ }
+ console.info('availability loaded', this.slots, this.timezoneId)
+ }
+ } catch (e) {
+ console.error('could not load existing availability', e)
+
+ showError(t('dav', 'Failed to load availability'))
+ } finally {
+ this.loading = false
+ }
+ },
+ methods: {
+ async save() {
+ try {
+ this.saving = true
+
+ await saveScheduleInboxAvailability(this.slots, this.timezone)
+ if (this.automated) {
+ await enableUserStatusAutomation()
+ } else {
+ await disableUserStatusAutomation()
+ }
+
+ showSuccess(t('dav', 'Saved availability'))
+ } catch (e) {
+ console.error('could not save availability', e)
+
+ showError(t('dav', 'Failed to save availability'))
+ } finally {
+ this.saving = false
+ }
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+:deep(.availability-day) {
+ padding: 0 10px 0 10px;
+ position: absolute;
+}
+
+:deep(.availability-slots) {
+ display: flex;
+ white-space: normal;
+}
+
+:deep(.availability-slot) {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+:deep(.availability-slot-group) {
+ display: flex;
+ flex-direction: column;
+}
+
+:deep(.mx-input-wrapper) {
+ width: 85px;
+}
+
+:deep(.mx-datepicker) {
+ width: 97px;
+}
+
+:deep(.multiselect) {
+ border: 1px solid var(--color-border-dark);
+ width: 120px;
+}
+
+.time-zone {
+ padding-block: 32px 12px;
+ padding-inline: 0 12px;
+ display: flex;
+ flex-wrap: wrap;
+
+ &__heading {
+ margin-inline-end: calc(var(--default-grid-baseline) * 2);
+ line-height: var(--default-clickable-area);
+ font-weight: bold;
+ }
+}
+
+.grid-table {
+ display: grid;
+ margin-bottom: 32px;
+ grid-column-gap: 24px;
+ grid-row-gap: 6px;
+ grid-template-columns: min-content auto min-content;
+ max-width: 500px;
+}
+
+.button {
+ align-self: flex-end;
+}
+
+:deep(.label-weekday) {
+ position: relative;
+ display: inline-flex;
+ padding-top: 4px;
+ align-self: center;
+}
+
+:deep(.delete-slot) {
+ padding-bottom: unset;
+}
+
+:deep(.add-another) {
+ align-self: center;
+}
+
+.to-text {
+ padding-inline-end: 12px;
+}
+
+.empty-content {
+ color: var(--color-text-lighter);
+ margin-block-start: var(--default-grid-baseline);
+ align-self: center;
+}
+</style>
diff --git a/apps/dav/src/components/ExampleContactSettings.vue b/apps/dav/src/components/ExampleContactSettings.vue
new file mode 100644
index 00000000000..cdfdc130189
--- /dev/null
+++ b/apps/dav/src/components/ExampleContactSettings.vue
@@ -0,0 +1,172 @@
+<!--
+ - SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <div class="example-contact-settings">
+ <NcCheckboxRadioSwitch :checked="enableDefaultContact"
+ type="switch"
+ @update:model-value="updateEnableDefaultContact">
+ {{ $t('dav', "Add example contact to user's address book when they first log in") }}
+ </NcCheckboxRadioSwitch>
+ <div v-if="enableDefaultContact" class="example-contact-settings__buttons">
+ <ExampleContentDownloadButton :href="downloadUrl">
+ <template #icon>
+ <IconAccount :size="20" />
+ </template>
+ example_contact.vcf
+ </ExampleContentDownloadButton>
+ <NcButton type="secondary"
+ @click="toggleModal">
+ <template #icon>
+ <IconUpload :size="20" />
+ </template>
+ {{ $t('dav', 'Import contact') }}
+ </NcButton>
+ <NcButton v-if="hasCustomDefaultContact"
+ type="tertiary"
+ @click="resetContact">
+ <template #icon>
+ <IconRestore :size="20" />
+ </template>
+ {{ $t('dav', 'Reset to default') }}
+ </NcButton>
+ </div>
+ <NcDialog :open.sync="isModalOpen"
+ :name="$t('dav', 'Import contacts')"
+ :buttons="buttons">
+ <div>
+ <p>{{ $t('dav', 'Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?') }}</p>
+ </div>
+ </NcDialog>
+ <input id="example-contact-import"
+ ref="exampleContactImportInput"
+ :disabled="loading"
+ type="file"
+ accept=".vcf"
+ class="hidden-visually"
+ @change="processFile">
+ </div>
+</template>
+<script>
+import axios from '@nextcloud/axios'
+import { generateUrl } from '@nextcloud/router'
+import { loadState } from '@nextcloud/initial-state'
+import { NcDialog, NcButton, NcCheckboxRadioSwitch } from '@nextcloud/vue'
+import { showError, showSuccess } from '@nextcloud/dialogs'
+import IconUpload from 'vue-material-design-icons/Upload.vue'
+import IconRestore from 'vue-material-design-icons/Restore.vue'
+import IconAccount from 'vue-material-design-icons/Account.vue'
+import IconCancel from '@mdi/svg/svg/cancel.svg?raw'
+import IconCheck from '@mdi/svg/svg/check.svg?raw'
+import logger from '../service/logger.js'
+import ExampleContentDownloadButton from './ExampleContentDownloadButton.vue'
+
+const enableDefaultContact = loadState('dav', 'enableDefaultContact')
+const hasCustomDefaultContact = loadState('dav', 'hasCustomDefaultContact')
+
+export default {
+ name: 'ExampleContactSettings',
+ components: {
+ NcDialog,
+ NcButton,
+ NcCheckboxRadioSwitch,
+ IconUpload,
+ IconRestore,
+ IconAccount,
+ ExampleContentDownloadButton,
+ },
+ data() {
+ return {
+ enableDefaultContact,
+ hasCustomDefaultContact,
+ isModalOpen: false,
+ loading: false,
+ buttons: [
+ {
+ label: this.$t('dav', 'Cancel'),
+ icon: IconCancel,
+ callback: () => { this.isModalOpen = false },
+ },
+ {
+ label: this.$t('dav', 'Import'),
+ type: 'primary',
+ icon: IconCheck,
+ callback: () => { this.clickImportInput() },
+ },
+ ],
+ }
+ },
+ computed: {
+ downloadUrl() {
+ return generateUrl('/apps/dav/api/defaultcontact/contact')
+ },
+ },
+ methods: {
+ updateEnableDefaultContact() {
+ axios.put(generateUrl('apps/dav/api/defaultcontact/config'), {
+ allow: !this.enableDefaultContact,
+ }).then(() => {
+ this.enableDefaultContact = !this.enableDefaultContact
+ }).catch(() => {
+ showError(this.$t('dav', 'Error while saving settings'))
+ })
+ },
+ toggleModal() {
+ this.isModalOpen = !this.isModalOpen
+ },
+ clickImportInput() {
+ this.$refs.exampleContactImportInput.click()
+ },
+ resetContact() {
+ this.loading = true
+ axios.put(generateUrl('/apps/dav/api/defaultcontact/contact'))
+ .then(() => {
+ this.hasCustomDefaultContact = false
+ showSuccess(this.$t('dav', 'Contact reset successfully'))
+ })
+ .catch((error) => {
+ logger.error('Error importing contact:', { error })
+ showError(this.$t('dav', 'Error while resetting contact'))
+ })
+ .finally(() => {
+ this.loading = false
+ })
+ },
+ processFile(event) {
+ this.loading = true
+
+ const file = event.target.files[0]
+ const reader = new FileReader()
+
+ reader.onload = async () => {
+ this.isModalOpen = false
+ try {
+ await axios.put(generateUrl('/apps/dav/api/defaultcontact/contact'), { contactData: reader.result })
+ this.hasCustomDefaultContact = true
+ showSuccess(this.$t('dav', 'Contact imported successfully'))
+ } catch (error) {
+ logger.error('Error importing contact:', { error })
+ showError(this.$t('dav', 'Error while importing contact'))
+ } finally {
+ this.loading = false
+ event.target.value = ''
+ }
+ }
+ reader.readAsText(file)
+ },
+ },
+}
+</script>
+<style lang="scss" scoped>
+.example-contact-settings {
+ margin-block-start: 2rem;
+
+ &__buttons {
+ display: flex;
+ gap: calc(var(--default-grid-baseline) * 2);
+ margin-top: calc(var(--default-grid-baseline) * 2);
+ }
+}
+</style>
diff --git a/apps/dav/src/components/ExampleContentDownloadButton.vue b/apps/dav/src/components/ExampleContentDownloadButton.vue
new file mode 100644
index 00000000000..6ee13e057bd
--- /dev/null
+++ b/apps/dav/src/components/ExampleContentDownloadButton.vue
@@ -0,0 +1,57 @@
+<!--
+ - SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+ -->
+
+<template>
+ <NcButton type="tertiary" :href="href">
+ <template #icon>
+ <slot name="icon" />
+ </template>
+ <div class="download-button">
+ <span class="download-button__label">
+ <slot name="default" />
+ </span>
+ <IconDownload class="download-button__icon"
+ :size="20" />
+ </div>
+ </NcButton>
+</template>
+
+<script>
+import { NcButton } from '@nextcloud/vue'
+import IconDownload from 'vue-material-design-icons/Download.vue'
+
+export default {
+ name: 'ExampleContentDownloadButton',
+ components: {
+ NcButton,
+ IconDownload,
+ },
+ props: {
+ href: {
+ type: String,
+ required: true,
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+.download-button {
+ display: flex;
+ max-width: 200px;
+
+ &__label {
+ font-weight: initial;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ }
+
+ &__icon {
+ margin-top: 2px;
+ margin-inline-start: var(--default-grid-baseline);
+ }
+}
+</style>
diff --git a/apps/dav/src/components/ExampleEventSettings.vue b/apps/dav/src/components/ExampleEventSettings.vue
new file mode 100644
index 00000000000..5d2053def50
--- /dev/null
+++ b/apps/dav/src/components/ExampleEventSettings.vue
@@ -0,0 +1,217 @@
+<!--
+ - SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+ -->
+
+<template>
+ <div class="example-event-settings">
+ <NcCheckboxRadioSwitch :checked="createExampleEvent"
+ :disabled="savingConfig"
+ type="switch"
+ @update:model-value="updateCreateExampleEvent">
+ {{ t('dav', "Add example event to user's calendar when they first log in") }}
+ </NcCheckboxRadioSwitch>
+ <div v-if="createExampleEvent"
+ class="example-event-settings__buttons">
+ <ExampleContentDownloadButton :href="downloadUrl">
+ <template #icon>
+ <IconCalendarBlank :size="20" />
+ </template>
+ example_event.ics
+ </ExampleContentDownloadButton>
+ <NcButton type="secondary"
+ @click="showImportModal = true">
+ <template #icon>
+ <IconUpload :size="20" />
+ </template>
+ {{ t('dav', 'Import calendar event') }}
+ </NcButton>
+ <NcButton v-if="hasCustomEvent"
+ type="tertiary"
+ :disabled="deleting"
+ @click="deleteCustomEvent">
+ <template #icon>
+ <IconRestore :size="20" />
+ </template>
+ {{ t('dav', 'Reset to default') }}
+ </NcButton>
+ </div>
+ <NcDialog :open.sync="showImportModal"
+ :name="t('dav', 'Import calendar event')">
+ <div class="import-event-modal">
+ <p>
+ {{ t('dav', 'Uploading a new event will overwrite the existing one.') }}
+ </p>
+ <input ref="event-file"
+ :disabled="uploading"
+ type="file"
+ accept=".ics,text/calendar"
+ class="import-event-modal__file-picker"
+ @change="selectFile">
+ <div class="import-event-modal__buttons">
+ <NcButton :disabled="uploading || !selectedFile"
+ type="primary"
+ @click="uploadCustomEvent()">
+ <template #icon>
+ <IconUpload :size="20" />
+ </template>
+ {{ t('dav', 'Upload event') }}
+ </NcButton>
+ </div>
+ </div>
+ </NcDialog>
+ </div>
+</template>
+
+<script>
+import { NcButton, NcCheckboxRadioSwitch, NcDialog } from '@nextcloud/vue'
+import { loadState } from '@nextcloud/initial-state'
+import IconCalendarBlank from 'vue-material-design-icons/CalendarBlank.vue'
+import IconUpload from 'vue-material-design-icons/Upload.vue'
+import IconRestore from 'vue-material-design-icons/Restore.vue'
+import * as ExampleEventService from '../service/ExampleEventService.js'
+import { showError, showSuccess } from '@nextcloud/dialogs'
+import logger from '../service/logger.js'
+import { generateUrl } from '@nextcloud/router'
+import ExampleContentDownloadButton from './ExampleContentDownloadButton.vue'
+
+export default {
+ name: 'ExampleEventSettings',
+ components: {
+ NcButton,
+ NcCheckboxRadioSwitch,
+ NcDialog,
+ IconCalendarBlank,
+ IconUpload,
+ IconRestore,
+ ExampleContentDownloadButton,
+ },
+ data() {
+ return {
+ createExampleEvent: loadState('dav', 'create_example_event', false),
+ hasCustomEvent: loadState('dav', 'has_custom_example_event', false),
+ showImportModal: false,
+ uploading: false,
+ deleting: false,
+ savingConfig: false,
+ selectedFile: undefined,
+ }
+ },
+ computed: {
+ downloadUrl() {
+ return generateUrl('/apps/dav/api/exampleEvent/event')
+ },
+ },
+ methods: {
+ selectFile() {
+ this.selectedFile = this.$refs['event-file']?.files[0]
+ },
+ async updateCreateExampleEvent() {
+ this.savingConfig = true
+
+ const enable = !this.createExampleEvent
+ try {
+ await ExampleEventService.setCreateExampleEvent(enable)
+ } catch (error) {
+ showError(t('dav', 'Failed to save example event creation setting'))
+ logger.error('Failed to save example event creation setting', {
+ error,
+ enable,
+ })
+ } finally {
+ this.savingConfig = false
+ }
+
+ this.createExampleEvent = enable
+ },
+ uploadCustomEvent() {
+ if (!this.selectedFile) {
+ return
+ }
+
+ this.uploading = true
+
+ const reader = new FileReader()
+ reader.addEventListener('load', async () => {
+ const ics = reader.result
+
+ try {
+ await ExampleEventService.uploadExampleEvent(ics)
+ } catch (error) {
+ showError(t('dav', 'Failed to upload the example event'))
+ logger.error('Failed to upload example ICS', {
+ error,
+ ics,
+ })
+ return
+ } finally {
+ this.uploading = false
+ }
+
+ showSuccess(t('dav', 'Custom example event was saved successfully'))
+ this.showImportModal = false
+ this.hasCustomEvent = true
+ })
+ reader.readAsText(this.selectedFile)
+ },
+ async deleteCustomEvent() {
+ this.deleting = true
+
+ try {
+ await ExampleEventService.deleteExampleEvent()
+ } catch (error) {
+ showError(t('dav', 'Failed to delete the custom example event'))
+ logger.error('Failed to delete the custom example event', {
+ error,
+ })
+ return
+ } finally {
+ this.deleting = false
+ }
+
+ showSuccess(t('dav', 'Custom example event was deleted successfully'))
+ this.hasCustomEvent = false
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+.example-event-settings {
+ margin-block: 2rem;
+
+ &__buttons {
+ display: flex;
+ gap: calc(var(--default-grid-baseline) * 2);
+ margin-top: calc(var(--default-grid-baseline) * 2);
+
+ &__download-link {
+ display: flex;
+ max-width: 100px;
+
+ &__label {
+ text-decoration: underline;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ }
+ }
+ }
+}
+
+.import-event-modal {
+ display: flex;
+ flex-direction: column;
+ gap: calc(var(--default-grid-baseline) * 2);
+ padding: calc(var(--default-grid-baseline) * 2);
+
+ &__file-picker {
+ width: 100%;
+ }
+
+ &__buttons {
+ display: flex;
+ justify-content: flex-end;
+ }
+}
+</style>
diff --git a/apps/dav/src/dav/client.js b/apps/dav/src/dav/client.js
index ff858e0492c..d286f6f48d6 100644
--- a/apps/dav/src/dav/client.js
+++ b/apps/dav/src/dav/client.js
@@ -1,39 +1,31 @@
-/*
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @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
*/
-import * as webdav from 'webdav'
-import axios from '@nextcloud/axios'
-import memoize from 'lodash/fp/memoize'
+import { createClient } from 'webdav'
+import memoize from 'lodash/fp/memoize.js'
import { generateRemoteUrl } from '@nextcloud/router'
-import { getCurrentUser } from '@nextcloud/auth'
+import { getCurrentUser, getRequestToken, onRequestTokenUpdate } from '@nextcloud/auth'
export const getClient = memoize((service) => {
- // Add this so the server knows it is an request from the browser
- axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
+ // init webdav client
+ const remote = generateRemoteUrl(`dav/${service}/${getCurrentUser().uid}`)
+ const client = createClient(remote)
- // force our axios
- const patcher = webdav.getPatcher()
- patcher.patch('request', axios)
+ // set CSRF token header
+ const setHeaders = (token) => {
+ client.setHeaders({
+ // Add this so the server knows it is an request from the browser
+ 'X-Requested-With': 'XMLHttpRequest',
+ // Inject user auth
+ requesttoken: token ?? '',
+ })
+ }
- return webdav.createClient(
- generateRemoteUrl(`dav/${service}/${getCurrentUser().uid}`)
- )
+ // refresh headers when request token changes
+ onRequestTokenUpdate(setHeaders)
+ setHeaders(getRequestToken())
+
+ return client
})
diff --git a/apps/dav/src/service/CalendarService.js b/apps/dav/src/service/CalendarService.js
index 2b416d6b670..93b36b8e74f 100644
--- a/apps/dav/src/service/CalendarService.js
+++ b/apps/dav/src/service/CalendarService.js
@@ -1,26 +1,10 @@
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @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
*/
-import { getClient } from '../dav/client'
-import logger from './logger'
-import { parseXML } from 'webdav/dist/node/tools/dav'
+import { getClient } from '../dav/client.js'
+import logger from './logger.js'
+import { parseXML } from 'webdav'
import {
slotsToVavailability,
@@ -58,7 +42,7 @@ export async function findScheduleInboxAvailability() {
</x0:propfind>`,
})
- const xml = await parseXML(response.data)
+ const xml = await parseXML(await response.text())
if (!xml) {
return undefined
diff --git a/apps/dav/src/service/ExampleEventService.js b/apps/dav/src/service/ExampleEventService.js
new file mode 100644
index 00000000000..a39e3641bd9
--- /dev/null
+++ b/apps/dav/src/service/ExampleEventService.js
@@ -0,0 +1,43 @@
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { generateUrl } from '@nextcloud/router'
+import axios from '@nextcloud/axios'
+
+/**
+ * Configure the creation of example events on a user's first login.
+ *
+ * @param {boolean} enable Whether to enable or disable the feature.
+ * @return {Promise<void>}
+ */
+export async function setCreateExampleEvent(enable) {
+ const url = generateUrl('/apps/dav/api/exampleEvent/enable')
+ await axios.post(url, {
+ enable,
+ })
+}
+
+/**
+ * Upload a custom example event.
+ *
+ * @param {string} ics The ICS data of the event.
+ * @return {Promise<void>}
+ */
+export async function uploadExampleEvent(ics) {
+ const url = generateUrl('/apps/dav/api/exampleEvent/event')
+ await axios.post(url, {
+ ics,
+ })
+}
+
+/**
+ * Delete a previously uploaded custom example event.
+ *
+ * @return {Promise<void>}
+ */
+export async function deleteExampleEvent() {
+ const url = generateUrl('/apps/dav/api/exampleEvent/event')
+ await axios.delete(url)
+}
diff --git a/apps/dav/src/service/PreferenceService.js b/apps/dav/src/service/PreferenceService.js
new file mode 100644
index 00000000000..39b2c067c61
--- /dev/null
+++ b/apps/dav/src/service/PreferenceService.js
@@ -0,0 +1,34 @@
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import axios from '@nextcloud/axios'
+import { generateOcsUrl } from '@nextcloud/router'
+
+/**
+ * Enable user status automation based on availability
+ */
+export async function enableUserStatusAutomation() {
+ return await axios.post(
+ generateOcsUrl('/apps/provisioning_api/api/v1/config/users/{appId}/{configKey}', {
+ appId: 'dav',
+ configKey: 'user_status_automation',
+ }),
+ {
+ configValue: 'yes',
+ },
+ )
+}
+
+/**
+ * Disable user status automation based on availability
+ */
+export async function disableUserStatusAutomation() {
+ return await axios.delete(
+ generateOcsUrl('/apps/provisioning_api/api/v1/config/users/{appId}/{configKey}', {
+ appId: 'dav',
+ configKey: 'user_status_automation',
+ }),
+ )
+}
diff --git a/apps/dav/src/service/logger.js b/apps/dav/src/service/logger.js
index dd6ec9163a6..cb7f1a95103 100644
--- a/apps/dav/src/service/logger.js
+++ b/apps/dav/src/service/logger.js
@@ -1,22 +1,6 @@
-/*
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @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
*/
import { getLoggerBuilder } from '@nextcloud/logger'
diff --git a/apps/dav/src/settings-example-content.js b/apps/dav/src/settings-example-content.js
new file mode 100644
index 00000000000..fdeba642a67
--- /dev/null
+++ b/apps/dav/src/settings-example-content.js
@@ -0,0 +1,18 @@
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import Vue from 'vue'
+import { translate } from '@nextcloud/l10n'
+import ExampleContentSettingsSection from './views/ExampleContentSettingsSection.vue'
+
+Vue.mixin({
+ methods: {
+ t: translate,
+ $t: translate,
+ },
+})
+
+const View = Vue.extend(ExampleContentSettingsSection);
+
+(new View({})).$mount('#settings-example-content')
diff --git a/apps/dav/src/settings-personal-availability.js b/apps/dav/src/settings-personal-availability.js
index b0d6b19aa8a..b24144b81f0 100644
--- a/apps/dav/src/settings-personal-availability.js
+++ b/apps/dav/src/settings-personal-availability.js
@@ -1,6 +1,10 @@
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
import Vue from 'vue'
import { translate } from '@nextcloud/l10n'
-import Availability from './views/Availability'
+import Availability from './views/Availability.vue'
Vue.prototype.$t = translate
diff --git a/apps/dav/src/settings.js b/apps/dav/src/settings.js
index 6744f22ad23..c69a8b03614 100644
--- a/apps/dav/src/settings.js
+++ b/apps/dav/src/settings.js
@@ -1,7 +1,11 @@
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
import Vue from 'vue'
import { loadState } from '@nextcloud/initial-state'
import { translate } from '@nextcloud/l10n'
-import CalDavSettings from './views/CalDavSettings'
+import CalDavSettings from './views/CalDavSettings.vue'
Vue.prototype.$t = translate
@@ -13,12 +17,12 @@ const CalDavSettingsView = new View({
sendInvitations: loadState('dav', 'sendInvitations'),
generateBirthdayCalendar: loadState(
'dav',
- 'generateBirthdayCalendar'
+ 'generateBirthdayCalendar',
),
sendEventReminders: loadState('dav', 'sendEventReminders'),
- sendEventRemindersToSharedGroupMembers: loadState(
+ sendEventRemindersToSharedUsers: loadState(
'dav',
- 'sendEventRemindersToSharedGroupMembers'
+ 'sendEventRemindersToSharedUsers',
),
sendEventRemindersPush: loadState('dav', 'sendEventRemindersPush'),
}
diff --git a/apps/dav/src/utils/date.js b/apps/dav/src/utils/date.js
new file mode 100644
index 00000000000..de1d65e310d
--- /dev/null
+++ b/apps/dav/src/utils/date.js
@@ -0,0 +1,17 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+/**
+ * Format a date as 'YYYY-MM-DD'.
+ *
+ * @param {Date} date A date instance to format.
+ * @return {string} 'YYYY-MM-DD'
+ */
+export function formatDateAsYMD(date) {
+ const year = date.getFullYear()
+ const month = (date.getMonth() + 1).toString().padStart(2, '0')
+ const day = date.getDate().toString().padStart(2, '0')
+ return `${year}-${month}-${day}`
+}
diff --git a/apps/dav/src/views/Availability.vue b/apps/dav/src/views/Availability.vue
index 928db771458..1922f5b706e 100644
--- a/apps/dav/src/views/Availability.vue
+++ b/apps/dav/src/views/Availability.vue
@@ -1,188 +1,40 @@
+<!--
+ - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
- <div class="section">
- <h2>{{ $t('dav', 'Availability') }}</h2>
- <p>
- {{ $t('dav', 'If you configure your working hours, other users will see when you are out of office when they book a meeting.') }}
- </p>
- <div class="time-zone">
- <strong>
- {{ $t('dav', 'Time zone:') }}
- </strong>
- <span class="time-zone-text">
- <TimezonePicker v-model="timezone" />
- </span>
- </div>
- <CalendarAvailability :slots.sync="slots"
- :loading="loading"
- :l10n-to="$t('dav', 'to')"
- :l10n-delete-slot="$t('dav', 'Delete slot')"
- :l10n-empty-day="$t('dav', 'No working hours set')"
- :l10n-add-slot="$t('dav', 'Add slot')"
- :l10n-monday="$t('dav', 'Monday')"
- :l10n-tuesday="$t('dav', 'Tuesday')"
- :l10n-wednesday="$t('dav', 'Wednesday')"
- :l10n-thursday="$t('dav', 'Thursday')"
- :l10n-friday="$t('dav', 'Friday')"
- :l10n-saturday="$t('dav', 'Saturday')"
- :l10n-sunday="$t('dav', 'Sunday')" />
- <Button :disabled="loading || saving"
- type="primary"
- @click="save">
- {{ $t('dav', 'Save') }}
- </Button>
+ <div>
+ <NcSettingsSection id="availability"
+ :name="$t('dav', 'Availability')"
+ :description="$t('dav', 'If you configure your working hours, other people will see when you are out of office when they book a meeting.')">
+ <AvailabilityForm />
+ </NcSettingsSection>
+ <NcSettingsSection v-if="!hideAbsenceSettings"
+ id="absence"
+ :name="$t('dav', 'Absence')"
+ :description="$t('dav', 'Configure your next absence period.')">
+ <AbsenceForm />
+ </NcSettingsSection>
</div>
</template>
<script>
-import { CalendarAvailability } from '@nextcloud/calendar-availability-vue'
-import {
- findScheduleInboxAvailability,
- getEmptySlots,
- saveScheduleInboxAvailability,
-} from '../service/CalendarService'
-import jstz from 'jstimezonedetect'
-import TimezonePicker from '@nextcloud/vue/dist/Components/TimezonePicker'
-import Button from '@nextcloud/vue/dist/Components/Button'
+import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
+import AbsenceForm from '../components/AbsenceForm.vue'
+import AvailabilityForm from '../components/AvailabilityForm.vue'
+import { loadState } from '@nextcloud/initial-state'
export default {
name: 'Availability',
components: {
- CalendarAvailability,
- TimezonePicker,
- Button,
+ NcSettingsSection,
+ AbsenceForm,
+ AvailabilityForm,
},
data() {
- // Try to determine the current timezone, and fall back to UTC otherwise
- const defaultTimezone = jstz.determine()
- const defaultTimezoneId = defaultTimezone ? defaultTimezone.name() : 'UTC'
-
return {
- loading: true,
- saving: false,
- timezone: defaultTimezoneId,
- slots: getEmptySlots(),
- }
- },
- async mounted() {
- try {
- const slotData = await findScheduleInboxAvailability()
- if (!slotData) {
- console.info('no availability is set')
- this.slots = getEmptySlots()
- } else {
- const { slots, timezoneId } = slotData
- this.slots = slots
- if (timezoneId) {
- this.timezone = timezoneId
- }
- console.info('availability loaded', this.slots, this.timezoneId)
- }
- } catch (e) {
- console.error('could not load existing availability', e)
-
- // TODO: show a nice toast
- } finally {
- this.loading = false
+ hideAbsenceSettings: loadState('dav', 'hide_absence_settings', true),
}
},
- methods: {
- async save() {
- try {
- this.saving = true
-
- await saveScheduleInboxAvailability(this.slots, this.timezone)
-
- // TODO: show a nice toast
- } catch (e) {
- console.error('could not save availability', e)
-
- // TODO: show a nice toast
- } finally {
- this.saving = false
- }
- },
- },
}
</script>
-
-<style lang="scss" scoped>
-.availability-day {
- padding: 0 10px 0 10px;
- position: absolute;
-}
-.availability-slots {
- display: flex;
- white-space: nowrap;
-}
-.availability-slot {
- display: flex;
- flex-direction: row;
- align-items: center;
-}
-.availability-slot-group {
- display: flex;
- flex-direction: column;
-}
-::v-deep .mx-input-wrapper {
- width: 85px;
-}
-::v-deep .mx-datepicker {
- width: 97px;
-}
-::v-deep .multiselect {
- border: 1px solid var(--color-border-dark);
- width: 120px;
-}
-.time-zone {
- padding: 32px 12px 12px 0;
-}
-.grid-table {
- display: grid;
- margin-bottom: 32px;
- grid-column-gap: 24px;
- grid-row-gap: 6px;
- grid-template-columns: min-content min-content min-content;
-}
-.button {
- align-self: flex-end;
-}
-.label-weekday {
- position: relative;
- display: inline-flex;
- padding-top: 4px;
-}
-.delete-slot {
- background-color: transparent;
- border: none;
- padding-bottom: 12px;
- opacity: .5;
- &:hover {
- opacity: 1;
- }
-}
-
-.add-another {
- background-color: transparent;
- border: none;
- opacity: .5;
- display: inline-flex;
- padding: 0;
- margin: 0;
- margin-bottom: 3px;
-
- &:hover {
- opacity: 1;
- }
-}
-.to-text {
- padding-right: 12px;
-}
-.time-zone-text{
- padding-left: 22px;
-}
-.empty-content {
- color: var(--color-text-lighter);
- margin-top: 4px;
-}
-
-</style>
diff --git a/apps/dav/src/views/CalDavSettings.spec.js b/apps/dav/src/views/CalDavSettings.spec.js
index 13abca03d5b..7a4345b3ddf 100644
--- a/apps/dav/src/views/CalDavSettings.spec.js
+++ b/apps/dav/src/views/CalDavSettings.spec.js
@@ -1,40 +1,35 @@
-import axios from '@nextcloud/axios'
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
import { render } from '@testing-library/vue'
-import userEvent from '@testing-library/user-event'
-import CalDavSettings from './CalDavSettings'
-// eslint-disable-next-line no-unused-vars
-import { generateUrl } from '@nextcloud/router'
+import { beforeEach, describe, expect, test, vi } from 'vitest'
-jest.mock('@nextcloud/axios')
-jest.mock('@nextcloud/router', () => {
+import CalDavSettings from './CalDavSettings.vue'
+
+vi.mock('@nextcloud/axios')
+vi.mock('@nextcloud/router', () => {
return {
generateUrl(url) {
return url
},
}
})
-jest.mock('@nextcloud/initial-state', () => {
+vi.mock('@nextcloud/initial-state', () => {
return {
- loadState: jest.fn(() => 'https://docs.nextcloud.com/server/23/go.php?to=user-sync-calendars'),
+ loadState: vi.fn(() => 'https://docs.nextcloud.com/server/23/go.php?to=user-sync-calendars'),
}
})
describe('CalDavSettings', () => {
- const originalOC = global.OC
- const originalOCP = global.OCP
-
beforeEach(() => {
- global.OC = { requestToken: 'secret' }
- global.OCP = {
+ window.OC = { requestToken: 'secret' }
+ window.OCP = {
AppConfig: {
- setValue: jest.fn(),
+ setValue: vi.fn(),
},
}
})
- afterAll(() => {
- global.OC = originalOC
- global.OCP = originalOCP
- })
test('interactions', async () => {
const TLUtils = render(
@@ -45,37 +40,40 @@ describe('CalDavSettings', () => {
sendInvitations: true,
generateBirthdayCalendar: true,
sendEventReminders: true,
- sendEventRemindersToSharedGroupMembers: true,
+ sendEventRemindersToSharedUsers: true,
sendEventRemindersPush: true,
}
},
},
Vue => {
- Vue.prototype.$t = jest.fn((app, text) => text)
- }
+ Vue.prototype.$t = vi.fn((app, text) => text)
+ },
)
expect(TLUtils.container).toMatchSnapshot()
const sendInvitations = TLUtils.getByLabelText(
- 'Send invitations to attendees'
+ 'Send invitations to attendees',
)
expect(sendInvitations).toBeChecked()
const generateBirthdayCalendar = TLUtils.getByLabelText(
- 'Automatically generate a birthday calendar'
+ 'Automatically generate a birthday calendar',
)
expect(generateBirthdayCalendar).toBeChecked()
const sendEventReminders = TLUtils.getByLabelText(
- 'Send notifications for events'
+ 'Send notifications for events',
)
expect(sendEventReminders).toBeChecked()
- const sendEventRemindersToSharedGroupMembers = TLUtils.getByLabelText(
- 'Send reminder notifications to calendar sharees as well'
+ const sendEventRemindersToSharedUsers = TLUtils.getByLabelText(
+ 'Send reminder notifications to calendar sharees as well',
)
- expect(sendEventRemindersToSharedGroupMembers).toBeChecked()
+ expect(sendEventRemindersToSharedUsers).toBeChecked()
const sendEventRemindersPush = TLUtils.getByLabelText(
- 'Enable notifications for events via push'
+ 'Enable notifications for events via push',
)
expect(sendEventRemindersPush).toBeChecked()
+ /*
+ FIXME userEvent.click is broken with nextcloud-vue/Button
+
await userEvent.click(sendInvitations)
expect(sendInvitations).not.toBeChecked()
expect(OCP.AppConfig.setValue).toHaveBeenCalledWith(
@@ -113,7 +111,7 @@ describe('CalDavSettings', () => {
'no'
)
- expect(sendEventRemindersToSharedGroupMembers).toBeDisabled()
+ expect(sendEventRemindersToSharedUsers).toBeDisabled()
expect(sendEventRemindersPush).toBeDisabled()
OCP.AppConfig.setValue.mockClear()
@@ -125,7 +123,8 @@ describe('CalDavSettings', () => {
'yes'
)
- expect(sendEventRemindersToSharedGroupMembers).toBeEnabled()
+ expect(sendEventRemindersToSharedUsers).toBeEnabled()
expect(sendEventRemindersPush).toBeEnabled()
+ */
})
})
diff --git a/apps/dav/src/views/CalDavSettings.vue b/apps/dav/src/views/CalDavSettings.vue
index f1d39abee42..6be67cf93ff 100644
--- a/apps/dav/src/views/CalDavSettings.vue
+++ b/apps/dav/src/views/CalDavSettings.vue
@@ -1,20 +1,21 @@
+<!--
+ - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
- <div class="section">
- <h2>{{ $t('dav', 'Calendar server') }}</h2>
+ <NcSettingsSection :name="$t('dav', 'Calendar server')"
+ :doc-url="userSyncCalendarsDocUrl">
<!-- Can use v-html as:
- $t passes the translated string through DOMPurify.sanitize,
- replacement strings are not user-controlled. -->
<!-- eslint-disable-next-line vue/no-v-html -->
<p class="settings-hint" v-html="hint" />
<p>
- <input id="caldavSendInvitations"
- v-model="sendInvitations"
- type="checkbox"
- class="checkbox">
- <label for="caldavSendInvitations">
+ <NcCheckboxRadioSwitch id="caldavSendInvitations"
+ :checked.sync="sendInvitations"
+ type="switch">
{{ $t('dav', 'Send invitations to attendees') }}
- </label>
- <br>
+ </NcCheckboxRadioSwitch>
<!-- Can use v-html as:
- $t passes the translated string through DOMPurify.sanitize,
- replacement strings are not user-controlled. -->
@@ -22,14 +23,12 @@
<em v-html="sendInvitationsHelpText" />
</p>
<p>
- <input id="caldavGenerateBirthdayCalendar"
- v-model="generateBirthdayCalendar"
- type="checkbox"
+ <NcCheckboxRadioSwitch id="caldavGenerateBirthdayCalendar"
+ :checked.sync="generateBirthdayCalendar"
+ type="switch"
class="checkbox">
- <label for="caldavGenerateBirthdayCalendar">
{{ $t('dav', 'Automatically generate a birthday calendar') }}
- </label>
- <br>
+ </NcCheckboxRadioSwitch>
<em>
{{ $t('dav', 'Birthday calendars will be generated by a background job.') }}
</em>
@@ -39,14 +38,11 @@
</em>
</p>
<p>
- <input id="caldavSendEventReminders"
- v-model="sendEventReminders"
- type="checkbox"
- class="checkbox">
- <label for="caldavSendEventReminders">
+ <NcCheckboxRadioSwitch id="caldavSendEventReminders"
+ :checked.sync="sendEventReminders"
+ type="switch">
{{ $t('dav', 'Send notifications for events') }}
- </label>
- <br>
+ </NcCheckboxRadioSwitch>
<!-- Can use v-html as:
- $t passes the translated string through DOMPurify.sanitize,
- replacement strings are not user-controlled. -->
@@ -58,47 +54,47 @@
</em>
</p>
<p class="indented">
- <input id="caldavSendEventRemindersToSharedGroupMembers"
- v-model="sendEventRemindersToSharedGroupMembers"
- type="checkbox"
- class="checkbox"
+ <NcCheckboxRadioSwitch id="caldavSendEventRemindersToSharedGroupMembers"
+ :checked.sync="sendEventRemindersToSharedUsers"
+ type="switch"
:disabled="!sendEventReminders">
- <label for="caldavSendEventRemindersToSharedGroupMembers">
{{ $t('dav', 'Send reminder notifications to calendar sharees as well' ) }}
- </label>
- <br>
+ </NcCheckboxRadioSwitch>
<em>
{{ $t('dav', 'Reminders are always sent to organizers and attendees.' ) }}
</em>
</p>
<p class="indented">
- <input id="caldavSendEventRemindersPush"
- v-model="sendEventRemindersPush"
- type="checkbox"
- class="checkbox"
+ <NcCheckboxRadioSwitch id="caldavSendEventRemindersPush"
+ :checked.sync="sendEventRemindersPush"
+ type="switch"
:disabled="!sendEventReminders">
- <label for="caldavSendEventRemindersPush">
{{ $t('dav', 'Enable notifications for events via push') }}
- </label>
+ </NcCheckboxRadioSwitch>
</p>
- </div>
+ </NcSettingsSection>
</template>
-<style lang="scss" scoped>
- .indented {
- padding-left: 28px;
- }
-</style>
-
<script>
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import { loadState } from '@nextcloud/initial-state'
+import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
const userSyncCalendarsDocUrl = loadState('dav', 'userSyncCalendarsDocUrl', '#')
export default {
name: 'CalDavSettings',
+ components: {
+ NcCheckboxRadioSwitch,
+ NcSettingsSection,
+ },
+ data() {
+ return {
+ userSyncCalendarsDocUrl,
+ }
+ },
computed: {
hint() {
const translated = this.$t(
@@ -132,17 +128,17 @@ export default {
OCP.AppConfig.setValue(
'dav',
'sendInvitations',
- value ? 'yes' : 'no'
+ value ? 'yes' : 'no',
)
},
sendEventReminders(value) {
OCP.AppConfig.setValue('dav', 'sendEventReminders', value ? 'yes' : 'no')
},
- sendEventRemindersToSharedGroupMembers(value) {
+ sendEventRemindersToSharedUsers(value) {
OCP.AppConfig.setValue(
'dav',
- 'sendEventRemindersToSharedGroupMembers',
- value ? 'yes' : 'no'
+ 'sendEventRemindersToSharedUsers',
+ value ? 'yes' : 'no',
)
},
sendEventRemindersPush(value) {
@@ -151,3 +147,19 @@ export default {
},
}
</script>
+
+<style scoped>
+ .indented {
+ padding-inline-start: 28px;
+ }
+ /** Use deep selector to affect v-html */
+ * :deep(a) {
+ text-decoration: underline;
+ }
+
+ .settings-hint {
+ margin-top: -.2em;
+ margin-bottom: 1em;
+ opacity: .7;
+ }
+</style>
diff --git a/apps/dav/src/views/ExampleContentSettingsSection.vue b/apps/dav/src/views/ExampleContentSettingsSection.vue
new file mode 100644
index 00000000000..3ee2d9e8648
--- /dev/null
+++ b/apps/dav/src/views/ExampleContentSettingsSection.vue
@@ -0,0 +1,38 @@
+<!--
+ - SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <NcSettingsSection id="example-content"
+ :name="$t('dav', 'Example content')"
+ class="example-content-setting"
+ :description="$t('dav', 'Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content.')">
+ <ExampleContactSettings v-if="hasContactsApp" />
+ <ExampleEventSettings v-if="hasCalendarApp" />
+ </NcSettingsSection>
+</template>
+
+<script>
+import { loadState } from '@nextcloud/initial-state'
+import { NcSettingsSection } from '@nextcloud/vue'
+import ExampleEventSettings from '../components/ExampleEventSettings.vue'
+import ExampleContactSettings from '../components/ExampleContactSettings.vue'
+
+export default {
+ name: 'ExampleContentSettingsSection',
+ components: {
+ NcSettingsSection,
+ ExampleContactSettings,
+ ExampleEventSettings,
+ },
+ computed: {
+ hasContactsApp() {
+ return loadState('dav', 'contactsEnabled')
+ },
+ hasCalendarApp() {
+ return loadState('dav', 'calendarEnabled')
+ },
+ },
+}
+</script>
diff --git a/apps/dav/src/views/__snapshots__/CalDavSettings.spec.js.snap b/apps/dav/src/views/__snapshots__/CalDavSettings.spec.js.snap
index 448361297fb..fdbe09f5b5e 100644
--- a/apps/dav/src/views/__snapshots__/CalDavSettings.spec.js.snap
+++ b/apps/dav/src/views/__snapshots__/CalDavSettings.spec.js.snap
@@ -1,16 +1,53 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-exports[`CalDavSettings interactions 1`] = `
+exports[`CalDavSettings > interactions 1`] = `
<div>
<div
- class="section"
+ class="settings-section settings-section--limit-width"
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
>
- <h2>
- Calendar server
+ <h2
+ class="settings-section__name"
+ data-v-6f6953b5=""
+ >
+ Calendar server
+ <a
+ aria-label="External documentation for Calendar server"
+ class="settings-section__info"
+ data-v-6f6953b5=""
+ href="https://docs.nextcloud.com/server/23/go.php?to=user-sync-calendars"
+ rel="noreferrer nofollow"
+ target="_blank"
+ title="External documentation for Calendar server"
+ >
+ <span
+ aria-hidden="true"
+ class="material-design-icon help-circle-icon"
+ data-v-6f6953b5=""
+ role="img"
+ >
+ <svg
+ class="material-design-icon__svg"
+ fill="currentColor"
+ height="20"
+ viewBox="0 0 24 24"
+ width="20"
+ >
+ <path
+ d="M15.07,11.25L14.17,12.17C13.45,12.89 13,13.5 13,15H11V14.5C11,13.39 11.45,12.39 12.17,11.67L13.41,10.41C13.78,10.05 14,9.55 14,9C14,7.89 13.1,7 12,7A2,2 0 0,0 10,9H8A4,4 0 0,1 12,5A4,4 0 0,1 16,9C16,9.88 15.64,10.67 15.07,11.25M13,19H11V17H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z"
+ >
+ <!---->
+ </path>
+ </svg>
+ </span>
+ </a>
</h2>
-
+ <!---->
<p
class="settings-hint"
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
>
Also install the
<a
@@ -29,25 +66,70 @@ exports[`CalDavSettings interactions 1`] = `
</a>
.
</p>
-
- <p>
- <input
- class="checkbox"
- id="caldavSendInvitations"
- type="checkbox"
- />
-
- <label
- for="caldavSendInvitations"
+ <p
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
+ >
+ <span
+ class="checkbox-radio-switch checkbox-radio-switch-switch checkbox-radio-switch--checked"
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
+ data-v-f275cf53=""
+ style="--icon-size: 36px; --icon-height: 16px;"
+ >
+ <input
+ aria-labelledby="caldavSendInvitations-label"
+ class="checkbox-radio-switch__input"
+ data-v-f275cf53=""
+ id="caldavSendInvitations"
+ type="checkbox"
+ value=""
+ />
+ <span
+ class="checkbox-content checkbox-radio-switch__content checkbox-content-switch checkbox-content--has-text"
+ data-v-3714b019=""
+ data-v-f275cf53=""
+ id="caldavSendInvitations-label"
+ >
+ <span
+ aria-hidden="true"
+ class="checkbox-content__icon checkbox-content__icon--checked checkbox-radio-switch__icon"
+ data-v-3714b019=""
+ inert="inert"
+ >
+ <span
+ aria-hidden="true"
+ class="material-design-icon toggle-switch-icon"
+ data-v-3714b019=""
+ role="img"
+ >
+ <svg
+ class="material-design-icon__svg"
+ fill="currentColor"
+ height="36"
+ viewBox="0 0 24 24"
+ width="36"
+ >
+ <path
+ d="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z"
+ >
+ <!---->
+ </path>
+ </svg>
+ </span>
+ </span>
+ <span
+ class="checkbox-content__text checkbox-radio-switch__text"
+ data-v-3714b019=""
+ >
+ Send invitations to attendees
+ </span>
+ </span>
+ </span>
+ <em
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
>
-
- Send invitations to attendees
-
- </label>
-
- <br />
-
- <em>
Please make sure to properly set up
<a
href="../admin#mail_general_settings"
@@ -57,57 +139,147 @@ exports[`CalDavSettings interactions 1`] = `
.
</em>
</p>
-
- <p>
- <input
- class="checkbox"
- id="caldavGenerateBirthdayCalendar"
- type="checkbox"
- />
-
- <label
- for="caldavGenerateBirthdayCalendar"
+ <p
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
+ >
+ <span
+ class="checkbox-radio-switch checkbox checkbox-radio-switch-switch checkbox-radio-switch--checked"
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
+ data-v-f275cf53=""
+ style="--icon-size: 36px; --icon-height: 16px;"
>
-
- Automatically generate a birthday calendar
-
- </label>
-
- <br />
-
- <em>
-
- Birthday calendars will be generated by a background job.
-
+ <input
+ aria-labelledby="caldavGenerateBirthdayCalendar-label"
+ class="checkbox-radio-switch__input"
+ data-v-f275cf53=""
+ id="caldavGenerateBirthdayCalendar"
+ type="checkbox"
+ value=""
+ />
+ <span
+ class="checkbox-content checkbox-radio-switch__content checkbox-content-switch checkbox-content--has-text"
+ data-v-3714b019=""
+ data-v-f275cf53=""
+ id="caldavGenerateBirthdayCalendar-label"
+ >
+ <span
+ aria-hidden="true"
+ class="checkbox-content__icon checkbox-content__icon--checked checkbox-radio-switch__icon"
+ data-v-3714b019=""
+ inert="inert"
+ >
+ <span
+ aria-hidden="true"
+ class="material-design-icon toggle-switch-icon"
+ data-v-3714b019=""
+ role="img"
+ >
+ <svg
+ class="material-design-icon__svg"
+ fill="currentColor"
+ height="36"
+ viewBox="0 0 24 24"
+ width="36"
+ >
+ <path
+ d="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z"
+ >
+ <!---->
+ </path>
+ </svg>
+ </span>
+ </span>
+ <span
+ class="checkbox-content__text checkbox-radio-switch__text"
+ data-v-3714b019=""
+ >
+ Automatically generate a birthday calendar
+ </span>
+ </span>
+ </span>
+ <em
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
+ >
+ Birthday calendars will be generated by a background job.
</em>
-
- <br />
-
- <em>
-
- Hence they will not be available immediately after enabling but will show up after some time.
-
+ <br
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
+ />
+ <em
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
+ >
+ Hence they will not be available immediately after enabling but will show up after some time.
</em>
</p>
-
- <p>
- <input
- class="checkbox"
- id="caldavSendEventReminders"
- type="checkbox"
- />
-
- <label
- for="caldavSendEventReminders"
+ <p
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
+ >
+ <span
+ class="checkbox-radio-switch checkbox-radio-switch-switch checkbox-radio-switch--checked"
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
+ data-v-f275cf53=""
+ style="--icon-size: 36px; --icon-height: 16px;"
+ >
+ <input
+ aria-labelledby="caldavSendEventReminders-label"
+ class="checkbox-radio-switch__input"
+ data-v-f275cf53=""
+ id="caldavSendEventReminders"
+ type="checkbox"
+ value=""
+ />
+ <span
+ class="checkbox-content checkbox-radio-switch__content checkbox-content-switch checkbox-content--has-text"
+ data-v-3714b019=""
+ data-v-f275cf53=""
+ id="caldavSendEventReminders-label"
+ >
+ <span
+ aria-hidden="true"
+ class="checkbox-content__icon checkbox-content__icon--checked checkbox-radio-switch__icon"
+ data-v-3714b019=""
+ inert="inert"
+ >
+ <span
+ aria-hidden="true"
+ class="material-design-icon toggle-switch-icon"
+ data-v-3714b019=""
+ role="img"
+ >
+ <svg
+ class="material-design-icon__svg"
+ fill="currentColor"
+ height="36"
+ viewBox="0 0 24 24"
+ width="36"
+ >
+ <path
+ d="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z"
+ >
+ <!---->
+ </path>
+ </svg>
+ </span>
+ </span>
+ <span
+ class="checkbox-content__text checkbox-radio-switch__text"
+ data-v-3714b019=""
+ >
+ Send notifications for events
+ </span>
+ </span>
+ </span>
+ <em
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
>
-
- Send notifications for events
-
- </label>
-
- <br />
-
- <em>
Please make sure to properly set up
<a
href="../admin#mail_general_settings"
@@ -116,58 +288,146 @@ exports[`CalDavSettings interactions 1`] = `
</a>
.
</em>
-
- <br />
-
- <em>
-
- Notifications are sent via background jobs, so these must occur often enough.
-
+ <br
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
+ />
+ <em
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
+ >
+ Notifications are sent via background jobs, so these must occur often enough.
</em>
</p>
-
<p
class="indented"
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
>
- <input
- class="checkbox"
- id="caldavSendEventRemindersToSharedGroupMembers"
- type="checkbox"
- />
-
- <label
- for="caldavSendEventRemindersToSharedGroupMembers"
+ <span
+ class="checkbox-radio-switch checkbox-radio-switch-switch checkbox-radio-switch--checked"
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
+ data-v-f275cf53=""
+ style="--icon-size: 36px; --icon-height: 16px;"
>
-
- Send reminder notifications to calendar sharees as well
-
- </label>
-
- <br />
-
- <em>
-
- Reminders are always sent to organizers and attendees.
-
+ <input
+ aria-labelledby="caldavSendEventRemindersToSharedGroupMembers-label"
+ class="checkbox-radio-switch__input"
+ data-v-f275cf53=""
+ id="caldavSendEventRemindersToSharedGroupMembers"
+ type="checkbox"
+ value=""
+ />
+ <span
+ class="checkbox-content checkbox-radio-switch__content checkbox-content-switch checkbox-content--has-text"
+ data-v-3714b019=""
+ data-v-f275cf53=""
+ id="caldavSendEventRemindersToSharedGroupMembers-label"
+ >
+ <span
+ aria-hidden="true"
+ class="checkbox-content__icon checkbox-content__icon--checked checkbox-radio-switch__icon"
+ data-v-3714b019=""
+ inert="inert"
+ >
+ <span
+ aria-hidden="true"
+ class="material-design-icon toggle-switch-icon"
+ data-v-3714b019=""
+ role="img"
+ >
+ <svg
+ class="material-design-icon__svg"
+ fill="currentColor"
+ height="36"
+ viewBox="0 0 24 24"
+ width="36"
+ >
+ <path
+ d="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z"
+ >
+ <!---->
+ </path>
+ </svg>
+ </span>
+ </span>
+ <span
+ class="checkbox-content__text checkbox-radio-switch__text"
+ data-v-3714b019=""
+ >
+ Send reminder notifications to calendar sharees as well
+ </span>
+ </span>
+ </span>
+ <em
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
+ >
+ Reminders are always sent to organizers and attendees.
</em>
</p>
-
<p
class="indented"
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
>
- <input
- class="checkbox"
- id="caldavSendEventRemindersPush"
- type="checkbox"
- />
-
- <label
- for="caldavSendEventRemindersPush"
+ <span
+ class="checkbox-radio-switch checkbox-radio-switch-switch checkbox-radio-switch--checked"
+ data-v-6b8d4c30=""
+ data-v-6f6953b5=""
+ data-v-f275cf53=""
+ style="--icon-size: 36px; --icon-height: 16px;"
>
-
- Enable notifications for events via push
-
- </label>
+ <input
+ aria-labelledby="caldavSendEventRemindersPush-label"
+ class="checkbox-radio-switch__input"
+ data-v-f275cf53=""
+ id="caldavSendEventRemindersPush"
+ type="checkbox"
+ value=""
+ />
+ <span
+ class="checkbox-content checkbox-radio-switch__content checkbox-content-switch checkbox-content--has-text"
+ data-v-3714b019=""
+ data-v-f275cf53=""
+ id="caldavSendEventRemindersPush-label"
+ >
+ <span
+ aria-hidden="true"
+ class="checkbox-content__icon checkbox-content__icon--checked checkbox-radio-switch__icon"
+ data-v-3714b019=""
+ inert="inert"
+ >
+ <span
+ aria-hidden="true"
+ class="material-design-icon toggle-switch-icon"
+ data-v-3714b019=""
+ role="img"
+ >
+ <svg
+ class="material-design-icon__svg"
+ fill="currentColor"
+ height="36"
+ viewBox="0 0 24 24"
+ width="36"
+ >
+ <path
+ d="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z"
+ >
+ <!---->
+ </path>
+ </svg>
+ </span>
+ </span>
+ <span
+ class="checkbox-content__text checkbox-radio-switch__text"
+ data-v-3714b019=""
+ >
+ Enable notifications for events via push
+ </span>
+ </span>
+ </span>
</p>
</div>
</div>
diff --git a/apps/dav/src/views/__snapshots__/CalDavSettings.spec.js.snap.license b/apps/dav/src/views/__snapshots__/CalDavSettings.spec.js.snap.license
new file mode 100644
index 00000000000..b8f52265f1f
--- /dev/null
+++ b/apps/dav/src/views/__snapshots__/CalDavSettings.spec.js.snap.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+SPDX-License-Identifier: AGPL-3.0-or-later \ No newline at end of file
diff --git a/apps/dav/templates/schedule-response-error.php b/apps/dav/templates/schedule-response-error.php
index 010ea2ed6cb..9604c83f922 100644
--- a/apps/dav/templates/schedule-response-error.php
+++ b/apps/dav/templates/schedule-response-error.php
@@ -1,7 +1,16 @@
-<div class="update">
- <p class="message"><?php p($l->t('There was an error updating your attendance status.'));?></p>
- <p class="message"><?php p($l->t('Please contact the organizer directly.'));?></p>
- <?php if (isset($_['organizer'])): ?>
- <p class="message"><a href="<?php p($_['organizer']) ?>"><?php p(substr($_['organizer'], 7)) ?></a></p>
- <?php endif; ?>
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+?>
+<div class="guest-box">
+ <div class="notecard error">
+ <p><?php p($l->t('There was an error updating your attendance status.'));?></p>
+ <p><?php p($l->t('Please contact the organizer directly.'));?></p>
+ <?php if (isset($_['organizer'])): ?>
+ <p><a href="<?php p($_['organizer']) ?>"><?php p(substr($_['organizer'], 7)) ?></a></p>
+ <?php endif; ?>
+ </div>
</div>
diff --git a/apps/dav/templates/schedule-response-options.php b/apps/dav/templates/schedule-response-options.php
index f5ebb46cb26..30020cc8535 100644
--- a/apps/dav/templates/schedule-response-options.php
+++ b/apps/dav/templates/schedule-response-options.php
@@ -1,8 +1,12 @@
<?php
-style('dav', 'schedule-response');
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+\OCP\Util::addStyle('dav', 'schedule-response');
?>
-<div class="update">
+<div class="guest-box">
<form action="" method="post">
<fieldset id="partStat">
<h2><?php p($l->t('Are you accepting the invitation?')); ?></h2>
@@ -23,10 +27,6 @@ style('dav', 'schedule-response');
</label>
</div>
</fieldset>
- <fieldset id="more_options">
- <input type="number" min="0" name="guests" placeholder="<?php p($l->t('Number of guests')); ?>" />
- <input type="text" name="comment" placeholder="<?php p($l->t('Comment')); ?>" />
- </fieldset>
<fieldset>
<input type="submit" value="<?php p($l->t('Save'));?>">
</fieldset>
diff --git a/apps/dav/templates/schedule-response-success.php b/apps/dav/templates/schedule-response-success.php
index f60cb1e0fa9..4651b9ea964 100644
--- a/apps/dav/templates/schedule-response-success.php
+++ b/apps/dav/templates/schedule-response-success.php
@@ -1,4 +1,11 @@
-<div class="update" style="justify-content: space-around; display: flex;">
- <span class="icon icon-checkmark-white"></span>
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+?>
+<div class="guest-box">
+ <div class="icon icon-checkmark"></div>
<p class="message"><?php p($l->t('Your attendance was updated successfully.'));?></p>
</div>
diff --git a/apps/dav/templates/settings-admin-caldav.php b/apps/dav/templates/settings-admin-caldav.php
index 3a67f21a9cc..636114ced75 100644
--- a/apps/dav/templates/settings-admin-caldav.php
+++ b/apps/dav/templates/settings-admin-caldav.php
@@ -1,28 +1,10 @@
<?php
+
/**
- * @copyright 2017, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @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
*/
-
-script('dav', 'settings-admin-caldav');
+\OCP\Util::addScript('dav', 'settings-admin-caldav', 'core');
?>
diff --git a/apps/dav/templates/settings-example-content.php b/apps/dav/templates/settings-example-content.php
new file mode 100644
index 00000000000..7c6f15bd82b
--- /dev/null
+++ b/apps/dav/templates/settings-example-content.php
@@ -0,0 +1,11 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+\OCP\Util::addScript('dav', 'settings-example-content', 'core');
+
+?>
+
+<div id="settings-example-content"></div>
diff --git a/apps/dav/templates/settings-personal-availability.php b/apps/dav/templates/settings-personal-availability.php
index b6a4b62602e..f1c049392a3 100644
--- a/apps/dav/templates/settings-personal-availability.php
+++ b/apps/dav/templates/settings-personal-availability.php
@@ -1,28 +1,10 @@
<?php
+
/**
- * @copyright 2017, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @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
*/
-
-script('dav', 'settings-personal-availability');
+\OCP\Util::addScript('dav', 'settings-personal-availability', 'core');
?>
diff --git a/apps/dav/tests/benchmarks/benchmark.sh b/apps/dav/tests/benchmarks/benchmark.sh
index 27d7c4ecbc7..613a83fe68a 100755
--- a/apps/dav/tests/benchmarks/benchmark.sh
+++ b/apps/dav/tests/benchmarks/benchmark.sh
@@ -1,5 +1,8 @@
#!/bin/bash
-
+#
+# SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
+#
set -eu
# benchmark.sh
diff --git a/apps/dav/tests/benchmarks/bulk_upload.sh b/apps/dav/tests/benchmarks/bulk_upload.sh
index e2099188654..346403a3d61 100755
--- a/apps/dav/tests/benchmarks/bulk_upload.sh
+++ b/apps/dav/tests/benchmarks/bulk_upload.sh
@@ -1,5 +1,8 @@
#!/bin/bash
-
+#
+# SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
+#
set -eu
# bulk_upload.sh <nb-of-files> <size-of-files>
diff --git a/apps/dav/tests/benchmarks/single_upload.sh b/apps/dav/tests/benchmarks/single_upload.sh
index ec57e66668d..e28a5deedb9 100755
--- a/apps/dav/tests/benchmarks/single_upload.sh
+++ b/apps/dav/tests/benchmarks/single_upload.sh
@@ -1,5 +1,8 @@
#!/bin/bash
-
+#
+# SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
+#
set -eu
# single_upload.sh <nb-of-files> <size-of-files>
diff --git a/apps/dav/tests/integration/DAV/Sharing/CalDavSharingBackendTest.php b/apps/dav/tests/integration/DAV/Sharing/CalDavSharingBackendTest.php
new file mode 100644
index 00000000000..be06e8e4d4b
--- /dev/null
+++ b/apps/dav/tests/integration/DAV/Sharing/CalDavSharingBackendTest.php
@@ -0,0 +1,251 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\integration\DAV\Sharing;
+
+use OC\Memcache\NullCache;
+use OCA\DAV\CalDAV\Calendar;
+use OCA\DAV\CalDAV\Sharing\Service;
+use OCA\DAV\Connector\Sabre\Principal;
+use OCA\DAV\DAV\Sharing\Backend;
+use OCA\DAV\DAV\Sharing\SharingMapper;
+use OCA\DAV\DAV\Sharing\SharingService;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\ICacheFactory;
+use OCP\IDBConnection;
+use OCP\IGroupManager;
+use OCP\IUserManager;
+use OCP\Server;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+/**
+ * @group DB
+ */
+class CalDavSharingBackendTest extends TestCase {
+
+ private IDBConnection $db;
+ private IUserManager $userManager;
+ private IGroupManager $groupManager;
+ private Principal $principalBackend;
+ private ICacheFactory $cacheFactory;
+ private LoggerInterface $logger;
+ private SharingMapper $sharingMapper;
+ private SharingService $sharingService;
+ private Backend $sharingBackend;
+
+ private $resourceIds = [10001];
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->db = Server::get(IDBConnection::class);
+
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->principalBackend = $this->createMock(Principal::class);
+ $this->cacheFactory = $this->createMock(ICacheFactory::class);
+ $this->cacheFactory->method('createInMemory')
+ ->willReturn(new NullCache());
+ $this->logger = new \Psr\Log\NullLogger();
+
+ $this->sharingMapper = new SharingMapper($this->db);
+ $this->sharingService = new Service($this->sharingMapper);
+
+ $this->sharingBackend = new \OCA\DAV\CalDAV\Sharing\Backend(
+ $this->userManager,
+ $this->groupManager,
+ $this->principalBackend,
+ $this->cacheFactory,
+ $this->sharingService,
+ $this->logger
+ );
+
+ $this->removeFixtures();
+ }
+
+ protected function tearDown(): void {
+ $this->removeFixtures();
+ }
+
+ protected function removeFixtures(): void {
+ $qb = $this->db->getQueryBuilder();
+ $qb->delete('dav_shares')
+ ->where($qb->expr()->in('resourceid', $qb->createNamedParameter($this->resourceIds, IQueryBuilder::PARAM_INT_ARRAY)));
+ $qb->executeStatement();
+ }
+
+ public function testShareCalendarWithGroup(): void {
+ $calendar = $this->createMock(Calendar::class);
+ $calendar->method('getResourceId')
+ ->willReturn(10001);
+ $calendar->method('getOwner')
+ ->willReturn('principals/users/admin');
+
+ $this->principalBackend->method('findByUri')
+ ->willReturn('principals/groups/alice_bob');
+
+ $this->groupManager->method('groupExists')
+ ->willReturn(true);
+
+ $this->sharingBackend->updateShares(
+ $calendar,
+ [['href' => 'principals/groups/alice_bob']],
+ [],
+ []
+ );
+
+ $this->assertCount(1, $this->sharingService->getShares(10001));
+ }
+
+ public function testUnshareCalendarFromGroup(): void {
+ $calendar = $this->createMock(Calendar::class);
+ $calendar->method('getResourceId')
+ ->willReturn(10001);
+ $calendar->method('getOwner')
+ ->willReturn('principals/users/admin');
+
+ $this->principalBackend->method('findByUri')
+ ->willReturn('principals/groups/alice_bob');
+
+ $this->groupManager->method('groupExists')
+ ->willReturn(true);
+
+ $this->sharingBackend->updateShares(
+ shareable: $calendar,
+ add: [['href' => 'principals/groups/alice_bob']],
+ remove: [],
+ );
+
+ $this->assertCount(1, $this->sharingService->getShares(10001));
+
+ $this->sharingBackend->updateShares(
+ shareable: $calendar,
+ add: [],
+ remove: ['principals/groups/alice_bob'],
+ );
+
+ $this->assertCount(0, $this->sharingService->getShares(10001));
+ }
+
+ public function testShareCalendarWithGroupAndUnshareAsUser(): void {
+ $calendar = $this->createMock(Calendar::class);
+ $calendar->method('getResourceId')
+ ->willReturn(10001);
+ $calendar->method('getOwner')
+ ->willReturn('principals/users/admin');
+
+ $this->principalBackend->method('findByUri')
+ ->willReturnMap([
+ ['principals/groups/alice_bob', '', 'principals/groups/alice_bob'],
+ ['principals/users/bob', '', 'principals/users/bob'],
+ ]);
+ $this->principalBackend->method('getGroupMembership')
+ ->willReturn([
+ 'principals/groups/alice_bob',
+ ]);
+ $this->principalBackend->method('getCircleMembership')
+ ->willReturn([]);
+
+ $this->groupManager->method('groupExists')
+ ->willReturn(true);
+
+ /*
+ * Owner is sharing the calendar with a group.
+ */
+ $this->sharingBackend->updateShares(
+ shareable: $calendar,
+ add: [['href' => 'principals/groups/alice_bob']],
+ remove: [],
+ );
+
+ $this->assertCount(1, $this->sharingService->getShares(10001));
+
+ /*
+ * Member of the group unshares the calendar.
+ */
+ $this->sharingBackend->unshare(
+ shareable: $calendar,
+ principalUri: 'principals/users/bob'
+ );
+
+ $this->assertCount(1, $this->sharingService->getShares(10001));
+ $this->assertCount(1, $this->sharingService->getUnshares(10001));
+ }
+
+ /**
+ * Tests the functionality of sharing a calendar with a user, then with a group (that includes the shared user),
+ * and subsequently unsharing it from the individual user. Verifies that the unshare operation correctly removes the specific user share
+ * without creating an additional unshare entry.
+ */
+ public function testShareCalendarWithUserThenGroupThenUnshareUser(): void {
+ $calendar = $this->createMock(Calendar::class);
+ $calendar->method('getResourceId')
+ ->willReturn(10001);
+ $calendar->method('getOwner')
+ ->willReturn('principals/users/admin');
+
+ $this->principalBackend->method('findByUri')
+ ->willReturnMap([
+ ['principals/groups/alice_bob', '', 'principals/groups/alice_bob'],
+ ['principals/users/bob', '', 'principals/users/bob'],
+ ]);
+ $this->principalBackend->method('getGroupMembership')
+ ->willReturn([
+ 'principals/groups/alice_bob',
+ ]);
+ $this->principalBackend->method('getCircleMembership')
+ ->willReturn([]);
+
+ $this->userManager->method('userExists')
+ ->willReturn(true);
+ $this->groupManager->method('groupExists')
+ ->willReturn(true);
+
+ /*
+ * Step 1) The owner shares the calendar with a user.
+ */
+ $this->sharingBackend->updateShares(
+ shareable: $calendar,
+ add: [['href' => 'principals/users/bob']],
+ remove: [],
+ );
+
+ $this->assertCount(1, $this->sharingService->getShares(10001));
+
+ /*
+ * Step 2) The owner shares the calendar with a group that includes the
+ * user from step 1 as a member.
+ */
+ $this->sharingBackend->updateShares(
+ shareable: $calendar,
+ add: [['href' => 'principals/groups/alice_bob']],
+ remove: [],
+ );
+
+ $this->assertCount(2, $this->sharingService->getShares(10001));
+
+ /*
+ * Step 3) Unshare the calendar from user as owner.
+ */
+ $this->sharingBackend->updateShares(
+ shareable: $calendar,
+ add: [],
+ remove: ['principals/users/bob'],
+ );
+
+ /*
+ * The purpose of this test is to ensure that removing a user from a share, as the owner, does not result in an "unshare" row being added.
+ * Instead, the actual user share should be removed.
+ */
+ $this->assertCount(1, $this->sharingService->getShares(10001));
+ $this->assertCount(0, $this->sharingService->getUnshares(10001));
+ }
+
+}
diff --git a/apps/dav/tests/integration/DAV/Sharing/SharingMapperTest.php b/apps/dav/tests/integration/DAV/Sharing/SharingMapperTest.php
new file mode 100644
index 00000000000..bcf84254034
--- /dev/null
+++ b/apps/dav/tests/integration/DAV/Sharing/SharingMapperTest.php
@@ -0,0 +1,95 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\integration\DAV\Sharing;
+
+use OCA\DAV\DAV\Sharing\SharingMapper;
+use OCP\IDBConnection;
+use OCP\Server;
+use Test\TestCase;
+
+/**
+ * @group DB
+ */
+class SharingMapperTest extends TestCase {
+
+ private SharingMapper $mapper;
+ private IDBConnection $db;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->db = Server::get(IDBConnection::class);
+ $this->mapper = new SharingMapper($this->db);
+ $qb = $this->db->getQueryBuilder();
+ $qb->delete('dav_shares')->executeStatement();
+ }
+
+ public function testShareAndGet(): void {
+ $resourceId = 42;
+ $resourceType = 'calendar';
+ $access = 3;
+ $principal = 'principals/users/bob';
+ $this->mapper->share($resourceId, $resourceType, $access, $principal);
+ $shares = $this->mapper->getSharesForId($resourceId, $resourceType);
+ $this->assertCount(1, $shares);
+ }
+
+ public function testShareDelete(): void {
+ $resourceId = 42;
+ $resourceType = 'calendar';
+ $access = 3;
+ $principal = 'principals/users/bob';
+ $this->mapper->share($resourceId, $resourceType, $access, $principal);
+ $this->mapper->deleteShare($resourceId, $resourceType, $principal);
+ $shares = $this->mapper->getSharesForId($resourceId, $resourceType);
+ $this->assertEmpty($shares);
+ }
+
+ public function testShareUnshare(): void {
+ $resourceId = 42;
+ $resourceType = 'calendar';
+ $access = 3;
+ $principal = 'principals/groups/alicegroup';
+ $userPrincipal = 'principals/users/alice';
+ $this->mapper->share($resourceId, $resourceType, $access, $principal);
+ $this->mapper->unshare($resourceId, $resourceType, $userPrincipal);
+ $shares = $this->mapper->getSharesForId($resourceId, $resourceType);
+ $this->assertCount(1, $shares);
+ }
+
+ public function testShareDeleteAll(): void {
+ $resourceId = 42;
+ $resourceType = 'calendar';
+ $access = 3;
+ $principal = 'principals/groups/alicegroup';
+ $userPrincipal = 'principals/users/alice';
+ $this->mapper->share($resourceId, $resourceType, $access, $principal);
+ $this->mapper->unshare($resourceId, $resourceType, $userPrincipal);
+ $shares = $this->mapper->getSharesForId($resourceId, $resourceType);
+ $this->assertCount(1, $shares);
+ $this->mapper->deleteAllShares($resourceId, $resourceType);
+ $shares = $this->mapper->getSharesForId($resourceId, $resourceType);
+ $this->assertEmpty($shares);
+ }
+
+ public function testShareDeleteAllForUser(): void {
+ $resourceId = 42;
+ $resourceType = 'calendar';
+ $access = 3;
+ $principal = 'principals/groups/alicegroup';
+ $this->mapper->share($resourceId, $resourceType, $access, $principal);
+ $shares = $this->mapper->getSharesForId($resourceId, $resourceType);
+ $this->assertCount(1, $shares);
+ $this->mapper->deleteAllSharesByUser($principal, $resourceType);
+ $shares = $this->mapper->getSharesForId($resourceId, $resourceType);
+ $this->assertEmpty($shares);
+ }
+
+}
diff --git a/apps/dav/tests/integration/Db/PropertyMapperTest.php b/apps/dav/tests/integration/Db/PropertyMapperTest.php
new file mode 100644
index 00000000000..9bd47a82d35
--- /dev/null
+++ b/apps/dav/tests/integration/Db/PropertyMapperTest.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\integration\Db;
+
+use OCA\DAV\Db\PropertyMapper;
+use OCP\Server;
+use Test\TestCase;
+
+/**
+ * @group DB
+ */
+class PropertyMapperTest extends TestCase {
+
+ /** @var PropertyMapper */
+ private PropertyMapper $mapper;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->mapper = Server::get(PropertyMapper::class);
+ }
+
+ public function testFindNonExistent(): void {
+ $props = $this->mapper->findPropertyByPathAndName(
+ 'userthatdoesnotexist',
+ 'path/that/does/not/exist/either',
+ 'nope',
+ );
+
+ self::assertEmpty($props);
+ }
+
+}
diff --git a/apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php b/apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php
index cf0b24e9a69..0682733a103 100644
--- a/apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php
+++ b/apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php
@@ -3,30 +3,12 @@
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\Tests\integration\UserMigration;
-use function Safe\scandir;
use OCA\DAV\AppInfo\Application;
use OCA\DAV\UserMigration\CalendarMigrator;
use OCP\AppFramework\App;
@@ -38,6 +20,7 @@ use Sabre\VObject\Reader as VObjectReader;
use Sabre\VObject\UUIDUtil;
use Symfony\Component\Console\Output\OutputInterface;
use Test\TestCase;
+use function scandir;
/**
* @group DB
@@ -61,7 +44,7 @@ class CalendarMigratorTest extends TestCase {
$this->output = $this->createMock(OutputInterface::class);
}
- public function dataAssets(): array {
+ public static function dataAssets(): array {
return array_map(
function (string $filename) {
/** @var VCalendar $vCalendar */
@@ -85,7 +68,7 @@ class CalendarMigratorTest extends TestCase {
fn (VObjectProperty $property) => $property->serialize(),
array_values(array_filter(
$vCalendar->children(),
- fn (mixed $child) => $child instanceof VObjectProperty,
+ fn ($child) => $child instanceof VObjectProperty,
)),
);
}
@@ -106,9 +89,7 @@ class CalendarMigratorTest extends TestCase {
);
}
- /**
- * @dataProvider dataAssets
- */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataAssets')]
public function testImportExportAsset(string $userId, string $filename, string $initialCalendarUri, VCalendar $importCalendar): void {
$user = $this->userManager->createUser($userId, 'topsecretpassword');
diff --git a/apps/dav/tests/integration/UserMigration/ContactsMigratorTest.php b/apps/dav/tests/integration/UserMigration/ContactsMigratorTest.php
index 568b83396ba..9ae1ca28501 100644
--- a/apps/dav/tests/integration/UserMigration/ContactsMigratorTest.php
+++ b/apps/dav/tests/integration/UserMigration/ContactsMigratorTest.php
@@ -3,30 +3,12 @@
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\Tests\integration\UserMigration;
-use function Safe\scandir;
use OCA\DAV\AppInfo\Application;
use OCA\DAV\UserMigration\ContactsMigrator;
use OCP\AppFramework\App;
@@ -38,6 +20,7 @@ use Sabre\VObject\Splitter\VCard as VCardSplitter;
use Sabre\VObject\UUIDUtil;
use Symfony\Component\Console\Output\OutputInterface;
use Test\TestCase;
+use function scandir;
/**
* @group DB
@@ -61,7 +44,7 @@ class ContactsMigratorTest extends TestCase {
$this->output = $this->createMock(OutputInterface::class);
}
- public function dataAssets(): array {
+ public static function dataAssets(): array {
return array_map(
function (string $filename) {
$vCardSplitter = new VCardSplitter(
@@ -108,11 +91,11 @@ class ContactsMigratorTest extends TestCase {
}
/**
- * @dataProvider dataAssets
*
* @param array{displayName: string, description?: string} $importMetadata
* @param VCard[] $importCards
*/
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataAssets')]
public function testImportExportAsset(string $userId, string $filename, string $initialAddressBookUri, array $importMetadata, array $importCards): void {
$user = $this->userManager->createUser($userId, 'topsecretpassword');
@@ -131,20 +114,30 @@ class ContactsMigratorTest extends TestCase {
$exportMetadata = array_filter(['displayName' => $displayName, 'description' => $description]);
$this->assertEquals($importMetadata, $exportMetadata);
- $this->assertEquals(count($importCards), count($exportCards));
+ $this->assertSameSize($importCards, $exportCards);
- for ($i = 0; $i < count($importCards); ++$i) {
- $this->assertNotEqualsCanonicalizing(
- $this->getPropertiesChangedOnImport($importCards[$i]),
- $this->getPropertiesChangedOnImport($exportCards[$i]),
- );
+ $importProperties = [];
+ $exportProperties = [];
+ for ($i = 0, $iMax = count($importCards); $i < $iMax; ++$i) {
+ $importProperties[] = $this->getPropertiesChangedOnImport($importCards[$i]);
+ $exportProperties[] = $this->getPropertiesChangedOnImport($exportCards[$i]);
}
- for ($i = 0; $i < count($importCards); ++$i) {
- $this->assertEqualsCanonicalizing(
- $this->getProperties($importCards[$i]),
- $this->getProperties($exportCards[$i]),
- );
+ $this->assertNotEqualsCanonicalizing(
+ $importProperties,
+ $exportProperties,
+ );
+
+ $importProperties = [];
+ $exportProperties = [];
+ for ($i = 0, $iMax = count($importCards); $i < $iMax; ++$i) {
+ $importProperties[] = $this->getProperties($importCards[$i]);
+ $exportProperties[] = $this->getProperties($exportCards[$i]);
}
+
+ $this->assertEqualsCanonicalizing(
+ $importProperties,
+ $exportProperties,
+ );
}
}
diff --git a/apps/dav/tests/integration/UserMigration/assets/address_books/contact-complex.vcf b/apps/dav/tests/integration/UserMigration/assets/address_books/contact-complex.vcf
index 647c4019e68..d7fe063cb2d 100644
--- a/apps/dav/tests/integration/UserMigration/assets/address_books/contact-complex.vcf
+++ b/apps/dav/tests/integration/UserMigration/assets/address_books/contact-complex.vcf
@@ -8,15889 +8,6 @@ ADR;TYPE=HOME:ABC;123 River St. Unit #5;123 River St.;Los Angeles;Californi
EMAIL;TYPE=HOME:bob@example.org
TEL;TYPE="HOME,VOICE":+1 505-644-0462
TITLE:Engineer
-PHOTO;VALUE=URI:data:image/jpeg;base64\,/9j/4AAQSkZJRgABAQEASABIAAD/4gIcSUN
- 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
- yY7BvNvSlxK1Yw8tbm5TVilrkySvLvfhuZhCzTKb6PIYYda0ZDCTR2G5DD1toUUkYpCCGG3mYr
- TL7bpXsTWXqr2LKMaa1Elp6oTUhu2lhD5B2SnazPpyjnSq7D1s1ow1XdIsK2bk6VpIppObf0LT
- 8jteV2+i7Xjs7n9HoWGxOB38zaZ/Ftd7y2yextyG0LFbqMfUjd7y/qLyXsp1l3rrfG0VvTnYP0
- v5LWc13WC8r6rz90fnWQ8p776F8K8/67u+b6XX0Nbyevb5/kOOTb3LmfH8ttp9C3/njZZdfR+M
- TKLRVSba49AheTdzb8x5H9F1fl/B9XmfQNrwf6ORvXHNeO8WRutScnua9NNaXHPrE6JVcTvelz
- IW9h+ztHPj7HOWHNv8AzgonN/ffzXCbmsFIxuIIIA5EuA5Apwg7ZgFDcaWrgJDKsAK5AAqbiVK
- xrDkZCzUHJ0lyBRFIsKWC0T4SxkSW5GSkFIw4s1LYWbIHW1roWSSW1YSmMSFJatKHGjQEqSyLN
- IDurYUXediykK3kvJolPtSqb0IktAwmrBi2mI7IIGA+spGWwViA0nIYAkI1hSHUkAsG5XEmsBk
- KdOMgKTHC2lxVtrEhEsoSWlcgbeVFYTITDHEgokdD5EMx5CLEiE+h8jJSG47QeRAg1OxIpTEyR
- HXRIwUopGDcUVaSo5CJZNCCjgBOkCSwciAsAgGYJOIKMSwIFLQTx1KiVlkG5Y4EGAayEsfNl2O
- 4SThQRCBCXUwNKWcRCm3Cjr7ShfMEaShcUlNdzhpdhaC1CNiQ80ZkG/XYt41V3PPMLSx9CWxa5
- IiiJZv1ctNFvNp7PPrmuCRTqQt0JZDj2TLpDEwikFyc+rxJM6wp109i9Qo+pRnK0y6y7x6MFM2
- oteBKVrBaDyo8RmS1IytJFUglBYrVg2ZXtWceCuanxnrr4ljEeuvZnxmywW7YMsAXWly9Cu0U8
- uV6RivYbiVtc8xv5EkoratYu0qle6dopFWi1ZjSar6+BewLaaZm1jacUGezb1XTuhZPo3A9f6g
- 9oeG+w/Ovf++aTz/K9N4XpHCOX2PmfZ9Z4t0LsGe7h1n6nw3U43z05/1Hn1Hob2qd6orc2wXeW
- mXzX0r0MzblPY5dnkdDTUypVFsqRhOeaqZvjPsVF6/y/K+laHU2pk3terndPnWp2G1rbY9Se6X
- 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
- 3VrmpbEetZOXdXWzsqjV0SbjbTkehuup8Bm03+qI3nhHN6XbsHhspuxXPLddXdzz+as2JmnBNs
- YacvSYjNIvxPElYVDj9gmiJZS7rJ0Mi9sDqszKOgyKNfM4/RXXr5yvrEKu3nVxaqhhU+xSj03d
- cJr+Z1dNsw55/u7zS8m6Bgu2MyDq0o8vyezZPRZxy46LqIfO6PT1TJ5ggepKFj5nY9EP3V+ZaL
- 2QGTwvR+38F0+f5EpO+4vv8DitN1fI9zy+KZuGujxqNNiizLCE4zIa5xA16J0Rq2SMmoMNkZJN
- ABWaDMdNgESUIekSahIhLqYTcZciuusvh1PoeGg323lIcJ4F1xC0tXBs0GUZW7FlFaqUT5I5TF
- l64WRyQHJylkI7ASyscnrZYq5Lld8UrEQVyZjZqaeeUtzbqnVtSpxIYKJYYycWI0iQkxISchty
- TggsXTQNS1dMtXRRdFHeqjdmx7MzsqPNXVIkxZefTYSkSsu6RpK24xdO2taqVh6k6vfjI9bX3s
- bTlzh2rOjHVtWsGyqIxZIdIbrsgCLJkNpcmvOqtyk0S7qWwtLVglBqkNONugUoI6CefdITjylZ
- K5LqvGVJIWxzdMRpmQkOwdmSNEUboLCksqX1Jlq0e1tL/ABdaLr4+o5PaiXhW/P31/fc73zlau
- J+gcz1rZybum1Vf7j53YUOgrLqKOp0V3y+rzOs6+nNt4H5S9vcF8R9C8F4f0ZyTsDCafQ9dBwX
- mf6WeLac7uU9mZPL1PKx+hmtVXAq72/ZY7fnNJ9jcs6vP5Jfd23fN6XleD1bj+2mDWos+xwMzD
- 3UqHmcLWZ/ocqHpoHTMuzfbmyq/nv0XYdH5p0/nX5ew7pG18+q61i+udbz1XjJPL82o8bYeeed
- 3+h1vIbrfR17kUaE4ysmxn9TDVYC6552ODea/m+N63C7ZF47t3TV2fO64rm25bfq/Axzfekiuy
- 3UuZsYjldtg/Uv13Nt2DsiXXGqtNlSXOfElVz8iynDtSo2/hkttoiZLqH4buRRzqN13MyjtWu+
- k5cgdhJyMmjTq282dd1zBhtWVaCVmFJfeqoFK1nX18e/HauVYguXKWXXbrLSLpeV6CovKl2nTv
- bbPdA43diTtVY8rq1Oyp9tg1Sc73FWJ/MWJ9QQ9dXkGH645N08OL1rNgt9rKsoOHTorbK6bLbp
- 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
- 1uPLuc3Y5dmxr42Vy7dW54Buer53tcPz71HZzfoH7R+MXGPIe5++eT8m/RvzHoo2s8+clo1+kf
- PuIvcnWu6rj0PVV3DN43u3M6Xkih9u8m34fNtN0nlXpPO5bnO4596nw1HKq4XZ8xu9dxjS4ev6
- e6V5c9HeD+oco0+gyOim4jRbpHf17ljyexFq7W2rtiRPTvesD+PO8dPscFnBqvpHIMHUz3CvU1
- tsTw/s/UOo24fF+l7oKr/O9H6lbsX5d21bafbfz83EtwlzVyxbZ9aJzLOfVb1sxaX89oul1+/k
- YiSmBs5e7tcNLx9XZysrq8fTm2Uqby+1UuyZIeJPW5ToKJoNLl1YFvqOertx9J3SFW3IMp3flf
- Q53PYuhHa4ONjbKLblyLOzj2Z8svTSZMuNc1XflT1TwmXk2r62QbCfMy9CpnWMyjTEsrHQ4Onk
- bDYSsuyhvbzQ8zp1G3d2HG6sjUOX/ADLsnLrksOh1GHnROd8h7p5r9DzM/ArIvrfIT2YpXVSjr
- TZLUVUit5z1bMVp9nQOZtewscMrLu7RsPNVlzel6m6R4v61wuv6p5hnuW5rBlU1HpOVLvM4V1P
- rjZc87d4b0nIOGeiKa1vLFX2vmPp+NQx9rnNeWgg2iduJuVEllY2X6RWpZwzO+icT2PPcdj7mo
- 7Hnsoq7j6cMCclYYR32YrTc07ErjmtPVHW6tkQbyg0VExl0ZAFiAGcqSp1Mj8+umJe80FQg1LK
- tqUsiK1KilGwA1SG1k1CQ4ZBOLWCh9c5bXLGRb4+rnG9NXiU6LuBdTVNW6HpgzVmHkWVe9Tqsp
- EKZm1uzIxV2WaaSVHsXat5XkyIYD6NynFWm7m01nm0TZaHaL7G7pbujT0Pv/FfavE2tdaey3sv
- ns3MS6vFu57z71y/h6Pma/wDRpa8fEK3tArs53eaOotqtr/j+56HN0HO9xyrNrr8Jj8B5L2/XL
- HjEYv1nJ81Z01buNkZ11Wnzb9ZYkPG6qk25MjA1FR0cFWqdPvywLNKqbbaZRu12oqbeK6g30us
- dK1tVXQbGPbTAbtSeqiVevq9Au+aS+irdkyJiGdRDvzZdvSQXqoW71RSiTpThyY0bZSoFy+Dnr
- GxuVem8Ke8/BPQ4fD/ROep+x5qsrtG5bkZzsuHE+ivof4udX817H7o+asTy35n9o7/fea7Cw66
- z4NUdXld8xfHc1u5fVMpzaq6/Au6NiN0+DJYfmmUcTUNE5CynONTu/XXl70f85+wepehckvfm3
- v8AqFRi9tox6a2jo38rcLzM/pcrav8AL6zRm6tz+vqcXRjV20w3N62kxeWtrlhaqRRU3aW15RM
- I7RA4fbPT807K3tfr3yegkaeVm3Y+r6Fh9GN6A1a35aiyZjvXubvl+r5/Wh4jqrsnC3OuUm7l0
- Nyc6vRrOjcO6hwfTams0l/570fJYurynY5MvYQZGTXpcTtLHndDA1kmF1eZA5x0rCdXi1Ma1r+
- hzIc+Lpg6VXtly+zzhzYVujJT19/SWJVWLbtlVWLF+K3oUann9SHZXDOHoSdbiZWe/vma3tl5X
- 0eG18/a5bMtpbLKVrHr6eh1Cn57Y8i9Pwdo/wAxg9Tk9AHN2NGedUOR+hzDmQpMiXWHw0NS1vW
- J9dLpukuMv0XnKjLWyatCatEqwpnEfc1VNOybXkxZEebqqnbc/f1LoHnLsfk+/YWfMWlfoXPNz
- ZUtltbT36HzXUentH2OZ4N6x2PSsfJuR9p8P3ZuPMdKxHX52Zpuz71R4vyfufiXU4/m6N1nFeh
- 8rlUXUPby4jwJ1kORlgyg28rGZkHJD5tXXosE2U17c8XVQ33TEadlEGbdmuV2wlTUwwzfNkgNy
- itogInRHpMpD0aC5MmCVbtlMS2ut5Ntm2OpVAz7n62S1dmZjyzemNGnERFRMj2VoS+xED8JJl9
- IzISzUS8lMrt0jdSdOm3OtkA2k6slV6be0z9ln2aK0or3Du1+notTyet1Ts/lHrnPt9ZXHBk9f
- zfoDJZmjvo7g9VWfovMNs2WGqtus9wC84Ppu/Rm973PO8b64eekx/A/UHOvMet8aZHp/OB1o78
- F7bjsyZjlXVw2rabhFXGKSEV8fRn0UCK8RFkxk30TlQn1LCXEWUk5JU4Q+6um6NHsYxFI3aMac
- scKaMmSoR02S0IkK7qTOjRSwLqk0ZaiLaRNGSI87IhQuQ6j1EW1hWVRn3ZpRvOb/kd2b59Va7L
- 1nzdu5aiK9lWyqZ8LLCo9mNPXOQdux9T2Jndzkfnn3PK01ll9/Kr6vR2dicygb+Rbm5avoUG2n
- Gjo8jNr5i52baYOn5jP1DcZtXlTSek4WPpc+9CZ7s/kfaOOyOGc3b3Pofz41voPM+4qjy/tcO7
- 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
- Ov1WxzmqrvhuyRFQYShUG3VJrQFslKZVXZJNs0sDbiFsZaU1ZUptwBiiymIWUvtvWbpS69CJD8
- yh48mbKour3JKEJKbdhJMh1S3bw5VN/T9XzLq3m+plqfb44GgzfTOf9nAwl5O3M0T8+mzO3UzT
- 0XDRc0brmvoKdnTRPiRVbs7BuGyRU2LVtMJx6TZVBi6FFleNY1idGfKKvVac3Bflj738E+5+Y2
- d/mtP1uLnLClu3q9h+7vnn9H/E/RYcsneP3jS4qyuMVil0gFLZZWG3mCpLhef7svziy9jO+j/C
- fpD6N+T31r8T9epG3nOR6Jckn69HIPkp9hPjZ6754lhxz1PhU9MwHVa7s3Et4KmIyCfPookqKt
- lacqKYcVwo31zw24yvzb7bN9CcA0vmfUetOU9E5Pyde0b4dd9LHoML0vMas2Yj2ETtcZso0a6l
- 66h3ObSe0z/QuT1Nk1mazk9DpEniwM61U86rqNPR6rNv0X17+fzG3JNi+UvJ/qvI/X/xz4zx3a
- 4HuXvXzuo6Nvc9P4f616rxHuJ7Sp0cylK+QRURtFDk8f/Tv5k/V5s1t6O4P6W4vrdL8pPrV8fb
- 8Xjye2/3fI8J9G8C9GX8rN3ed1OfpdW59uMbRu1bVb0Hn+nzHEvQ3ALcnkfS4vZ9ryXf+E+l+K
- ZOjxuzupW7gZi/ryZZbFQRpmuZdxlvfcHz8+ivI9b2q3i2fk/rGVFy2rwouiiRso9c0cTlnHez
- 8c6nnfItlAl+v+Pjvvnvv2Pt+Z6OUndyO2nLPl+s9eXNdY+c9zwc6p/q+draW6rL8mau6DYvm6
- 99dfl/9SPKe7xVVqKnn76duxtatOMv9bkM9+NnZ1q+uzEHV1W4+PYsaM1YmehpVosUQVKLdiGu
- KcJK5NsUlcuc5JXKtHVNL4y92cM2Yei6x/n1F/QpfiPj/AFON6k+cVXTek8X7vMK4nsCDpxWnj
- clTppfW0SIzoINxQZTb6FuN0lqXQsxY+onq7FvtSEtefYkVaAsnK7VmoBgozR1qJyOlZvqUOKe
- ptjqeWtjBSyJYU+FLKnFiNOOPgx1ynkaNIkOpZFkuCpyQ+lbGTJUKUvAs0p1NUSFAEOMrV5TRl
- Te44wK7DQ4iFaFSUshqmGVhCxkFayTMertiuOOIzS4zCtNbbSjGuCqGzXWEGtBWvrLre8z0GHX
- 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
- kN5UXZ9pzTtPnZTcc7fy3d8ouqxeM7fku2NcXXbk7Kjjhw9Yk8iD09Oe5QiHpK+ZBk6Ijngk3s
- THBl63awLrl+kwGT6WNvJ9KXmMs/P8At9ezSLz7U8W6fy3byPZEull8D2cyhn5G/LPdoZF2flX
- EOr8c7fjsO/Ff2cm7ZgSs2wR36+yoLmwbKiYltNXHW/FEUlpTK4slSKSJAZlbcQGfGYBVyVCcM
- 32Un0r0yyjrFrztSJLZ6OkWZlThbOMaXo0awkItaN9cq8g12QEMtvXfFWPJbaMLqpLahuaApYz
- 48qEnVpr2VMsPvlOutK5bFhQIiRVtvnbeSLc7Rh2QKSEujoF1dho5DcgWxkLQtiauxrnW002f0
- OXotOtNqXoU2OV7VJUjLvNme0GJmwTBLsaqQlr0qLoqtCknAS60uaqXVe1PVqEtx2suK2rRAec
- nhs6e7JLMvC6a4Gw+0rqNLN5K5JHZO0K5TLW3eMcgiW5vR1BxZ+L0c+P6O7Nqbel29WnP9MxlL
- Rp3kXI10fXU0S2elNtD0Ye6j09BVd0fOc9orKez57kGvYb6Di5BTa2PMjK+lLHyZZZtndIODdJ
- 6dAoaBLNlQ5fn+jHui5HCto6NzXnvNehwn+QbnI9vydQxPauzMqcEdhqU7EjCRLIrkXqktq+rY
- vS1Xd9l8inYOzsWMwqNqKaukMkqVXGDeuUkpX0bmdl16tNGp5tdl+5m5SvskZJ+u7W5t+DDb1q
- HSjyaNx0frZDDpEZkpsprUzIb0RYVpWskWJoIJSqlonMlBZJWQ24/Dgfqz1kGWfnwWpacn3y6O
- XZ3t0W3N57rfQeSuycme2NRZTS+mvNv0Bx9LyR3znfp7ken6hZM9Z43qcdreJ5WJ60frmcPV8x
- eRvoHWdvy3n3b8+l+c9r03q+t8y9fha3q/in20h+WHVLHrHZ8v5Io9jP38nRx8dR+e9t7g8Nek
- eDX4OE9d5x3P0fheY9tvOE8/t7vznv8p1OJ0uZCl4OpQpnO3Z4Giz1wr712jkc/tcxgahXV89l
- XmQ9Gl23Kxm3+gtzw/qPnva+yvI/oDh/G9Fzvn+wwPrfn2JxWtzvofCxk6mHdjpztSIqitIhWM
- i0eDUSb6hBIyEIUlUnabOlsOH7LX6rzHA0YfZE/K2nG9ZeipfrvreV77E7eV6elVtlxPWVWW2O
- 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
- /dZ1dT7rHCf3GWl4GR+DHGVtmZX4FpPkdXN8mdVU+6zqqv3H+Q4VVykWlpoy8rSVtz5299Tvpc
- 9BSn3t9RSne92KU7R30M07Na7jnO7eupnqK3eZml4sdWVtzrJeJ1ktO8zrpWtcdap3ttfI66T8
- NvA05jkXWo73VjMhc8NzulxtJCatuXMyaM0bCaO7zM6LllcUlc+0MXqZhti+BmSxuZS5fC7RfW
- 9ic8y25YSSYoLmRqKwq2uxCvBRS1PlEUvtHXw8yrbNdC5F98Psl9C6sxSuh7YMp+/H1Jp8jXmd
- JTnGpCzaI+6iq/m5eg9sOZY54yaFl88ELbCWwr+A72we6IW7nlBiwW1xjI7ovqLmN95Fx3etzU
- bdh7sghLzLLmb3NPDBt3LPLuJ9wzNjcrlO92N6si3lZd5fibRLsu8G9SPuiW7PlK8GS4lckQ4p
- W7258rps+VpWtIlUi3uUkp6nVwOqg0RorxFRjzYqVLwJSow30IdXJXR3L7GWLLR8C2u4oJa5v5
- EbamgmXWpCeaK5HEQqO7UtraEp1tblnhYtRcH71xTp21R1uT3Vy5kpZ9XZHM0GWIq5l7jFBPmT
- puLawRbKJodOOVWevgTSWyXwNfAjS2vNHUtqTutBxGKMvBnVVLd55fUyz8GakU2dVPxIwdnqOn
- VX2X+RdibuZKkuQs6fusSfNjz3MzWrQpZ5qK5nyea1zJ28xcLUavmRUvTdr3OsbZ1h1rFUl90z
- u/uk68pM6wUi+ixzXuci+mDwbfgcO7QTsfKKuupSqyl73idJKUqlOyI3sTrTUpalaUtBmuhdn2
- 9hvBuxezFv8DkcjUmXJPQXvDa8CFTy+zYyjj4Cj5Dhdji0QTlbTU6ibb0tZCpTUfde51FRu+U6
- qe+Us2cyeiN2JIVzXRi2NFf8AQzO9i2ojkXL2LjL6DkX0Fa5m1we4tjWx1dT7p1cuYqTa0Z1Ej
- 5PLxR8nl4opwlDmKXmZ0ZytPlewp20zM7viKaX2hzV731KNa+hmLjsd1Iq8RkdlZi4x31KnGNq
- yXMp8XKEdhcdL7pLi5yum9GOu3Gw5JkppmcUvMfrgm8GciHvDWhFWsVm3LGSskSpVO47b7FaHE
- KbVm9CEq19Uy7uZkuYpbkrZErIyWSZGOq11J2zWbvYVSmoe+RVCa1v6nzUfdJOQnK6KcuT0Jwi
- 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
- hkbbw2YdP4pDUzT1MIF4fwfxeDjvk/I4//Z
PHOTO;VALUE=URI:
PHOTO;VALUE=URI:
CATEGORIES:People with Pictures,Family
diff --git a/apps/dav/tests/misc/sharing.xml b/apps/dav/tests/misc/sharing.xml
deleted file mode 100644
index 8771256ce79..00000000000
--- a/apps/dav/tests/misc/sharing.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="utf-8" ?>
- <CS:share xmlns:D="DAV:" xmlns:CS="urn:ietf:params:xml:ns:carddav">
- <CS:set>
- <D:href>principal:principals/admin</D:href>
- <CS:read-write />
- </CS:set>
- </CS:share>
diff --git a/apps/dav/tests/travis/caldav/install.sh b/apps/dav/tests/testsuits/caldav/install.sh
index d6064fb7a38..ecbab3a1fd7 100644
--- a/apps/dav/tests/travis/caldav/install.sh
+++ b/apps/dav/tests/testsuits/caldav/install.sh
@@ -1,4 +1,9 @@
#!/usr/bin/env bash
+#
+# SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+# SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
+#
SCRIPT=`realpath $0`
SCRIPTPATH=`dirname $SCRIPT`
diff --git a/apps/dav/tests/travis/caldav/script-new-endpoint.sh b/apps/dav/tests/testsuits/caldav/script-new-endpoint.sh
index c9bb7307c7c..91a78e97e35 100644
--- a/apps/dav/tests/travis/caldav/script-new-endpoint.sh
+++ b/apps/dav/tests/testsuits/caldav/script-new-endpoint.sh
@@ -1,4 +1,9 @@
#!/usr/bin/env bash
+#
+# SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+# SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
+#
SCRIPT=`realpath $0`
SCRIPTPATH=`dirname $SCRIPT`
diff --git a/apps/dav/tests/travis/caldav/script-old-endpoint.sh b/apps/dav/tests/testsuits/caldav/script-old-endpoint.sh
index 6e08f27310b..cbe2d0d85fa 100644
--- a/apps/dav/tests/travis/caldav/script-old-endpoint.sh
+++ b/apps/dav/tests/testsuits/caldav/script-old-endpoint.sh
@@ -1,4 +1,9 @@
#!/usr/bin/env bash
+#
+# SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+# SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
+#
SCRIPT=`realpath $0`
SCRIPTPATH=`dirname $SCRIPT`
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/current-user-principal/1.xml b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/current-user-principal/1.xml
index 77a67c110df..b84d2cedf68 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/current-user-principal/1.xml
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/current-user-principal/1.xml
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ - SPDX-FileCopyrightText: 2013 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+-->
<D:propfind xmlns:D="DAV:">
<D:prop>
<D:current-user-principal/>
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/put/1.txt b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/1.txt
index 2d0a3641ac4..2d0a3641ac4 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/put/1.txt
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/1.txt
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/put/1.xml b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/1.xml
index 676679bdd85..3ac43b1d507 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/put/1.xml
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/1.xml
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ - SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+-->
<D:mkcol xmlns:D="DAV:"
xmlns:E="urn:ietf:params:xml:ns:caldav">
<D:set>
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/put/2.txt b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/2.txt
index 67de0524ec3..67de0524ec3 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/put/2.txt
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/2.txt
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/put/3.txt b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/3.txt
index bfdc0dbd116..bfdc0dbd116 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/put/3.txt
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/3.txt
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/put/4.txt b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/4.txt
index be223854641..be223854641 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/put/4.txt
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/4.txt
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/put/5.txt b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/5.txt
index e1d701e2698..e1d701e2698 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/put/5.txt
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/5.txt
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/put/6.txt b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/6.txt
index ecabe13a707..ecabe13a707 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/put/6.txt
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/put/6.txt
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/1.xml b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/1.xml
index 4c54b88fd0c..343f73a6f84 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/1.xml
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/1.xml
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ - SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+-->
<D:propfind xmlns:D="DAV:">
<D:prop>
<D:supported-report-set/>
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/10.xml b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/10.xml
index b20b6d645a5..494f75ff986 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/10.xml
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/10.xml
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ - SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+-->
<D:sync-collection xmlns:D="DAV:">
<D:sync-token/>
<D:sync-level>bogus</D:sync-level>
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/11.xml b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/11.xml
index c7706328d5a..dffb0310eba 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/11.xml
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/11.xml
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ - SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+-->
<D:sync-collection xmlns:D="DAV:">
<D:sync-token>null</D:sync-token>
<D:prop>
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/vreports/sync/2.xml b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/2.xml
index 99ee3dbb0e0..fc8270bb8d8 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/vreports/sync/2.xml
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/2.xml
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ - SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+-->
<D:sync-collection xmlns:D="DAV:">
<D:sync-token/>
<D:prop/>
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/21.xml b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/21.xml
index 7a851c80c9a..cfbb3ff6c5e 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/21.xml
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/21.xml
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ - SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+-->
<D:sync-collection xmlns:D="DAV:">
<D:sync-token/>
<D:sync-level>0</D:sync-level>
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/3.xml b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/3.xml
index 9cb886b8133..2ee8f3e0f3c 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/3.xml
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/3.xml
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ - SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+-->
<D:sync-collection xmlns:D="DAV:">
<D:sync-token>$synctoken1:</D:sync-token>
<D:prop/>
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/4.xml b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/4.xml
index e0d0baf4a7f..1b8f73c98b0 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/4.xml
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/4.xml
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ - SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+-->
<D:sync-collection xmlns:D="DAV:">
<D:sync-token>$synctoken2:</D:sync-token>
<D:prop/>
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/5.xml b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/5.xml
index 4469bb434fc..769ebb6907e 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/5.xml
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/5.xml
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ - SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+-->
<D:sync-collection xmlns:D="DAV:">
<D:sync-token/>
<D:prop>
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/6.xml b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/6.xml
index 05b7198eb6e..78c175c3f03 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/6.xml
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/6.xml
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ - SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+-->
<D:sync-collection xmlns:D="DAV:">
<D:sync-token>$synctoken1:</D:sync-token>
<D:prop>
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/7.xml b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/7.xml
index 575b2f673d7..b7aa5926c87 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/7.xml
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/7.xml
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ - SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+-->
<D:sync-collection xmlns:D="DAV:">
<D:sync-token>$synctoken2:</D:sync-token>
<D:prop>
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/8.xml b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/8.xml
index 6badc4143cf..d61c413e2a4 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/8.xml
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/reports/sync/8.xml
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ - SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+-->
<D:sync-collection xmlns:D="DAV:">
<D:sync-token/>
<D:sync-level>1</D:sync-level>
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/1.xml b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/1.xml
index 3bcf9dc47f9..3be574add20 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/1.xml
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/1.xml
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ - SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+-->
<CS:share xmlns:D="DAV:" xmlns:CS="http://owncloud.org/ns">
<CS:set>
<D:href>principal:principals/users/user02</D:href>
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/4.xml b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/4.xml
index fd0f248bb31..5c679977eb9 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/4.xml
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/4.xml
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ - SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+-->
<D:propfind xmlns:D="DAV:">
<D:prop>
<D:resourcetype/>
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/5.ics b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/5.ics
index ae21adac8b2..ae21adac8b2 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/5.ics
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/5.ics
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/5.xml b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/5.xml
index 4862ed195f8..e13f1dd3cb1 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/5.xml
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/5.xml
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ - SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+-->
<D:propfind xmlns:D="DAV:" xmlns:CS="http://owncloud.org/ns">
<D:prop>
<CS:invite/>
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/6.ics b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/6.ics
index 145f5f14c7b..145f5f14c7b 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/6.ics
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/6.ics
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/7.ics b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/7.ics
index c4e816210df..c4e816210df 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/7.ics
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/7.ics
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/8.ics b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/8.ics
index 2da72d2f601..2da72d2f601 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/8.ics
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/8.ics
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/9.ics b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/9.ics
index dfc21bb9c5b..dfc21bb9c5b 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/9.ics
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CalDAV/sharing/calendars/read-write/9.ics
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/sharing/read-write/1.xml b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/1.xml
index 20d2ebf4cfc..482682b12ec 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/sharing/read-write/1.xml
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/1.xml
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ - SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+-->
<CS:share xmlns:D="DAV:" xmlns:CS="http://owncloud.org/ns">
<CS:set>
<D:href>principal:principals/users/user02</D:href>
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/sharing/read-write/4.xml b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/4.xml
index fd0f248bb31..5c679977eb9 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/sharing/read-write/4.xml
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/4.xml
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ - SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+-->
<D:propfind xmlns:D="DAV:">
<D:prop>
<D:resourcetype/>
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/sharing/read-write/6.vcf b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/6.vcf
index 6b53f8ba3bf..6b53f8ba3bf 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/sharing/read-write/6.vcf
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/6.vcf
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/sharing/read-write/7.vcf b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/7.vcf
index 27fdb9fae5f..27fdb9fae5f 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/sharing/read-write/7.vcf
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/7.vcf
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/sharing/read-write/8.vcf b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/8.vcf
index 9188fdd913c..9188fdd913c 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/sharing/read-write/8.vcf
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/8.vcf
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/sharing/read-write/9.vcf b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/9.vcf
index 1ca0a36ca4c..1ca0a36ca4c 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/sharing/read-write/9.vcf
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/sharing/read-write/9.vcf
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/vcurrent-user-principal/1.xml b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vcurrent-user-principal/1.xml
index dffedc6880d..d101bc698b5 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/vcurrent-user-principal/1.xml
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vcurrent-user-principal/1.xml
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ - SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+-->
<D:propfind xmlns:D="DAV:">
<D:prop>
<D:current-user-principal/>
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/vreports/put/1.vcf b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vreports/put/1.vcf
index 2121c65f1f4..2121c65f1f4 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/vreports/put/1.vcf
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vreports/put/1.vcf
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/vreports/put/2.vcf b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vreports/put/2.vcf
index 390a3d8ae69..390a3d8ae69 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/vreports/put/2.vcf
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vreports/put/2.vcf
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/vreports/put/3.vcf b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vreports/put/3.vcf
index 37c3b81bdcf..37c3b81bdcf 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/vreports/put/3.vcf
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vreports/put/3.vcf
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/vreports/sync/1.xml b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vreports/sync/1.xml
index 7f454b38900..8818cef1e29 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CardDAV/vreports/sync/1.xml
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vreports/sync/1.xml
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ - SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+-->
<D:propfind xmlns:D="DAV:">
<D:prop>
<D:supported-report-set/>
diff --git a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/2.xml b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vreports/sync/2.xml
index 99ee3dbb0e0..fc8270bb8d8 100644
--- a/apps/dav/tests/travis/caldavtest/data/Resource/CalDAV/reports/sync/2.xml
+++ b/apps/dav/tests/testsuits/caldavtest/data/Resource/CardDAV/vreports/sync/2.xml
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ - SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+-->
<D:sync-collection xmlns:D="DAV:">
<D:sync-token/>
<D:prop/>
diff --git a/apps/dav/tests/travis/caldavtest/serverinfo-new-endpoint.xml b/apps/dav/tests/testsuits/caldavtest/serverinfo-new-endpoint.xml
index c3ba99ee03d..524f2ef609d 100644
--- a/apps/dav/tests/travis/caldavtest/serverinfo-new-endpoint.xml
+++ b/apps/dav/tests/testsuits/caldavtest/serverinfo-new-endpoint.xml
@@ -1,23 +1,11 @@
<?xml version="1.0" standalone="no"?>
-<!DOCTYPE serverinfo SYSTEM
- "/home/deepdiver/Development/ownCloud/master/apps/dav/tests/travis/caldavtest/serverinfo.dtd">
+<!DOCTYPE serverinfo SYSTEM "./serverinfo.dtd">
<!--
- Copyright (c) 2006-2015 Apple Inc. All rights reserved.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
+ - SPDX-FileCopyrightText: 2006-2015 Apple Inc. All rights reserved.
+ - SPDX-License-Identifier: Apache-2.0
+-->
<serverinfo>
<host>localhost</host>
@@ -183,7 +171,7 @@
<value>$root:principals/</value>
</substitution>
- <!-- the core recored type collections-->
+ <!-- the core record type collections-->
<substitution>
<key>$uidstype:</key>
<value>__uids__</value>
diff --git a/apps/dav/tests/travis/caldavtest/serverinfo-old-caldav-endpoint.xml b/apps/dav/tests/testsuits/caldavtest/serverinfo-old-caldav-endpoint.xml
index 8ac822d8e4e..c95efd2ca0d 100644
--- a/apps/dav/tests/travis/caldavtest/serverinfo-old-caldav-endpoint.xml
+++ b/apps/dav/tests/testsuits/caldavtest/serverinfo-old-caldav-endpoint.xml
@@ -1,23 +1,11 @@
<?xml version="1.0" standalone="no"?>
-<!DOCTYPE serverinfo SYSTEM
- "/home/deepdiver/Development/ownCloud/master/apps/dav/tests/travis/caldavtest/serverinfo.dtd">
+<!DOCTYPE serverinfo SYSTEM "./serverinfo.dtd">
<!--
- Copyright (c) 2006-2015 Apple Inc. All rights reserved.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
+ - SPDX-FileCopyrightText: 2006-2015 Apple Inc. All rights reserved.
+ - SPDX-License-Identifier: Apache-2.0
+-->
<serverinfo>
<host>localhost</host>
@@ -183,7 +171,7 @@
<value>$root:principals/</value>
</substitution>
- <!-- the core recored type collections-->
+ <!-- the core record type collections-->
<substitution>
<key>$uidstype:</key>
<value>__uids__</value>
diff --git a/apps/dav/tests/travis/caldavtest/serverinfo-old-carddav-endpoint.xml b/apps/dav/tests/testsuits/caldavtest/serverinfo-old-carddav-endpoint.xml
index bd9f83a4814..960cc8cdf17 100644
--- a/apps/dav/tests/travis/caldavtest/serverinfo-old-carddav-endpoint.xml
+++ b/apps/dav/tests/testsuits/caldavtest/serverinfo-old-carddav-endpoint.xml
@@ -1,23 +1,11 @@
<?xml version="1.0" standalone="no"?>
-<!DOCTYPE serverinfo SYSTEM
- "/home/deepdiver/Development/ownCloud/master/apps/dav/tests/travis/caldavtest/serverinfo.dtd">
+<!DOCTYPE serverinfo SYSTEM "./serverinfo.dtd">
<!--
- Copyright (c) 2006-2015 Apple Inc. All rights reserved.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
+ - SPDX-FileCopyrightText: 2006-2015 Apple Inc. All rights reserved.
+ - SPDX-License-Identifier: Apache-2.0
+-->
<serverinfo>
<host>localhost</host>
@@ -183,7 +171,7 @@
<value>$root:principals/</value>
</substitution>
- <!-- the core recored type collections-->
+ <!-- the core record type collections-->
<substitution>
<key>$uidstype:</key>
<value>__uids__</value>
diff --git a/apps/dav/tests/travis/caldavtest/serverinfo.dtd b/apps/dav/tests/testsuits/caldavtest/serverinfo.dtd
index d642f4f90cd..d3841e363ad 100644
--- a/apps/dav/tests/travis/caldavtest/serverinfo.dtd
+++ b/apps/dav/tests/testsuits/caldavtest/serverinfo.dtd
@@ -1,18 +1,7 @@
<!--
- Copyright (c) 2006-2015 Apple Inc. All rights reserved.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
+ - SPDX-FileCopyrightText: 2006-2015 Apple Inc. All rights reserved.
+ - SPDX-License-Identifier: Apache-2.0
+-->
<!ELEMENT serverinfo (host, path, nonsslport, sslport, unix?,
host2?, nonsslport2?, sslport2?, unix2?,
diff --git a/apps/dav/tests/travis/caldavtest/tests/CalDAV/current-user-principal.xml b/apps/dav/tests/testsuits/caldavtest/tests/CalDAV/current-user-principal.xml
index d01058fee0a..79ccd50e01f 100644
--- a/apps/dav/tests/travis/caldavtest/tests/CalDAV/current-user-principal.xml
+++ b/apps/dav/tests/testsuits/caldavtest/tests/CalDAV/current-user-principal.xml
@@ -3,20 +3,9 @@
<!DOCTYPE caldavtest SYSTEM "caldavtest.dtd">
<!--
- Copyright (c) 2006-2015 Apple Inc. All rights reserved.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
+ - SPDX-FileCopyrightText: 2006-2015 Apple Inc. All rights reserved.
+ - SPDX-License-Identifier: Apache-2.0
+-->
<caldavtest>
<description>Test DAV:current-user-principal support</description>
diff --git a/apps/dav/tests/travis/caldavtest/tests/CalDAV/sharing-calendars.xml b/apps/dav/tests/testsuits/caldavtest/tests/CalDAV/sharing-calendars.xml
index 6c15a793226..97541025736 100644
--- a/apps/dav/tests/travis/caldavtest/tests/CalDAV/sharing-calendars.xml
+++ b/apps/dav/tests/testsuits/caldavtest/tests/CalDAV/sharing-calendars.xml
@@ -3,20 +3,9 @@
<!DOCTYPE caldavtest SYSTEM "caldavtest.dtd">
<!--
- Copyright (c) 2006-2015 Apple Inc. All rights reserved.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
+ - SPDX-FileCopyrightText: 2006-2015 Apple Inc. All rights reserved.
+ - SPDX-License-Identifier: Apache-2.0
+-->
<caldavtest>
<description>Test calendar sharing calendars</description>
diff --git a/apps/dav/tests/travis/caldavtest/tests/CalDAV/sync-report.xml b/apps/dav/tests/testsuits/caldavtest/tests/CalDAV/sync-report.xml
index cf4fcde251f..fda5a5f3fe6 100644
--- a/apps/dav/tests/travis/caldavtest/tests/CalDAV/sync-report.xml
+++ b/apps/dav/tests/testsuits/caldavtest/tests/CalDAV/sync-report.xml
@@ -3,20 +3,9 @@
<!DOCTYPE caldavtest SYSTEM "caldavtest.dtd">
<!--
- Copyright (c) 2006-2015 Apple Inc. All rights reserved.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
+ - SPDX-FileCopyrightText: 2006-2015 Apple Inc. All rights reserved.
+ - SPDX-License-Identifier: Apache-2.0
+-->
<caldavtest>
<require-feature>
diff --git a/apps/dav/tests/travis/caldavtest/tests/CardDAV/current-user-principal.xml b/apps/dav/tests/testsuits/caldavtest/tests/CardDAV/current-user-principal.xml
index dd206bbcfb8..fc120a092dd 100644
--- a/apps/dav/tests/travis/caldavtest/tests/CardDAV/current-user-principal.xml
+++ b/apps/dav/tests/testsuits/caldavtest/tests/CardDAV/current-user-principal.xml
@@ -3,20 +3,9 @@
<!DOCTYPE caldavtest SYSTEM "caldavtest.dtd">
<!--
- Copyright (c) 2006-2015 Apple Inc. All rights reserved.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
+ - SPDX-FileCopyrightText: 2006-2015 Apple Inc. All rights reserved.
+ - SPDX-License-Identifier: Apache-2.0
+-->
<caldavtest>
<description>Test DAV:current-user-principal support</description>
diff --git a/apps/dav/tests/travis/caldavtest/tests/CardDAV/sharing-addressbooks.xml b/apps/dav/tests/testsuits/caldavtest/tests/CardDAV/sharing-addressbooks.xml
index 99e40f5c68c..a9c1a84af44 100644
--- a/apps/dav/tests/travis/caldavtest/tests/CardDAV/sharing-addressbooks.xml
+++ b/apps/dav/tests/testsuits/caldavtest/tests/CardDAV/sharing-addressbooks.xml
@@ -2,6 +2,11 @@
<!DOCTYPE caldavtest SYSTEM "caldavtest.dtd">
+<!--
+ - SPDX-FileCopyrightText: 2006-2015 Apple Inc. All rights reserved.
+ - SPDX-License-Identifier: Apache-2.0
+-->
+
<caldavtest>
<description>Test addressbook sharing</description>
diff --git a/apps/dav/tests/travis/caldavtest/tests/CardDAV/sync-report.xml b/apps/dav/tests/testsuits/caldavtest/tests/CardDAV/sync-report.xml
index ffa6662981c..bdd3c58e93b 100644
--- a/apps/dav/tests/travis/caldavtest/tests/CardDAV/sync-report.xml
+++ b/apps/dav/tests/testsuits/caldavtest/tests/CardDAV/sync-report.xml
@@ -3,20 +3,9 @@
<!DOCTYPE caldavtest SYSTEM "caldavtest.dtd">
<!--
- Copyright (c) 2006-2015 Apple Inc. All rights reserved.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
+ - SPDX-FileCopyrightText: 2006-2015 Apple Inc. All rights reserved.
+ - SPDX-License-Identifier: Apache-2.0
+-->
<caldavtest>
<require-feature>
diff --git a/apps/dav/tests/travis/carddav/install.sh b/apps/dav/tests/testsuits/carddav/install.sh
index db8b53bc861..91c9079948d 100644
--- a/apps/dav/tests/travis/carddav/install.sh
+++ b/apps/dav/tests/testsuits/carddav/install.sh
@@ -1,4 +1,8 @@
#!/usr/bin/env bash
+#
+# SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
+#
SCRIPT=`realpath $0`
SCRIPTPATH=`dirname $SCRIPT`
diff --git a/apps/dav/tests/travis/carddav/script-new-endpoint.sh b/apps/dav/tests/testsuits/carddav/script-new-endpoint.sh
index 9140c37b45f..f42876a0ade 100644
--- a/apps/dav/tests/travis/carddav/script-new-endpoint.sh
+++ b/apps/dav/tests/testsuits/carddav/script-new-endpoint.sh
@@ -1,4 +1,8 @@
#!/usr/bin/env bash
+#
+# SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
+#
SCRIPT=`realpath $0`
SCRIPTPATH=`dirname $SCRIPT`
diff --git a/apps/dav/tests/travis/carddav/script-old-endpoint.sh b/apps/dav/tests/testsuits/carddav/script-old-endpoint.sh
index 216f2f5af31..e5de79bfebb 100644
--- a/apps/dav/tests/travis/carddav/script-old-endpoint.sh
+++ b/apps/dav/tests/testsuits/carddav/script-old-endpoint.sh
@@ -1,4 +1,8 @@
#!/usr/bin/env bash
+#
+# SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
+#
SCRIPT=`realpath $0`
SCRIPTPATH=`dirname $SCRIPT`
diff --git a/apps/dav/tests/travis/carddav/script.sh b/apps/dav/tests/testsuits/carddav/script.sh
index ecdc0f95863..8261d3189bf 100644
--- a/apps/dav/tests/travis/carddav/script.sh
+++ b/apps/dav/tests/testsuits/carddav/script.sh
@@ -1,4 +1,8 @@
#!/usr/bin/env bash
+#
+# SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
+#
SCRIPT=`realpath $0`
SCRIPTPATH=`dirname $SCRIPT`
diff --git a/apps/dav/tests/travis/litmus-v1/script.sh b/apps/dav/tests/testsuits/litmus-v1/script.sh
index cba305683b2..d2da46904d4 100644
--- a/apps/dav/tests/travis/litmus-v1/script.sh
+++ b/apps/dav/tests/testsuits/litmus-v1/script.sh
@@ -1,4 +1,9 @@
#!/usr/bin/env bash
+#
+# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+# SPDX-FileCopyrightText: 2015 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
+#
SCRIPT=`realpath $0`
SCRIPTPATH=`dirname $SCRIPT`
@@ -10,4 +15,4 @@ sleep 30
# run the tests
cd /tmp/litmus/litmus-0.13
-make URL=http://127.0.0.1:8888/remote.php/webdav CREDS="admin admin" TESTS="basic copymove props locks" check
+make URL=http://127.0.0.1:8888/remote.php/webdav CREDS="admin admin" TESTS="basic copymove props largefile" check
diff --git a/apps/dav/tests/travis/litmus-v2/script.sh b/apps/dav/tests/testsuits/litmus-v2/script.sh
index 966ed5a2052..8b425228efb 100644
--- a/apps/dav/tests/travis/litmus-v2/script.sh
+++ b/apps/dav/tests/testsuits/litmus-v2/script.sh
@@ -1,4 +1,9 @@
#!/usr/bin/env bash
+#
+# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+# SPDX-FileCopyrightText: 2015 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
+#
SCRIPT=`realpath $0`
SCRIPTPATH=`dirname $SCRIPT`
@@ -10,4 +15,4 @@ sleep 30
# run the tests
cd /tmp/litmus/litmus-0.13
-make URL=http://127.0.0.1:8888/remote.php/dav/files/admin CREDS="admin admin" TESTS="basic copymove props locks" check
+make URL=http://127.0.0.1:8888/remote.php/dav/files/admin CREDS="admin admin" TESTS="basic copymove props largefile" check
diff --git a/apps/dav/tests/unit/AppInfo/ApplicationTest.php b/apps/dav/tests/unit/AppInfo/ApplicationTest.php
index 2e728f8e872..336f487e0b8 100644
--- a/apps/dav/tests/unit/AppInfo/ApplicationTest.php
+++ b/apps/dav/tests/unit/AppInfo/ApplicationTest.php
@@ -1,24 +1,10 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Tests\unit\AppInfo;
@@ -35,7 +21,7 @@ use Test\TestCase;
* @package OCA\DAV\Tests\Unit\AppInfo
*/
class ApplicationTest extends TestCase {
- public function test() {
+ public function test(): void {
$app = new Application();
$c = $app->getContainer();
diff --git a/apps/dav/tests/unit/AppInfo/PluginManagerTest.php b/apps/dav/tests/unit/AppInfo/PluginManagerTest.php
index 53e63269067..0082aa45286 100644
--- a/apps/dav/tests/unit/AppInfo/PluginManagerTest.php
+++ b/apps/dav/tests/unit/AppInfo/PluginManagerTest.php
@@ -1,34 +1,16 @@
<?php
+
+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 Joas Schilling <coding@schilljs.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: 2016 ownCloud GmbH.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Tests\unit\AppInfo;
use OC\App\AppManager;
use OC\ServerContainer;
use OCA\DAV\AppInfo\PluginManager;
+use OCA\DAV\CalDAV\AppCalendar\AppCalendarPlugin;
use OCA\DAV\CalDAV\Integration\ICalendarProvider;
use Sabre\DAV\Collection;
use Sabre\DAV\ServerPlugin;
@@ -40,12 +22,11 @@ use Test\TestCase;
* @package OCA\DAV\Tests\Unit\AppInfo
*/
class PluginManagerTest extends TestCase {
- public function test() {
+ public function test(): void {
$server = $this->createMock(ServerContainer::class);
-
$appManager = $this->createMock(AppManager::class);
- $appManager->method('getInstalledApps')
+ $appManager->method('getEnabledApps')
->willReturn(['adavapp', 'adavapp2']);
$appInfo1 = [
@@ -94,6 +75,7 @@ class PluginManagerTest extends TestCase {
$pluginManager = new PluginManager($server, $appManager);
+ $appCalendarPlugin = $this->createMock(AppCalendarPlugin::class);
$calendarPlugin1 = $this->createMock(ICalendarProvider::class);
$calendarPlugin2 = $this->createMock(ICalendarProvider::class);
$calendarPlugin3 = $this->createMock(ICalendarProvider::class);
@@ -106,17 +88,18 @@ class PluginManagerTest extends TestCase {
$dummyCollection2 = $this->createMock(Collection::class);
$dummy2Collection1 = $this->createMock(Collection::class);
- $server->method('query')
+ $server->method('get')
->willReturnMap([
- ['\OCA\DAV\ADavApp\PluginOne', true, $dummyPlugin1],
- ['\OCA\DAV\ADavApp\PluginTwo', true, $dummyPlugin2],
- ['\OCA\DAV\ADavApp\CalendarPluginOne', true, $calendarPlugin1],
- ['\OCA\DAV\ADavApp\CalendarPluginTwo', true, $calendarPlugin2],
- ['\OCA\DAV\ADavApp\CollectionOne', true, $dummyCollection1],
- ['\OCA\DAV\ADavApp\CollectionTwo', true, $dummyCollection2],
- ['\OCA\DAV\ADavApp2\PluginOne', true, $dummy2Plugin1],
- ['\OCA\DAV\ADavApp2\CalendarPluginOne', true, $calendarPlugin3],
- ['\OCA\DAV\ADavApp2\CollectionOne', true, $dummy2Collection1],
+ [AppCalendarPlugin::class, $appCalendarPlugin],
+ ['\OCA\DAV\ADavApp\PluginOne', $dummyPlugin1],
+ ['\OCA\DAV\ADavApp\PluginTwo', $dummyPlugin2],
+ ['\OCA\DAV\ADavApp\CalendarPluginOne', $calendarPlugin1],
+ ['\OCA\DAV\ADavApp\CalendarPluginTwo', $calendarPlugin2],
+ ['\OCA\DAV\ADavApp\CollectionOne', $dummyCollection1],
+ ['\OCA\DAV\ADavApp\CollectionTwo', $dummyCollection2],
+ ['\OCA\DAV\ADavApp2\PluginOne', $dummy2Plugin1],
+ ['\OCA\DAV\ADavApp2\CalendarPluginOne', $calendarPlugin3],
+ ['\OCA\DAV\ADavApp2\CollectionOne', $dummy2Collection1],
]);
$expectedPlugins = [
@@ -125,6 +108,7 @@ class PluginManagerTest extends TestCase {
$dummy2Plugin1,
];
$expectedCalendarPlugins = [
+ $appCalendarPlugin,
$calendarPlugin1,
$calendarPlugin2,
$calendarPlugin3,
diff --git a/apps/dav/tests/unit/Avatars/AvatarHomeTest.php b/apps/dav/tests/unit/Avatars/AvatarHomeTest.php
index 9b2d91954de..7117637a000 100644
--- a/apps/dav/tests/unit/Avatars/AvatarHomeTest.php
+++ b/apps/dav/tests/unit/Avatars/AvatarHomeTest.php
@@ -1,44 +1,25 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2017, ownCloud GmbH
- *
- * @author Georg Ehrke <oc.list@georgehrke.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: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2017 ownCloud GmbH
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-namespace OCA\DAV\Tests\Unit\Avatars;
+namespace OCA\DAV\Tests\unit\Avatars;
use OCA\DAV\Avatars\AvatarHome;
use OCA\DAV\Avatars\AvatarNode;
use OCP\IAvatar;
use OCP\IAvatarManager;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\Exception\MethodNotAllowed;
use Sabre\DAV\Exception\NotFound;
use Test\TestCase;
class AvatarHomeTest extends TestCase {
-
- /** @var AvatarHome */
- private $home;
-
- /** @var IAvatarManager | \PHPUnit\Framework\MockObject\MockObject */
- private $avatarManager;
+ private AvatarHome $home;
+ private IAvatarManager&MockObject $avatarManager;
protected function setUp(): void {
parent::setUp();
@@ -46,16 +27,14 @@ class AvatarHomeTest extends TestCase {
$this->home = new AvatarHome(['uri' => 'principals/users/admin'], $this->avatarManager);
}
- /**
- * @dataProvider providesForbiddenMethods
- */
- public function testForbiddenMethods($method) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesForbiddenMethods')]
+ public function testForbiddenMethods($method): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$this->home->$method('');
}
- public function providesForbiddenMethods() {
+ public static function providesForbiddenMethods(): array {
return [
['createFile'],
['createDirectory'],
@@ -64,12 +43,12 @@ class AvatarHomeTest extends TestCase {
];
}
- public function testGetName() {
+ public function testGetName(): void {
$n = $this->home->getName();
self::assertEquals('admin', $n);
}
- public function providesTestGetChild() {
+ public static function providesTestGetChild(): array {
return [
[MethodNotAllowed::class, false, ''],
[MethodNotAllowed::class, false, 'bla.foo'],
@@ -79,10 +58,8 @@ class AvatarHomeTest extends TestCase {
];
}
- /**
- * @dataProvider providesTestGetChild
- */
- public function testGetChild($expectedException, $hasAvatar, $path) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesTestGetChild')]
+ public function testGetChild(?string $expectedException, bool $hasAvatar, string $path): void {
if ($expectedException !== null) {
$this->expectException($expectedException);
}
@@ -95,7 +72,7 @@ class AvatarHomeTest extends TestCase {
$this->assertInstanceOf(AvatarNode::class, $avatarNode);
}
- public function testGetChildren() {
+ public function testGetChildren(): void {
$avatarNodes = $this->home->getChildren();
self::assertEquals(0, count($avatarNodes));
@@ -106,10 +83,8 @@ class AvatarHomeTest extends TestCase {
self::assertEquals(1, count($avatarNodes));
}
- /**
- * @dataProvider providesTestGetChild
- */
- public function testChildExists($expectedException, $hasAvatar, $path) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesTestGetChild')]
+ public function testChildExists(?string $expectedException, bool $hasAvatar, string $path): void {
$avatar = $this->createMock(IAvatar::class);
$avatar->method('exists')->willReturn($hasAvatar);
@@ -118,7 +93,7 @@ class AvatarHomeTest extends TestCase {
$this->assertEquals($hasAvatar, $childExists);
}
- public function testGetLastModified() {
+ public function testGetLastModified(): void {
self::assertNull($this->home->getLastModified());
}
}
diff --git a/apps/dav/tests/unit/Avatars/AvatarNodeTest.php b/apps/dav/tests/unit/Avatars/AvatarNodeTest.php
index 5a124d7f15b..0ca147a1f3b 100644
--- a/apps/dav/tests/unit/Avatars/AvatarNodeTest.php
+++ b/apps/dav/tests/unit/Avatars/AvatarNodeTest.php
@@ -1,41 +1,28 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2017, ownCloud GmbH
- *
- * @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: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2017 ownCloud GmbH
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-namespace OCA\DAV\Tests\Unit\Avatars;
+namespace OCA\DAV\Tests\unit\Avatars;
use OCA\DAV\Avatars\AvatarNode;
use OCP\IAvatar;
+use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class AvatarNodeTest extends TestCase {
- public function testGetName() {
- /** @var IAvatar | \PHPUnit\Framework\MockObject\MockObject $a */
+ public function testGetName(): void {
+ /** @var IAvatar&MockObject $a */
$a = $this->createMock(IAvatar::class);
$n = new AvatarNode(1024, 'png', $a);
$this->assertEquals('1024.png', $n->getName());
}
- public function testGetContentType() {
- /** @var IAvatar | \PHPUnit\Framework\MockObject\MockObject $a */
+ public function testGetContentType(): void {
+ /** @var IAvatar&MockObject $a */
$a = $this->createMock(IAvatar::class);
$n = new AvatarNode(1024, 'png', $a);
$this->assertEquals('image/png', $n->getContentType());
diff --git a/apps/dav/tests/unit/BackgroundJob/CleanupInvitationTokenJobTest.php b/apps/dav/tests/unit/BackgroundJob/CleanupInvitationTokenJobTest.php
index 6233980edbb..b2199e3e657 100644
--- a/apps/dav/tests/unit/BackgroundJob/CleanupInvitationTokenJobTest.php
+++ b/apps/dav/tests/unit/BackgroundJob/CleanupInvitationTokenJobTest.php
@@ -3,48 +3,23 @@
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 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: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Tests\unit\BackgroundJob;
use OCA\DAV\BackgroundJob\CleanupInvitationTokenJob;
use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\DB\QueryBuilder\IExpressionBuilder;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
+use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class CleanupInvitationTokenJobTest extends TestCase {
-
- /** @var IDBConnection | \PHPUnit\Framework\MockObject\MockObject */
- private $dbConnection;
-
- /** @var ITimeFactory | \PHPUnit\Framework\MockObject\MockObject */
- private $timeFactory;
-
- /** @var \OCA\DAV\BackgroundJob\GenerateBirthdayCalendarBackgroundJob */
- private $backgroundJob;
+ private IDBConnection&MockObject $dbConnection;
+ private ITimeFactory&MockObject $timeFactory;
+ private CleanupInvitationTokenJob $backgroundJob;
protected function setUp(): void {
parent::setUp();
@@ -56,14 +31,14 @@ class CleanupInvitationTokenJobTest extends TestCase {
$this->dbConnection, $this->timeFactory);
}
- public function testRun() {
+ public function testRun(): void {
$this->timeFactory->expects($this->once())
->method('getTime')
->with()
->willReturn(1337);
$queryBuilder = $this->createMock(IQueryBuilder::class);
- $expr = $this->createMock(\OCP\DB\QueryBuilder\IExpressionBuilder::class);
+ $expr = $this->createMock(IExpressionBuilder::class);
$stmt = $this->createMock(\Doctrine\DBAL\Driver\Statement::class);
$this->dbConnection->expects($this->once())
@@ -77,25 +52,26 @@ class CleanupInvitationTokenJobTest extends TestCase {
[1337, \PDO::PARAM_STR, null, 'namedParameter1337']
]);
+ $function = 'function1337';
$expr->expects($this->once())
->method('lt')
->with('expiration', 'namedParameter1337')
- ->willReturn('LT STATEMENT');
+ ->willReturn($function);
$this->dbConnection->expects($this->once())
->method('getQueryBuilder')
->with()
->willReturn($queryBuilder);
- $queryBuilder->expects($this->at(0))
+ $queryBuilder->expects($this->once())
->method('delete')
->with('calendar_invitations')
->willReturn($queryBuilder);
- $queryBuilder->expects($this->at(3))
+ $queryBuilder->expects($this->once())
->method('where')
- ->with('LT STATEMENT')
+ ->with($function)
->willReturn($queryBuilder);
- $queryBuilder->expects($this->at(4))
+ $queryBuilder->expects($this->once())
->method('execute')
->with()
->willReturn($stmt);
diff --git a/apps/dav/tests/unit/BackgroundJob/CleanupOrphanedChildrenJobTest.php b/apps/dav/tests/unit/BackgroundJob/CleanupOrphanedChildrenJobTest.php
new file mode 100644
index 00000000000..2065b8fe946
--- /dev/null
+++ b/apps/dav/tests/unit/BackgroundJob/CleanupOrphanedChildrenJobTest.php
@@ -0,0 +1,170 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\BackgroundJob;
+
+use OCA\DAV\BackgroundJob\CleanupOrphanedChildrenJob;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
+use OCP\DB\IResult;
+use OCP\DB\QueryBuilder\IExpressionBuilder;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class CleanupOrphanedChildrenJobTest extends TestCase {
+ private CleanupOrphanedChildrenJob $job;
+
+ private ITimeFactory&MockObject $timeFactory;
+ private IDBConnection&MockObject $connection;
+ private LoggerInterface&MockObject $logger;
+ private IJobList&MockObject $jobList;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->connection = $this->createMock(IDBConnection::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->jobList = $this->createMock(IJobList::class);
+
+ $this->job = new CleanupOrphanedChildrenJob(
+ $this->timeFactory,
+ $this->connection,
+ $this->logger,
+ $this->jobList,
+ );
+ }
+
+ private function getArgument(): array {
+ return [
+ 'childTable' => 'childTable',
+ 'parentTable' => 'parentTable',
+ 'parentId' => 'parentId',
+ 'logMessage' => 'logMessage',
+ ];
+ }
+
+ private function getMockQueryBuilder(): IQueryBuilder&MockObject {
+ $expr = $this->createMock(IExpressionBuilder::class);
+ $qb = $this->createMock(IQueryBuilder::class);
+ $qb->method('select')
+ ->willReturnSelf();
+ $qb->method('from')
+ ->willReturnSelf();
+ $qb->method('leftJoin')
+ ->willReturnSelf();
+ $qb->method('where')
+ ->willReturnSelf();
+ $qb->method('setMaxResults')
+ ->willReturnSelf();
+ $qb->method('andWhere')
+ ->willReturnSelf();
+ $qb->method('expr')
+ ->willReturn($expr);
+ $qb->method('delete')
+ ->willReturnSelf();
+ return $qb;
+ }
+
+ public function testRunWithoutOrphans(): void {
+ $argument = $this->getArgument();
+ $selectQb = $this->getMockQueryBuilder();
+ $result = $this->createMock(IResult::class);
+
+ $this->connection->expects(self::once())
+ ->method('getQueryBuilder')
+ ->willReturn($selectQb);
+ $selectQb->expects(self::once())
+ ->method('executeQuery')
+ ->willReturn($result);
+ $result->expects(self::once())
+ ->method('fetchAll')
+ ->willReturn([]);
+ $result->expects(self::once())
+ ->method('closeCursor');
+ $this->jobList->expects(self::never())
+ ->method('add');
+
+ self::invokePrivate($this->job, 'run', [$argument]);
+ }
+
+ public function testRunWithPartialBatch(): void {
+ $argument = $this->getArgument();
+ $selectQb = $this->getMockQueryBuilder();
+ $deleteQb = $this->getMockQueryBuilder();
+ $result = $this->createMock(IResult::class);
+
+ $calls = [
+ $selectQb,
+ $deleteQb,
+ ];
+ $this->connection->method('getQueryBuilder')
+ ->willReturnCallback(function () use (&$calls) {
+ return array_shift($calls);
+ });
+ $selectQb->expects(self::once())
+ ->method('executeQuery')
+ ->willReturn($result);
+ $result->expects(self::once())
+ ->method('fetchAll')
+ ->willReturn([
+ ['id' => 42],
+ ['id' => 43],
+ ]);
+ $result->expects(self::once())
+ ->method('closeCursor');
+ $deleteQb->expects(self::once())
+ ->method('delete')
+ ->willReturnSelf();
+ $deleteQb->expects(self::once())
+ ->method('executeStatement');
+ $this->jobList->expects(self::never())
+ ->method('add');
+
+ self::invokePrivate($this->job, 'run', [$argument]);
+ }
+
+ public function testRunWithFullBatch(): void {
+ $argument = $this->getArgument();
+ $selectQb = $this->getMockQueryBuilder();
+ $deleteQb = $this->getMockQueryBuilder();
+ $result = $this->createMock(IResult::class);
+
+ $calls = [
+ $selectQb,
+ $deleteQb,
+ ];
+ $this->connection->method('getQueryBuilder')
+ ->willReturnCallback(function () use (&$calls) {
+ return array_shift($calls);
+ });
+
+ $selectQb->expects(self::once())
+ ->method('executeQuery')
+ ->willReturn($result);
+ $result->expects(self::once())
+ ->method('fetchAll')
+ ->willReturn(array_map(static fn ($i) => ['id' => 42 + $i], range(0, 999)));
+ $result->expects(self::once())
+ ->method('closeCursor');
+ $deleteQb->expects(self::once())
+ ->method('delete')
+ ->willReturnSelf();
+ $deleteQb->expects(self::once())
+ ->method('executeStatement');
+ $this->jobList->expects(self::once())
+ ->method('add')
+ ->with(CleanupOrphanedChildrenJob::class, $argument);
+
+ self::invokePrivate($this->job, 'run', [$argument]);
+ }
+}
diff --git a/apps/dav/tests/unit/BackgroundJob/EventReminderJobTest.php b/apps/dav/tests/unit/BackgroundJob/EventReminderJobTest.php
index ed3c324e710..a46a1e5e5b0 100644
--- a/apps/dav/tests/unit/BackgroundJob/EventReminderJobTest.php
+++ b/apps/dav/tests/unit/BackgroundJob/EventReminderJobTest.php
@@ -3,28 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2018, Thomas Citharel <tcit@tcit.fr>
- *
- * @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: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Tests\unit\BackgroundJob;
@@ -36,18 +16,10 @@ use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class EventReminderJobTest extends TestCase {
-
- /** @var ITimeFactory|MockObject */
- private $time;
-
- /** @var ReminderService|MockObject */
- private $reminderService;
-
- /** @var IConfig|MockObject */
- private $config;
-
- /** @var EventReminderJob|MockObject */
- private $backgroundJob;
+ private ITimeFactory&MockObject $time;
+ private ReminderService&MockObject $reminderService;
+ private IConfig&MockObject $config;
+ private EventReminderJob $backgroundJob;
protected function setUp(): void {
parent::setUp();
@@ -63,7 +35,7 @@ class EventReminderJobTest extends TestCase {
);
}
- public function data(): array {
+ public static function data(): array {
return [
[true, true, true],
[true, false, false],
@@ -73,24 +45,19 @@ class EventReminderJobTest extends TestCase {
}
/**
- * @dataProvider data
*
* @param bool $sendEventReminders
* @param bool $sendEventRemindersMode
* @param bool $expectCall
*/
+ #[\PHPUnit\Framework\Attributes\DataProvider('data')]
public function testRun(bool $sendEventReminders, bool $sendEventRemindersMode, bool $expectCall): void {
- $this->config->expects($this->at(0))
+ $this->config->expects($this->exactly($sendEventReminders ? 2 : 1))
->method('getAppValue')
- ->with('dav', 'sendEventReminders', 'yes')
- ->willReturn($sendEventReminders ? 'yes' : 'no');
-
- if ($sendEventReminders) {
- $this->config->expects($this->at(1))
- ->method('getAppValue')
- ->with('dav', 'sendEventRemindersMode', 'backgroundjob')
- ->willReturn($sendEventRemindersMode ? 'backgroundjob' : 'cron');
- }
+ ->willReturnMap([
+ ['dav', 'sendEventReminders', 'yes', ($sendEventReminders ? 'yes' : 'no')],
+ ['dav', 'sendEventRemindersMode', 'backgroundjob', ($sendEventRemindersMode ? 'backgroundjob' : 'cron')],
+ ]);
if ($expectCall) {
$this->reminderService->expects($this->once())
diff --git a/apps/dav/tests/unit/BackgroundJob/GenerateBirthdayCalendarBackgroundJobTest.php b/apps/dav/tests/unit/BackgroundJob/GenerateBirthdayCalendarBackgroundJobTest.php
index e0601c5c71a..88a76ae1332 100644
--- a/apps/dav/tests/unit/BackgroundJob/GenerateBirthdayCalendarBackgroundJobTest.php
+++ b/apps/dav/tests/unit/BackgroundJob/GenerateBirthdayCalendarBackgroundJobTest.php
@@ -3,28 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2017, Georg Ehrke
- *
- * @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\Tests\unit\BackgroundJob;
@@ -36,18 +16,10 @@ use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class GenerateBirthdayCalendarBackgroundJobTest extends TestCase {
-
- /** @var ITimeFactory|MockObject */
- private $time;
-
- /** @var BirthdayService | MockObject */
- private $birthdayService;
-
- /** @var IConfig | MockObject */
- private $config;
-
- /** @var \OCA\DAV\BackgroundJob\GenerateBirthdayCalendarBackgroundJob */
- private $backgroundJob;
+ private ITimeFactory&MockObject $time;
+ private BirthdayService&MockObject $birthdayService;
+ private IConfig&MockObject $config;
+ private GenerateBirthdayCalendarBackgroundJob $backgroundJob;
protected function setUp(): void {
parent::setUp();
@@ -63,7 +35,7 @@ class GenerateBirthdayCalendarBackgroundJobTest extends TestCase {
);
}
- public function testRun() {
+ public function testRun(): void {
$this->config->expects($this->once())
->method('getAppValue')
->with('dav', 'generateBirthdayCalendar', 'yes')
@@ -85,7 +57,7 @@ class GenerateBirthdayCalendarBackgroundJobTest extends TestCase {
$this->backgroundJob->run(['userId' => 'user123']);
}
- public function testRunAndReset() {
+ public function testRunAndReset(): void {
$this->config->expects($this->once())
->method('getAppValue')
->with('dav', 'generateBirthdayCalendar', 'yes')
@@ -107,7 +79,7 @@ class GenerateBirthdayCalendarBackgroundJobTest extends TestCase {
$this->backgroundJob->run(['userId' => 'user123', 'purgeBeforeGenerating' => true]);
}
- public function testRunGloballyDisabled() {
+ public function testRunGloballyDisabled(): void {
$this->config->expects($this->once())
->method('getAppValue')
->with('dav', 'generateBirthdayCalendar', 'yes')
@@ -122,7 +94,7 @@ class GenerateBirthdayCalendarBackgroundJobTest extends TestCase {
$this->backgroundJob->run(['userId' => 'user123']);
}
- public function testRunUserDisabled() {
+ public function testRunUserDisabled(): void {
$this->config->expects($this->once())
->method('getAppValue')
->with('dav', 'generateBirthdayCalendar', 'yes')
diff --git a/apps/dav/tests/unit/BackgroundJob/OutOfOfficeEventDispatcherJobTest.php b/apps/dav/tests/unit/BackgroundJob/OutOfOfficeEventDispatcherJobTest.php
new file mode 100644
index 00000000000..6135fd00fdc
--- /dev/null
+++ b/apps/dav/tests/unit/BackgroundJob/OutOfOfficeEventDispatcherJobTest.php
@@ -0,0 +1,148 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\BackgroundJob;
+
+use OCA\DAV\BackgroundJob\OutOfOfficeEventDispatcherJob;
+use OCA\DAV\CalDAV\TimezoneService;
+use OCA\DAV\Db\Absence;
+use OCA\DAV\Db\AbsenceMapper;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\User\Events\OutOfOfficeEndedEvent;
+use OCP\User\Events\OutOfOfficeStartedEvent;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class OutOfOfficeEventDispatcherJobTest extends TestCase {
+ private OutOfOfficeEventDispatcherJob $job;
+ private ITimeFactory&MockObject $timeFactory;
+ private AbsenceMapper&MockObject $absenceMapper;
+ private LoggerInterface&MockObject $logger;
+ private IEventDispatcher&MockObject $eventDispatcher;
+ private IUserManager&MockObject $userManager;
+ private MockObject|TimezoneService $timezoneService;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->absenceMapper = $this->createMock(AbsenceMapper::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->timezoneService = $this->createMock(TimezoneService::class);
+
+ $this->job = new OutOfOfficeEventDispatcherJob(
+ $this->timeFactory,
+ $this->absenceMapper,
+ $this->logger,
+ $this->eventDispatcher,
+ $this->userManager,
+ $this->timezoneService,
+ );
+ }
+
+ public function testDispatchStartEvent(): void {
+ $this->timezoneService->method('getUserTimezone')->with('user')->willReturn('Europe/Berlin');
+
+ $absence = new Absence();
+ $absence->setId(200);
+ $absence->setUserId('user');
+
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('user');
+
+ $this->absenceMapper->expects(self::once())
+ ->method('findById')
+ ->with(1)
+ ->willReturn($absence);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with('user')
+ ->willReturn($user);
+ $this->eventDispatcher->expects(self::once())
+ ->method('dispatchTyped')
+ ->with(self::callback(static function ($event): bool {
+ self::assertInstanceOf(OutOfOfficeStartedEvent::class, $event);
+ return true;
+ }));
+
+ $this->job->run([
+ 'id' => 1,
+ 'event' => OutOfOfficeEventDispatcherJob::EVENT_START,
+ ]);
+ }
+
+ public function testDispatchStopEvent(): void {
+ $this->timezoneService->method('getUserTimezone')->with('user')->willReturn('Europe/Berlin');
+
+ $absence = new Absence();
+ $absence->setId(200);
+ $absence->setUserId('user');
+
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('user');
+
+ $this->absenceMapper->expects(self::once())
+ ->method('findById')
+ ->with(1)
+ ->willReturn($absence);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with('user')
+ ->willReturn($user);
+ $this->eventDispatcher->expects(self::once())
+ ->method('dispatchTyped')
+ ->with(self::callback(static function ($event): bool {
+ self::assertInstanceOf(OutOfOfficeEndedEvent::class, $event);
+ return true;
+ }));
+
+ $this->job->run([
+ 'id' => 1,
+ 'event' => OutOfOfficeEventDispatcherJob::EVENT_END,
+ ]);
+ }
+
+ public function testDoesntDispatchUnknownEvent(): void {
+ $this->timezoneService->method('getUserTimezone')->with('user')->willReturn('Europe/Berlin');
+
+ $absence = new Absence();
+ $absence->setId(100);
+ $absence->setUserId('user');
+
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('user');
+
+ $this->absenceMapper->expects(self::once())
+ ->method('findById')
+ ->with(1)
+ ->willReturn($absence);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with('user')
+ ->willReturn($user);
+ $this->eventDispatcher->expects(self::never())
+ ->method('dispatchTyped');
+ $this->logger->expects(self::once())
+ ->method('error');
+
+ $this->job->run([
+ 'id' => 1,
+ 'event' => 'foobar',
+ ]);
+ }
+}
diff --git a/apps/dav/tests/unit/BackgroundJob/PruneOutdatedSyncTokensJobTest.php b/apps/dav/tests/unit/BackgroundJob/PruneOutdatedSyncTokensJobTest.php
new file mode 100644
index 00000000000..1838fb2537d
--- /dev/null
+++ b/apps/dav/tests/unit/BackgroundJob/PruneOutdatedSyncTokensJobTest.php
@@ -0,0 +1,82 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\BackgroundJob;
+
+use InvalidArgumentException;
+use OCA\DAV\AppInfo\Application;
+use OCA\DAV\BackgroundJob\PruneOutdatedSyncTokensJob;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\IConfig;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class PruneOutdatedSyncTokensJobTest extends TestCase {
+ private ITimeFactory&MockObject $timeFactory;
+ private CalDavBackend&MockObject $calDavBackend;
+ private CardDavBackend&MockObject $cardDavBackend;
+ private IConfig&MockObject $config;
+ private LoggerInterface&MockObject $logger;
+ private PruneOutdatedSyncTokensJob $backgroundJob;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->calDavBackend = $this->createMock(CalDavBackend::class);
+ $this->cardDavBackend = $this->createMock(CardDavBackend::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+
+ $this->backgroundJob = new PruneOutdatedSyncTokensJob($this->timeFactory, $this->calDavBackend, $this->cardDavBackend, $this->config, $this->logger);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataForTestRun')]
+ public function testRun(string $configToKeep, string $configRetentionDays, int $actualLimit, int $retentionDays, int $deletedCalendarSyncTokens, int $deletedAddressBookSyncTokens): void {
+ $this->config->expects($this->exactly(2))
+ ->method('getAppValue')
+ ->with(Application::APP_ID, self::anything(), self::anything())
+ ->willReturnCallback(function ($app, $key) use ($configToKeep, $configRetentionDays) {
+ switch ($key) {
+ case 'totalNumberOfSyncTokensToKeep':
+ return $configToKeep;
+ case 'syncTokensRetentionDays':
+ return $configRetentionDays;
+ default:
+ throw new InvalidArgumentException();
+ }
+ });
+ $this->calDavBackend->expects($this->once())
+ ->method('pruneOutdatedSyncTokens')
+ ->with($actualLimit)
+ ->willReturn($deletedCalendarSyncTokens);
+ $this->cardDavBackend->expects($this->once())
+ ->method('pruneOutdatedSyncTokens')
+ ->with($actualLimit, $retentionDays)
+ ->willReturn($deletedAddressBookSyncTokens);
+ $this->logger->expects($this->once())
+ ->method('info')
+ ->with('Pruned {calendarSyncTokensNumber} calendar sync tokens and {addressBooksSyncTokensNumber} address book sync tokens', [
+ 'calendarSyncTokensNumber' => $deletedCalendarSyncTokens,
+ 'addressBooksSyncTokensNumber' => $deletedAddressBookSyncTokens
+ ]);
+
+ $this->backgroundJob->run(null);
+ }
+
+ public static function dataForTestRun(): array {
+ return [
+ ['100', '2', 100, 7 * 24 * 3600, 2, 3],
+ ['100', '14', 100, 14 * 24 * 3600, 2, 3],
+ ['0', '60', 1, 60 * 24 * 3600, 0, 0]
+ ];
+ }
+}
diff --git a/apps/dav/tests/unit/BackgroundJob/RefreshWebcalJobTest.php b/apps/dav/tests/unit/BackgroundJob/RefreshWebcalJobTest.php
index 360c4c791c7..7713ef2945a 100644
--- a/apps/dav/tests/unit/BackgroundJob/RefreshWebcalJobTest.php
+++ b/apps/dav/tests/unit/BackgroundJob/RefreshWebcalJobTest.php
@@ -3,28 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018, 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: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Tests\unit\BackgroundJob;
@@ -33,34 +13,24 @@ use OCA\DAV\CalDAV\WebcalCaching\RefreshWebcalService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
use OCP\IConfig;
-use OCP\ILogger;
use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
use Test\TestCase;
class RefreshWebcalJobTest extends TestCase {
-
- /** @var RefreshWebcalService | MockObject */
- private $refreshWebcalService;
-
- /** @var IConfig | MockObject */
- private $config;
-
- /** @var ILogger | MockObject */
- private $logger;
-
- /** @var ITimeFactory | MockObject */
- private $timeFactory;
-
- /** @var IJobList | MockObject */
- private $jobList;
+ private RefreshWebcalService&MockObject $refreshWebcalService;
+ private IConfig&MockObject $config;
+ private LoggerInterface $logger;
+ private ITimeFactory&MockObject $timeFactory;
+ private IJobList&MockObject $jobList;
protected function setUp(): void {
parent::setUp();
$this->refreshWebcalService = $this->createMock(RefreshWebcalService::class);
$this->config = $this->createMock(IConfig::class);
- $this->logger = $this->createMock(ILogger::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->jobList = $this->createMock(IJobList::class);
@@ -71,10 +41,9 @@ class RefreshWebcalJobTest extends TestCase {
* @param int $lastRun
* @param int $time
* @param bool $process
- *
- * @dataProvider runDataProvider
*/
- public function testRun(int $lastRun, int $time, bool $process) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('runDataProvider')]
+ public function testRun(int $lastRun, int $time, bool $process): void {
$backgroundJob = new RefreshWebcalJob($this->refreshWebcalService, $this->config, $this->logger, $this->timeFactory);
$backgroundJob->setId(42);
@@ -99,7 +68,7 @@ class RefreshWebcalJobTest extends TestCase {
$this->config->expects($this->once())
->method('getAppValue')
- ->with('dav', 'calendarSubscriptionRefreshRate', 'P1W')
+ ->with('dav', 'calendarSubscriptionRefreshRate', 'P1D')
->willReturn('P1W');
$this->timeFactory->method('getTime')
@@ -115,13 +84,10 @@ class RefreshWebcalJobTest extends TestCase {
->with('principals/users/testuser', 'sub123');
}
- $backgroundJob->execute($this->jobList, $this->logger);
+ $backgroundJob->start($this->jobList);
}
- /**
- * @return array
- */
- public function runDataProvider():array {
+ public static function runDataProvider():array {
return [
[0, 100000, true],
[100000, 100000, false]
diff --git a/apps/dav/tests/unit/BackgroundJob/RegisterRegenerateBirthdayCalendarsTest.php b/apps/dav/tests/unit/BackgroundJob/RegisterRegenerateBirthdayCalendarsTest.php
index 00931d53f64..6c9214d0268 100644
--- a/apps/dav/tests/unit/BackgroundJob/RegisterRegenerateBirthdayCalendarsTest.php
+++ b/apps/dav/tests/unit/BackgroundJob/RegisterRegenerateBirthdayCalendarsTest.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 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: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Tests\unit\BackgroundJob;
@@ -32,24 +12,16 @@ use OCA\DAV\BackgroundJob\GenerateBirthdayCalendarBackgroundJob;
use OCA\DAV\BackgroundJob\RegisterRegenerateBirthdayCalendars;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
-use OCP\IConfig;
use OCP\IUser;
use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class RegisterRegenerateBirthdayCalendarsTest extends TestCase {
-
- /** @var ITimeFactory | \PHPUnit\Framework\MockObject\MockObject */
- private $time;
-
- /** @var IUserManager | \PHPUnit\Framework\MockObject\MockObject */
- private $userManager;
-
- /** @var IJobList | \PHPUnit\Framework\MockObject\MockObject */
- private $jobList;
-
- /** @var RegisterRegenerateBirthdayCalendars */
- private $backgroundJob;
+ private ITimeFactory&MockObject $time;
+ private IUserManager&MockObject $userManager;
+ private IJobList&MockObject $jobList;
+ private RegisterRegenerateBirthdayCalendars $backgroundJob;
protected function setUp(): void {
parent::setUp();
@@ -65,10 +37,10 @@ class RegisterRegenerateBirthdayCalendarsTest extends TestCase {
);
}
- public function testRun() {
+ public function testRun(): void {
$this->userManager->expects($this->once())
->method('callForSeenUsers')
- ->willReturnCallback(function ($closure) {
+ ->willReturnCallback(function ($closure): void {
$user1 = $this->createMock(IUser::class);
$user1->method('getUID')->willReturn('uid1');
$user2 = $this->createMock(IUser::class);
@@ -81,24 +53,26 @@ class RegisterRegenerateBirthdayCalendarsTest extends TestCase {
$closure($user3);
});
- $this->jobList->expects($this->at(0))
+ $calls = [
+ 'uid1',
+ 'uid2',
+ 'uid3',
+ ];
+ $this->jobList->expects($this->exactly(3))
->method('add')
- ->with(GenerateBirthdayCalendarBackgroundJob::class, [
- 'userId' => 'uid1',
- 'purgeBeforeGenerating' => true
- ]);
- $this->jobList->expects($this->at(1))
- ->method('add')
- ->with(GenerateBirthdayCalendarBackgroundJob::class, [
- 'userId' => 'uid2',
- 'purgeBeforeGenerating' => true
- ]);
- $this->jobList->expects($this->at(2))
- ->method('add')
- ->with(GenerateBirthdayCalendarBackgroundJob::class, [
- 'userId' => 'uid3',
- 'purgeBeforeGenerating' => true
- ]);
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals(
+ [
+ GenerateBirthdayCalendarBackgroundJob::class,
+ [
+ 'userId' => $expected,
+ 'purgeBeforeGenerating' => true
+ ]
+ ],
+ func_get_args()
+ );
+ });
$this->backgroundJob->run([]);
}
diff --git a/apps/dav/tests/unit/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJobTest.php b/apps/dav/tests/unit/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJobTest.php
index 59b68452862..38a981787cd 100644
--- a/apps/dav/tests/unit/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJobTest.php
+++ b/apps/dav/tests/unit/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJobTest.php
@@ -3,64 +3,24 @@
declare(strict_types=1);
/**
- * @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 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: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Tests\unit\BackgroundJob;
use OCA\DAV\BackgroundJob\UpdateCalendarResourcesRoomsBackgroundJob;
-use OCA\DAV\CalDAV\CalDavBackend;
use OCP\AppFramework\Utility\ITimeFactory;
-use OCP\Calendar\BackendTemporarilyUnavailableException;
-use OCP\Calendar\IMetadataProvider;
-use OCP\Calendar\Resource\IBackend;
use OCP\Calendar\Resource\IManager as IResourceManager;
-use OCP\Calendar\Resource\IResource;
use OCP\Calendar\Room\IManager as IRoomManager;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
-interface tmpI extends IResource, IMetadataProvider {
-}
-
class UpdateCalendarResourcesRoomsBackgroundJobTest extends TestCase {
-
- /** @var ITimeFactory|MockObject */
- private $time;
-
- /** @var IResourceManager|MockObject */
- private $resourceManager;
-
- /** @var IRoomManager|MockObject */
- private $roomManager;
-
- /** @var CalDavBackend|MockObject */
- private $calDavBackend;
-
- /** @var UpdateCalendarResourcesRoomsBackgroundJob */
- private $backgroundJob;
+ private UpdateCalendarResourcesRoomsBackgroundJob $backgroundJob;
+ private ITimeFactory&MockObject $time;
+ private IResourceManager&MockObject $resourceManager;
+ private IRoomManager&MockObject $roomManager;
protected function setUp(): void {
parent::setUp();
@@ -68,390 +28,20 @@ class UpdateCalendarResourcesRoomsBackgroundJobTest extends TestCase {
$this->time = $this->createMock(ITimeFactory::class);
$this->resourceManager = $this->createMock(IResourceManager::class);
$this->roomManager = $this->createMock(IRoomManager::class);
- $this->calDavBackend = $this->createMock(CalDavBackend::class);
$this->backgroundJob = new UpdateCalendarResourcesRoomsBackgroundJob(
$this->time,
$this->resourceManager,
$this->roomManager,
- self::$realDatabase,
- $this->calDavBackend
);
}
- protected function tearDown(): void {
- $query = self::$realDatabase->getQueryBuilder();
- $query->delete('calendar_resources')->execute();
- $query->delete('calendar_resources_md')->execute();
- $query->delete('calendar_rooms')->execute();
- $query->delete('calendar_rooms_md')->execute();
- }
-
- /**
- * Data in Cache:
- * resources:
- * [backend1, res1, Beamer1, {}] - []
- * [backend1, res2, TV1, {}] - []
- * [backend2, res3, Beamer2, {}] - ['meta1' => 'value1', 'meta2' => 'value2']
- * [backend2, res4, TV2, {}] - ['meta1' => 'value1', 'meta3' => 'value3-old']
- * [backend3, res5, Beamer3, {}] - []
- * [backend3, res6, Pointer, {foo, bar}] - ['meta99' => 'value99']
- *
- * Data in Backend:
- * backend1 gone
- * backend2 throws BackendTemporarilyUnavailableException
- * [backend3, res6, Pointer123, {foo, biz}] - ['meta99' => 'value99-new', 'meta123' => 'meta456']
- * [backend3, res7, Resource4, {biz}] - ['meta1' => 'value1']
- * [backend4, res8, Beamer, {}] - ['meta2' => 'value2']
- * [backend4, res9, Beamer2, {}] - []
- *
- * Expected after run:
- * [backend1, res1, Beamer1, {}] - []
- * [backend1, res2, TV1, {}] - []
- * [backend2, res3, Beamer2, {}] - ['meta1' => 'value1', 'meta2' => 'value2']
- * [backend2, res4, TV2, {}] - ['meta1' => 'value1', 'meta3' => 'value3-old']
- * [backend3, res6, Pointer123, {foo, biz}] - ['meta99' => 'value99-new', 'meta123' => 'meta456']
- * [backend3, res7, Resource4, {biz}] - ['meta1' => 'value1']
- * [backend4, res8, Beamer, {}] - ['meta2' => 'value2']
- * [backend4, res9, Beamer2, {}] - []
- */
-
- public function testRun() {
- $this->createTestResourcesInCache();
-
- $backend2 = $this->createMock(IBackend::class);
- $backend3 = $this->createMock(IBackend::class);
- $backend4 = $this->createMock(IBackend::class);
-
- $res6 = $this->createMock(tmpI::class);
- $res7 = $this->createMock(tmpI::class);
- $res8 = $this->createMock(tmpI::class);
- $res9 = $this->createMock(IResource::class);
-
- $backend2->method('getBackendIdentifier')
- ->willReturn('backend2');
- $backend2->method('listAllResources')
- ->will($this->throwException(new BackendTemporarilyUnavailableException()));
- $backend2->method('getResource')
- ->will($this->throwException(new BackendTemporarilyUnavailableException()));
- $backend2->method('getAllResources')
- ->will($this->throwException(new BackendTemporarilyUnavailableException()));
- $backend3->method('getBackendIdentifier')
- ->willReturn('backend3');
- $backend3->method('listAllResources')
- ->willReturn(['res6', 'res7']);
- $backend3->method('getResource')
- ->willReturnMap([
- ['res6', $res6],
- ['res7', $res7],
- ]);
- $backend4->method('getBackendIdentifier')
- ->willReturn('backend4');
- $backend4->method('listAllResources')
- ->willReturn(['res8', 'res9']);
- $backend4->method('getResource')
- ->willReturnMap([
- ['res8', $res8],
- ['res9', $res9],
- ]);
-
- $res6->method('getId')->willReturn('res6');
- $res6->method('getDisplayName')->willReturn('Pointer123');
- $res6->method('getGroupRestrictions')->willReturn(['foo', 'biz']);
- $res6->method('getEMail')->willReturn('res6@foo.bar');
- $res6->method('getBackend')->willReturn($backend3);
-
- $res6->method('getAllAvailableMetadataKeys')->willReturn(['meta99', 'meta123']);
- $res6->method('getMetadataForKey')->willReturnCallback(function ($key) {
- switch ($key) {
- case 'meta99':
- return 'value99-new';
-
- case 'meta123':
- return 'meta456';
-
- default:
- return null;
- }
- });
-
- $res7->method('getId')->willReturn('res7');
- $res7->method('getDisplayName')->willReturn('Resource4');
- $res7->method('getGroupRestrictions')->willReturn(['biz']);
- $res7->method('getEMail')->willReturn('res7@foo.bar');
- $res7->method('getBackend')->willReturn($backend3);
- $res7->method('getAllAvailableMetadataKeys')->willReturn(['meta1']);
- $res7->method('getMetadataForKey')->willReturnCallback(function ($key) {
- switch ($key) {
- case 'meta1':
- return 'value1';
-
- default:
- return null;
- }
- });
-
- $res8->method('getId')->willReturn('res8');
- $res8->method('getDisplayName')->willReturn('Beamer');
- $res8->method('getGroupRestrictions')->willReturn([]);
- $res8->method('getEMail')->willReturn('res8@foo.bar');
- $res8->method('getBackend')->willReturn($backend4);
- $res8->method('getAllAvailableMetadataKeys')->willReturn(['meta2']);
- $res8->method('getMetadataForKey')->willReturnCallback(function ($key) {
- switch ($key) {
- case 'meta2':
- return 'value2';
-
- default:
- return null;
- }
- });
-
- $res9->method('getId')->willReturn('res9');
- $res9->method('getDisplayName')->willReturn('Beamer2');
- $res9->method('getGroupRestrictions')->willReturn([]);
- $res9->method('getEMail')->willReturn('res9@foo.bar');
- $res9->method('getBackend')->willReturn($backend4);
-
- $this->resourceManager
- ->method('getBackends')
- ->willReturn([
- $backend2, $backend3, $backend4
- ]);
- $this->resourceManager
- ->method('getBackend')
- ->willReturnMap([
- ['backend2', $backend2],
- ['backend3', $backend3],
- ['backend4', $backend4],
- ]);
+ public function testRun(): void {
+ $this->resourceManager->expects(self::once())
+ ->method('update');
+ $this->roomManager->expects(self::once())
+ ->method('update');
$this->backgroundJob->run([]);
-
- $query = self::$realDatabase->getQueryBuilder();
- $query->select('*')->from('calendar_resources');
-
- $rows = [];
- $ids = [];
- $stmt = $query->execute();
- while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
- $ids[$row['backend_id'] . '::' . $row['resource_id']] = $row['id'];
- unset($row['id']);
- $rows[] = $row;
- }
-
- $this->assertEquals([
- [
- 'backend_id' => 'backend1',
- 'resource_id' => 'res1',
- 'displayname' => 'Beamer1',
- 'email' => 'res1@foo.bar',
- 'group_restrictions' => '[]',
- ],
- [
- 'backend_id' => 'backend1',
- 'resource_id' => 'res2',
- 'displayname' => 'TV1',
- 'email' => 'res2@foo.bar',
- 'group_restrictions' => '[]',
- ],
- [
- 'backend_id' => 'backend2',
- 'resource_id' => 'res3',
- 'displayname' => 'Beamer2',
- 'email' => 'res3@foo.bar',
- 'group_restrictions' => '[]',
- ],
- [
- 'backend_id' => 'backend2',
- 'resource_id' => 'res4',
- 'displayname' => 'TV2',
- 'email' => 'res4@foo.bar',
- 'group_restrictions' => '[]',
- ],
- [
- 'backend_id' => 'backend3',
- 'resource_id' => 'res6',
- 'displayname' => 'Pointer123',
- 'email' => 'res6@foo.bar',
- 'group_restrictions' => '["foo","biz"]',
- ],
- [
- 'backend_id' => 'backend3',
- 'resource_id' => 'res7',
- 'displayname' => 'Resource4',
- 'email' => 'res7@foo.bar',
- 'group_restrictions' => '["biz"]',
- ],
- [
- 'backend_id' => 'backend4',
- 'resource_id' => 'res8',
- 'displayname' => 'Beamer',
- 'email' => 'res8@foo.bar',
- 'group_restrictions' => '[]',
- ],
- [
- 'backend_id' => 'backend4',
- 'resource_id' => 'res9',
- 'displayname' => 'Beamer2',
- 'email' => 'res9@foo.bar',
- 'group_restrictions' => '[]',
- ],
- ], $rows);
-
- $query2 = self::$realDatabase->getQueryBuilder();
- $query2->select('*')->from('calendar_resources_md');
-
- $rows2 = [];
- $stmt = $query2->execute();
- while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
- unset($row['id']);
- $rows2[] = $row;
- }
-
- $this->assertEquals([
- [
- 'resource_id' => $ids['backend2::res3'],
- 'key' => 'meta1',
- 'value' => 'value1',
- ],
- [
- 'resource_id' => $ids['backend2::res3'],
- 'key' => 'meta2',
- 'value' => 'value2',
- ],
- [
- 'resource_id' => $ids['backend2::res4'],
- 'key' => 'meta1',
- 'value' => 'value1',
- ],
- [
- 'resource_id' => $ids['backend2::res4'],
- 'key' => 'meta3',
- 'value' => 'value3-old',
- ],
- [
- 'resource_id' => $ids['backend3::res6'],
- 'key' => 'meta99',
- 'value' => 'value99-new',
- ],
- [
- 'resource_id' => $ids['backend3::res7'],
- 'key' => 'meta1',
- 'value' => 'value1',
- ],
- [
- 'resource_id' => $ids['backend3::res6'],
- 'key' => 'meta123',
- 'value' => 'meta456',
- ],
- [
- 'resource_id' => $ids['backend4::res8'],
- 'key' => 'meta2',
- 'value' => 'value2',
- ]
- ], $rows2);
- }
-
- protected function createTestResourcesInCache() {
- $query = self::$realDatabase->getQueryBuilder();
- $query->insert('calendar_resources')
- ->values([
- 'backend_id' => $query->createNamedParameter('backend1'),
- 'resource_id' => $query->createNamedParameter('res1'),
- 'email' => $query->createNamedParameter('res1@foo.bar'),
- 'displayname' => $query->createNamedParameter('Beamer1'),
- 'group_restrictions' => $query->createNamedParameter('[]'),
- ])
- ->execute();
-
- $query->insert('calendar_resources')
- ->values([
- 'backend_id' => $query->createNamedParameter('backend1'),
- 'resource_id' => $query->createNamedParameter('res2'),
- 'email' => $query->createNamedParameter('res2@foo.bar'),
- 'displayname' => $query->createNamedParameter('TV1'),
- 'group_restrictions' => $query->createNamedParameter('[]'),
- ])
- ->execute();
-
- $query->insert('calendar_resources')
- ->values([
- 'backend_id' => $query->createNamedParameter('backend2'),
- 'resource_id' => $query->createNamedParameter('res3'),
- 'email' => $query->createNamedParameter('res3@foo.bar'),
- 'displayname' => $query->createNamedParameter('Beamer2'),
- 'group_restrictions' => $query->createNamedParameter('[]'),
- ])
- ->execute();
- $id3 = $query->getLastInsertId();
-
- $query->insert('calendar_resources')
- ->values([
- 'backend_id' => $query->createNamedParameter('backend2'),
- 'resource_id' => $query->createNamedParameter('res4'),
- 'email' => $query->createNamedParameter('res4@foo.bar'),
- 'displayname' => $query->createNamedParameter('TV2'),
- 'group_restrictions' => $query->createNamedParameter('[]'),
- ])
- ->execute();
- $id4 = $query->getLastInsertId();
-
- $query->insert('calendar_resources')
- ->values([
- 'backend_id' => $query->createNamedParameter('backend3'),
- 'resource_id' => $query->createNamedParameter('res5'),
- 'email' => $query->createNamedParameter('res5@foo.bar'),
- 'displayname' => $query->createNamedParameter('Beamer3'),
- 'group_restrictions' => $query->createNamedParameter('[]'),
- ])
- ->execute();
-
- $query->insert('calendar_resources')
- ->values([
- 'backend_id' => $query->createNamedParameter('backend3'),
- 'resource_id' => $query->createNamedParameter('res6'),
- 'email' => $query->createNamedParameter('res6@foo.bar'),
- 'displayname' => $query->createNamedParameter('Pointer'),
- 'group_restrictions' => $query->createNamedParameter('["foo", "bar"]'),
- ])
- ->execute();
- $id6 = $query->getLastInsertId();
-
- $query->insert('calendar_resources_md')
- ->values([
- 'resource_id' => $query->createNamedParameter($id3),
- 'key' => $query->createNamedParameter('meta1'),
- 'value' => $query->createNamedParameter('value1')
- ])
- ->execute();
- $query->insert('calendar_resources_md')
- ->values([
- 'resource_id' => $query->createNamedParameter($id3),
- 'key' => $query->createNamedParameter('meta2'),
- 'value' => $query->createNamedParameter('value2')
- ])
- ->execute();
- $query->insert('calendar_resources_md')
- ->values([
- 'resource_id' => $query->createNamedParameter($id4),
- 'key' => $query->createNamedParameter('meta1'),
- 'value' => $query->createNamedParameter('value1')
- ])
- ->execute();
- $query->insert('calendar_resources_md')
- ->values([
- 'resource_id' => $query->createNamedParameter($id4),
- 'key' => $query->createNamedParameter('meta3'),
- 'value' => $query->createNamedParameter('value3-old')
- ])
- ->execute();
- $query->insert('calendar_resources_md')
- ->values([
- 'resource_id' => $query->createNamedParameter($id6),
- 'key' => $query->createNamedParameter('meta99'),
- 'value' => $query->createNamedParameter('value99')
- ])
- ->execute();
}
}
diff --git a/apps/dav/tests/unit/BackgroundJob/UserStatusAutomationTest.php b/apps/dav/tests/unit/BackgroundJob/UserStatusAutomationTest.php
new file mode 100644
index 00000000000..d49d20180d9
--- /dev/null
+++ b/apps/dav/tests/unit/BackgroundJob/UserStatusAutomationTest.php
@@ -0,0 +1,220 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\BackgroundJob;
+
+use OC\User\OutOfOfficeData;
+use OCA\DAV\BackgroundJob\UserStatusAutomation;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\Server;
+use OCP\User\IAvailabilityCoordinator;
+use OCP\UserStatus\IManager;
+use OCP\UserStatus\IUserStatus;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+/**
+ * @group DB
+ */
+class UserStatusAutomationTest extends TestCase {
+ protected ITimeFactory&MockObject $time;
+ protected IJobList&MockObject $jobList;
+ protected LoggerInterface&MockObject $logger;
+ protected IManager&MockObject $statusManager;
+ protected IConfig&MockObject $config;
+ private IAvailabilityCoordinator&MockObject $coordinator;
+ private IUserManager&MockObject $userManager;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->time = $this->createMock(ITimeFactory::class);
+ $this->jobList = $this->createMock(IJobList::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->statusManager = $this->createMock(IManager::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->coordinator = $this->createMock(IAvailabilityCoordinator::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+
+ }
+
+ protected function getAutomationMock(array $methods): MockObject|UserStatusAutomation {
+ if (empty($methods)) {
+ return new UserStatusAutomation(
+ $this->time,
+ Server::get(IDBConnection::class),
+ $this->jobList,
+ $this->logger,
+ $this->statusManager,
+ $this->config,
+ $this->coordinator,
+ $this->userManager,
+ );
+ }
+
+ return $this->getMockBuilder(UserStatusAutomation::class)
+ ->setConstructorArgs([
+ $this->time,
+ Server::get(IDBConnection::class),
+ $this->jobList,
+ $this->logger,
+ $this->statusManager,
+ $this->config,
+ $this->coordinator,
+ $this->userManager,
+ ])
+ ->onlyMethods($methods)
+ ->getMock();
+ }
+
+ public static function dataRun(): array {
+ return [
+ ['20230217', '2023-02-24 10:49:36.613834', true],
+ ['20230224', '2023-02-24 10:49:36.613834', true],
+ ['20230217', '2023-02-24 13:58:24.479357', false],
+ ['20230224', '2023-02-24 13:58:24.479357', false],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataRun')]
+ public function testRunNoOOO(string $ruleDay, string $currentTime, bool $isAvailable): void {
+ $user = $this->createConfiguredMock(IUser::class, [
+ 'getUID' => 'user'
+ ]);
+
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn($user);
+ $this->coordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->willReturn(null);
+ $this->config->method('getUserValue')
+ ->with('user', 'dav', 'user_status_automation', 'no')
+ ->willReturn('yes');
+ $this->time->method('getDateTime')
+ ->willReturn(new \DateTime($currentTime, new \DateTimeZone('UTC')));
+ $this->logger->expects(self::exactly(4))
+ ->method('debug');
+ if (!$isAvailable) {
+ $this->statusManager->expects(self::once())
+ ->method('setUserStatus')
+ ->with('user', IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND, true);
+ }
+ $automation = $this->getAutomationMock(['getAvailabilityFromPropertiesTable']);
+ $automation->method('getAvailabilityFromPropertiesTable')
+ ->with('user')
+ ->willReturn('BEGIN:VCALENDAR
+PRODID:Nextcloud DAV app
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+BEGIN:STANDARD
+TZNAME:CET
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+BEGIN:DAYLIGHT
+TZNAME:CEST
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VAVAILABILITY
+BEGIN:AVAILABLE
+DTSTART;TZID=Europe/Berlin:' . $ruleDay . 'T090000
+DTEND;TZID=Europe/Berlin:' . $ruleDay . 'T170000
+UID:3e6feeec-8e00-4265-b822-b73174e8b39f
+RRULE:FREQ=WEEKLY;BYDAY=TH
+END:AVAILABLE
+BEGIN:AVAILABLE
+DTSTART;TZID=Europe/Berlin:' . $ruleDay . 'T090000
+DTEND;TZID=Europe/Berlin:' . $ruleDay . 'T120000
+UID:8a634e99-07cf-443b-b480-005a0e1db323
+RRULE:FREQ=WEEKLY;BYDAY=FR
+END:AVAILABLE
+END:VAVAILABILITY
+END:VCALENDAR');
+
+ self::invokePrivate($automation, 'run', [['userId' => 'user']]);
+ }
+
+ public function testRunNoAvailabilityNoOOO(): void {
+ $user = $this->createConfiguredMock(IUser::class, [
+ 'getUID' => 'user'
+ ]);
+
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn($user);
+ $this->coordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->willReturn(null);
+ $this->config->method('getUserValue')
+ ->with('user', 'dav', 'user_status_automation', 'no')
+ ->willReturn('yes');
+ $this->time->method('getDateTime')
+ ->willReturn(new \DateTime('2023-02-24 13:58:24.479357', new \DateTimeZone('UTC')));
+ $this->jobList->expects($this->once())
+ ->method('remove')
+ ->with(UserStatusAutomation::class, ['userId' => 'user']);
+ $this->logger->expects(self::once())
+ ->method('debug');
+ $this->logger->expects(self::once())
+ ->method('info');
+ $automation = $this->getAutomationMock(['getAvailabilityFromPropertiesTable']);
+ $automation->method('getAvailabilityFromPropertiesTable')
+ ->with('user')
+ ->willReturn(false);
+
+ self::invokePrivate($automation, 'run', [['userId' => 'user']]);
+ }
+
+ public function testRunNoAvailabilityWithOOO(): void {
+ $user = $this->createConfiguredMock(IUser::class, [
+ 'getUID' => 'user'
+ ]);
+ $ooo = $this->createConfiguredMock(OutOfOfficeData::class, [
+ 'getShortMessage' => 'On Vacation',
+ 'getEndDate' => 123456,
+ ]);
+
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn($user);
+ $this->coordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->willReturn($ooo);
+ $this->coordinator->expects(self::once())
+ ->method('isInEffect')
+ ->willReturn(true);
+ $this->statusManager->expects(self::once())
+ ->method('setUserStatus')
+ ->with('user', IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::DND, true, $ooo->getShortMessage());
+ $this->config->expects(self::never())
+ ->method('getUserValue');
+ $this->time->method('getDateTime')
+ ->willReturn(new \DateTime('2023-02-24 13:58:24.479357', new \DateTimeZone('UTC')));
+ $this->jobList->expects($this->never())
+ ->method('remove');
+ $this->logger->expects(self::exactly(2))
+ ->method('debug');
+ $automation = $this->getAutomationMock([]);
+
+ self::invokePrivate($automation, 'run', [['userId' => 'user']]);
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php b/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php
index c659e2ccc6d..45937d86873 100644
--- a/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php
+++ b/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php
@@ -1,48 +1,36 @@
<?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 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\Tests\unit\CalDAV;
use OC\KnownUser\KnownUserService;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
+use OCA\DAV\CalDAV\Sharing\Backend as SharingBackend;
+use OCA\DAV\CalDAV\Sharing\Service;
use OCA\DAV\Connector\Sabre\Principal;
+use OCA\DAV\DAV\Sharing\SharingMapper;
+use OCP\Accounts\IAccountManager;
use OCP\App\IAppManager;
use OCP\EventDispatcher\IEventDispatcher;
+use OCP\ICacheFactory;
use OCP\IConfig;
+use OCP\IDBConnection;
use OCP\IGroupManager;
-use OCP\ILogger;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\L10N\IFactory;
use OCP\Security\ISecureRandom;
+use OCP\Server;
use OCP\Share\IManager as ShareManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
use Sabre\DAV\Xml\Property\Href;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Test\TestCase;
/**
@@ -54,25 +42,17 @@ use Test\TestCase;
*/
abstract class AbstractCalDavBackend extends TestCase {
- /** @var CalDavBackend */
- protected $backend;
-
- /** @var Principal | \PHPUnit\Framework\MockObject\MockObject */
- protected $principal;
- /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */
- protected $userManager;
- /** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject */
- protected $groupManager;
- /** @var IEventDispatcher|\PHPUnit\Framework\MockObject\MockObject */
- protected $dispatcher;
- /** @var EventDispatcherInterface|\PHPUnit\Framework\MockObject\MockObject */
- protected $legacyDispatcher;
-
- /** @var ISecureRandom */
- private $random;
- /** @var ILogger */
- private $logger;
+ protected CalDavBackend $backend;
+ protected Principal&MockObject $principal;
+ protected IUserManager&MockObject $userManager;
+ protected IGroupManager&MockObject $groupManager;
+ protected IEventDispatcher&MockObject $dispatcher;
+ private LoggerInterface&MockObject $logger;
+ private IConfig&MockObject $config;
+ private ISecureRandom $random;
+ protected SharingBackend $sharingBackend;
+ protected IDBConnection $db;
public const UNIT_TEST_USER = 'principals/users/caldav-unit-test';
public const UNIT_TEST_USER1 = 'principals/users/caldav-unit-test1';
public const UNIT_TEST_GROUP = 'principals/groups/caldav-unit-test-group';
@@ -84,11 +64,11 @@ abstract class AbstractCalDavBackend extends TestCase {
$this->userManager = $this->createMock(IUserManager::class);
$this->groupManager = $this->createMock(IGroupManager::class);
$this->dispatcher = $this->createMock(IEventDispatcher::class);
- $this->legacyDispatcher = $this->createMock(EventDispatcherInterface::class);
$this->principal = $this->getMockBuilder(Principal::class)
->setConstructorArgs([
$this->userManager,
$this->groupManager,
+ $this->createMock(IAccountManager::class),
$this->createMock(ShareManager::class),
$this->createMock(IUserSession::class),
$this->createMock(IAppManager::class),
@@ -97,7 +77,7 @@ abstract class AbstractCalDavBackend extends TestCase {
$this->createMock(IConfig::class),
$this->createMock(IFactory::class)
])
- ->setMethods(['getPrincipalByPath', 'getGroupMembership'])
+ ->onlyMethods(['getPrincipalByPath', 'getGroupMembership', 'findByUri'])
->getMock();
$this->principal->expects($this->any())->method('getPrincipalByPath')
->willReturn([
@@ -108,20 +88,27 @@ abstract class AbstractCalDavBackend extends TestCase {
->withAnyParameters()
->willReturn([self::UNIT_TEST_GROUP, self::UNIT_TEST_GROUP2]);
- $db = \OC::$server->getDatabaseConnection();
- $this->random = \OC::$server->getSecureRandom();
- $this->logger = $this->createMock(ILogger::class);
+ $this->db = Server::get(IDBConnection::class);
+ $this->random = Server::get(ISecureRandom::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
$this->config = $this->createMock(IConfig::class);
+ $this->sharingBackend = new SharingBackend(
+ $this->userManager,
+ $this->groupManager,
+ $this->principal,
+ $this->createMock(ICacheFactory::class),
+ new Service(new SharingMapper($this->db)),
+ $this->logger);
$this->backend = new CalDavBackend(
- $db,
+ $this->db,
$this->principal,
$this->userManager,
- $this->groupManager,
$this->random,
$this->logger,
$this->dispatcher,
- $this->legacyDispatcher,
- $this->config
+ $this->config,
+ $this->sharingBackend,
+ false,
);
$this->cleanUpBackend();
@@ -132,7 +119,7 @@ abstract class AbstractCalDavBackend extends TestCase {
parent::tearDown();
}
- public function cleanUpBackend() {
+ public function cleanUpBackend(): void {
if (is_null($this->backend)) {
return;
}
@@ -147,8 +134,6 @@ abstract class AbstractCalDavBackend extends TestCase {
$calendars = $this->backend->getCalendarsForUser($principal);
$this->dispatcher->expects(self::any())
->method('dispatchTyped');
- $this->legacyDispatcher->expects(self::any())
- ->method('dispatch');
foreach ($calendars as $calendar) {
$this->backend->deleteCalendar($calendar['id'], true);
}
@@ -158,7 +143,7 @@ abstract class AbstractCalDavBackend extends TestCase {
}
}
- protected function createTestCalendar() {
+ protected function createTestCalendar(): int {
$this->dispatcher->expects(self::any())
->method('dispatchTyped');
@@ -170,14 +155,12 @@ abstract class AbstractCalDavBackend extends TestCase {
$this->assertEquals(self::UNIT_TEST_USER, $calendars[0]['principaluri']);
/** @var SupportedCalendarComponentSet $components */
$components = $calendars[0]['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'];
- $this->assertEquals(['VEVENT','VTODO'], $components->getValue());
+ $this->assertEquals(['VEVENT','VTODO','VJOURNAL'], $components->getValue());
$color = $calendars[0]['{http://apple.com/ns/ical/}calendar-color'];
$this->assertEquals('#1C4587FF', $color);
$this->assertEquals('Example', $calendars[0]['uri']);
$this->assertEquals('Example', $calendars[0]['{DAV:}displayname']);
- $calendarId = $calendars[0]['id'];
-
- return $calendarId;
+ return (int)$calendars[0]['id'];
}
protected function createTestSubscription() {
@@ -223,6 +206,33 @@ EOD;
return $uri0;
}
+ protected function modifyEvent($calendarId, $objectId, $start = '20130912T130000Z', $end = '20130912T140000Z') {
+ $randomPart = self::getUniqueID();
+
+ $calData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:ownCloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8-$randomPart
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:$start
+DTEND;VALUE=DATE-TIME:$end
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ $this->backend->updateCalendarObject($calendarId, $objectId, $calData);
+ }
+
+ protected function deleteEvent($calendarId, $objectId) {
+ $this->backend->deleteCalendarObject($calendarId, $objectId);
+ }
+
protected function assertAcl($principal, $privilege, $acl) {
foreach ($acl as $a) {
if ($a['principal'] === $principal && $a['privilege'] === $privilege) {
diff --git a/apps/dav/tests/unit/CalDAV/Activity/BackendTest.php b/apps/dav/tests/unit/CalDAV/Activity/BackendTest.php
index 21cbbd169ff..4848a01f6b9 100644
--- a/apps/dav/tests/unit/CalDAV/Activity/BackendTest.php
+++ b/apps/dav/tests/unit/CalDAV/Activity/BackendTest.php
@@ -1,27 +1,9 @@
<?php
+
+declare(strict_types=1);
/**
- * @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\Tests\unit\CalDAV\Activity;
@@ -33,23 +15,17 @@ use OCP\App\IAppManager;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IUser;
+use OCP\IUserManager;
use OCP\IUserSession;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class BackendTest extends TestCase {
-
- /** @var IManager|MockObject */
- protected $activityManager;
-
- /** @var IGroupManager|MockObject */
- protected $groupManager;
-
- /** @var IUserSession|MockObject */
- protected $userSession;
-
- /** @var IAppManager|MockObject */
- protected $appManager;
+ protected IManager&MockObject $activityManager;
+ protected IGroupManager&MockObject $groupManager;
+ protected IUserSession&MockObject $userSession;
+ protected IAppManager&MockObject $appManager;
+ protected IUserManager&MockObject $userManager;
protected function setUp(): void {
parent::setUp();
@@ -57,19 +33,20 @@ class BackendTest extends TestCase {
$this->groupManager = $this->createMock(IGroupManager::class);
$this->userSession = $this->createMock(IUserSession::class);
$this->appManager = $this->createMock(IAppManager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
}
/**
- * @param array $methods
- * @return Backend|MockObject
+ * @return Backend|(Backend&MockObject)
*/
- protected function getBackend(array $methods = []) {
+ protected function getBackend(array $methods = []): Backend {
if (empty($methods)) {
return new Backend(
$this->activityManager,
$this->groupManager,
$this->userSession,
- $this->appManager
+ $this->appManager,
+ $this->userManager
);
} else {
return $this->getMockBuilder(Backend::class)
@@ -78,13 +55,14 @@ class BackendTest extends TestCase {
$this->groupManager,
$this->userSession,
$this->appManager,
+ $this->userManager
])
- ->setMethods($methods)
+ ->onlyMethods($methods)
->getMock();
}
}
- public function dataCallTriggerCalendarActivity() {
+ public static function dataCallTriggerCalendarActivity(): array {
return [
['onCalendarAdd', [['data']], Calendar::SUBJECT_ADD, [['data'], [], []]],
['onCalendarUpdate', [['data'], ['shares'], ['changed-properties']], Calendar::SUBJECT_UPDATE, [['data'], ['shares'], ['changed-properties']]],
@@ -93,19 +71,12 @@ class BackendTest extends TestCase {
];
}
- /**
- * @dataProvider dataCallTriggerCalendarActivity
- *
- * @param string $method
- * @param array $payload
- * @param string $expectedSubject
- * @param array $expectedPayload
- */
- public function testCallTriggerCalendarActivity($method, array $payload, $expectedSubject, array $expectedPayload) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataCallTriggerCalendarActivity')]
+ public function testCallTriggerCalendarActivity(string $method, array $payload, string $expectedSubject, array $expectedPayload): void {
$backend = $this->getBackend(['triggerCalendarActivity']);
$backend->expects($this->once())
->method('triggerCalendarActivity')
- ->willReturnCallback(function () use ($expectedPayload, $expectedSubject) {
+ ->willReturnCallback(function () use ($expectedPayload, $expectedSubject): void {
$arguments = func_get_args();
$this->assertSame($expectedSubject, array_shift($arguments));
$this->assertEquals($expectedPayload, $arguments);
@@ -114,7 +85,7 @@ class BackendTest extends TestCase {
call_user_func_array([$backend, $method], $payload);
}
- public function dataTriggerCalendarActivity() {
+ public static function dataTriggerCalendarActivity(): array {
return [
// Add calendar
[Calendar::SUBJECT_ADD, [], [], [], '', '', null, []],
@@ -195,18 +166,8 @@ class BackendTest extends TestCase {
];
}
- /**
- * @dataProvider dataTriggerCalendarActivity
- * @param string $action
- * @param array $data
- * @param array $shares
- * @param array $changedProperties
- * @param string $currentUser
- * @param string $author
- * @param string[]|null $shareUsers
- * @param string[] $users
- */
- public function testTriggerCalendarActivity($action, array $data, array $shares, array $changedProperties, $currentUser, $author, $shareUsers, array $users) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTriggerCalendarActivity')]
+ public function testTriggerCalendarActivity(string $action, array $data, array $shares, array $changedProperties, string $currentUser, string $author, ?array $shareUsers, array $users): void {
$backend = $this->getBackend(['getUsersForShares']);
if ($shareUsers === null) {
@@ -252,6 +213,10 @@ class BackendTest extends TestCase {
->with($author)
->willReturnSelf();
+ $this->userManager->expects($action === Calendar::SUBJECT_DELETE ? $this->exactly(sizeof($users)) : $this->never())
+ ->method('userExists')
+ ->willReturn(true);
+
$event->expects($this->exactly(sizeof($users)))
->method('setAffectedUser')
->willReturnSelf();
@@ -269,7 +234,25 @@ class BackendTest extends TestCase {
$this->invokePrivate($backend, 'triggerCalendarActivity', [$action, $data, $shares, $changedProperties]);
}
- public function dataGetUsersForShares() {
+ public function testUserDeletionDoesNotCreateActivity(): void {
+ $backend = $this->getBackend();
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->willReturn(false);
+
+ $this->activityManager->expects($this->never())
+ ->method('publish');
+
+ $this->invokePrivate($backend, 'triggerCalendarActivity', [Calendar::SUBJECT_DELETE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of calendar',
+ ], [], []]);
+ }
+
+ public static function dataGetUsersForShares(): array {
return [
[
[],
@@ -312,13 +295,8 @@ class BackendTest extends TestCase {
];
}
- /**
- * @dataProvider dataGetUsersForShares
- * @param array $shares
- * @param array $groups
- * @param array $expected
- */
- public function testGetUsersForShares(array $shares, array $groups, array $expected) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataGetUsersForShares')]
+ public function testGetUsersForShares(array $shares, array $groups, array $expected): void {
$backend = $this->getBackend();
$getGroups = [];
@@ -347,7 +325,7 @@ class BackendTest extends TestCase {
/**
* @param string[] $users
- * @return IUser[]|MockObject[]
+ * @return IUser[]&MockObject[]
*/
protected function getUsers(array $users) {
$list = [];
@@ -359,7 +337,7 @@ class BackendTest extends TestCase {
/**
* @param string $uid
- * @return IUser|MockObject
+ * @return IUser&MockObject
*/
protected function getUserMock($uid) {
$user = $this->createMock(IUser::class);
diff --git a/apps/dav/tests/unit/CalDAV/Activity/Filter/CalendarTest.php b/apps/dav/tests/unit/CalDAV/Activity/Filter/CalendarTest.php
index de31c71aacb..b4c4e14fe7d 100644
--- a/apps/dav/tests/unit/CalDAV/Activity/Filter/CalendarTest.php
+++ b/apps/dav/tests/unit/CalDAV/Activity/Filter/CalendarTest.php
@@ -1,28 +1,9 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @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\Tests\unit\CalDAV\Activity\Filter;
@@ -30,15 +11,12 @@ use OCA\DAV\CalDAV\Activity\Filter\Calendar;
use OCP\Activity\IFilter;
use OCP\IL10N;
use OCP\IURLGenerator;
+use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class CalendarTest extends TestCase {
-
- /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
- protected $url;
-
- /** @var IFilter|\PHPUnit\Framework\MockObject\MockObject */
- protected $filter;
+ protected IURLGenerator&MockObject $url;
+ protected IFilter $filter;
protected function setUp(): void {
parent::setUp();
@@ -55,7 +33,7 @@ class CalendarTest extends TestCase {
);
}
- public function testGetIcon() {
+ public function testGetIcon(): void {
$this->url->expects($this->once())
->method('imagePath')
->with('core', 'places/calendar.svg')
@@ -69,7 +47,7 @@ class CalendarTest extends TestCase {
$this->assertEquals('absolute-path-to-icon', $this->filter->getIcon());
}
- public function dataFilterTypes() {
+ public static function dataFilterTypes(): array {
return [
[[], []],
[['calendar', 'calendar_event'], ['calendar', 'calendar_event']],
@@ -79,11 +57,11 @@ class CalendarTest extends TestCase {
}
/**
- * @dataProvider dataFilterTypes
* @param string[] $types
* @param string[] $expected
*/
- public function testFilterTypes($types, $expected) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataFilterTypes')]
+ public function testFilterTypes(array $types, array $expected): void {
$this->assertEquals($expected, $this->filter->filterTypes($types));
}
}
diff --git a/apps/dav/tests/unit/CalDAV/Activity/Filter/GenericTest.php b/apps/dav/tests/unit/CalDAV/Activity/Filter/GenericTest.php
index af4c87c354b..87b55f14bcc 100644
--- a/apps/dav/tests/unit/CalDAV/Activity/Filter/GenericTest.php
+++ b/apps/dav/tests/unit/CalDAV/Activity/Filter/GenericTest.php
@@ -1,114 +1,78 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016 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: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Tests\unit\CalDAV\Activity\Filter;
use OCA\DAV\CalDAV\Activity\Filter\Calendar;
use OCA\DAV\CalDAV\Activity\Filter\Todo;
use OCP\Activity\IFilter;
+use OCP\Server;
use Test\TestCase;
/**
* @group DB
*/
class GenericTest extends TestCase {
- public function dataFilters() {
+ public static function dataFilters(): array {
return [
[Calendar::class],
[Todo::class],
];
}
- /**
- * @dataProvider dataFilters
- * @param string $filterClass
- */
- public function testImplementsInterface($filterClass) {
- $filter = \OC::$server->query($filterClass);
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataFilters')]
+ public function testImplementsInterface(string $filterClass): void {
+ $filter = Server::get($filterClass);
$this->assertInstanceOf(IFilter::class, $filter);
}
- /**
- * @dataProvider dataFilters
- * @param string $filterClass
- */
- public function testGetIdentifier($filterClass) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataFilters')]
+ public function testGetIdentifier(string $filterClass): void {
/** @var IFilter $filter */
- $filter = \OC::$server->query($filterClass);
+ $filter = Server::get($filterClass);
$this->assertIsString($filter->getIdentifier());
}
- /**
- * @dataProvider dataFilters
- * @param string $filterClass
- */
- public function testGetName($filterClass) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataFilters')]
+ public function testGetName(string $filterClass): void {
/** @var IFilter $filter */
- $filter = \OC::$server->query($filterClass);
+ $filter = Server::get($filterClass);
$this->assertIsString($filter->getName());
}
- /**
- * @dataProvider dataFilters
- * @param string $filterClass
- */
- public function testGetPriority($filterClass) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataFilters')]
+ public function testGetPriority(string $filterClass): void {
/** @var IFilter $filter */
- $filter = \OC::$server->query($filterClass);
+ $filter = Server::get($filterClass);
$priority = $filter->getPriority();
$this->assertIsInt($filter->getPriority());
$this->assertGreaterThanOrEqual(0, $priority);
$this->assertLessThanOrEqual(100, $priority);
}
- /**
- * @dataProvider dataFilters
- * @param string $filterClass
- */
- public function testGetIcon($filterClass) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataFilters')]
+ public function testGetIcon(string $filterClass): void {
/** @var IFilter $filter */
- $filter = \OC::$server->query($filterClass);
+ $filter = Server::get($filterClass);
$this->assertIsString($filter->getIcon());
$this->assertStringStartsWith('http', $filter->getIcon());
}
- /**
- * @dataProvider dataFilters
- * @param string $filterClass
- */
- public function testFilterTypes($filterClass) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataFilters')]
+ public function testFilterTypes(string $filterClass): void {
/** @var IFilter $filter */
- $filter = \OC::$server->query($filterClass);
+ $filter = Server::get($filterClass);
$this->assertIsArray($filter->filterTypes([]));
}
- /**
- * @dataProvider dataFilters
- * @param string $filterClass
- */
- public function testAllowedApps($filterClass) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataFilters')]
+ public function testAllowedApps(string $filterClass): void {
/** @var IFilter $filter */
- $filter = \OC::$server->query($filterClass);
+ $filter = Server::get($filterClass);
$this->assertIsArray($filter->allowedApps());
}
}
diff --git a/apps/dav/tests/unit/CalDAV/Activity/Filter/TodoTest.php b/apps/dav/tests/unit/CalDAV/Activity/Filter/TodoTest.php
index e119064046b..f18d66b9774 100644
--- a/apps/dav/tests/unit/CalDAV/Activity/Filter/TodoTest.php
+++ b/apps/dav/tests/unit/CalDAV/Activity/Filter/TodoTest.php
@@ -1,27 +1,9 @@
<?php
+
+declare(strict_types=1);
/**
- * @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>
- * @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\Tests\unit\CalDAV\Activity\Filter;
@@ -29,15 +11,12 @@ use OCA\DAV\CalDAV\Activity\Filter\Todo;
use OCP\Activity\IFilter;
use OCP\IL10N;
use OCP\IURLGenerator;
+use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class TodoTest extends TestCase {
-
- /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
- protected $url;
-
- /** @var IFilter|\PHPUnit\Framework\MockObject\MockObject */
- protected $filter;
+ protected IURLGenerator&MockObject $url;
+ protected IFilter $filter;
protected function setUp(): void {
parent::setUp();
@@ -54,7 +33,7 @@ class TodoTest extends TestCase {
);
}
- public function testGetIcon() {
+ public function testGetIcon(): void {
$this->url->expects($this->once())
->method('imagePath')
->with('core', 'actions/checkmark.svg')
@@ -68,7 +47,7 @@ class TodoTest extends TestCase {
$this->assertEquals('absolute-path-to-icon', $this->filter->getIcon());
}
- public function dataFilterTypes() {
+ public static function dataFilterTypes(): array {
return [
[[], []],
[['calendar_todo'], ['calendar_todo']],
@@ -78,11 +57,11 @@ class TodoTest extends TestCase {
}
/**
- * @dataProvider dataFilterTypes
* @param string[] $types
* @param string[] $expected
*/
- public function testFilterTypes($types, $expected) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataFilterTypes')]
+ public function testFilterTypes(array $types, array $expected): void {
$this->assertEquals($expected, $this->filter->filterTypes($types));
}
}
diff --git a/apps/dav/tests/unit/CalDAV/Activity/Provider/BaseTest.php b/apps/dav/tests/unit/CalDAV/Activity/Provider/BaseTest.php
index 21cafee4f88..3e6219beef8 100644
--- a/apps/dav/tests/unit/CalDAV/Activity/Provider/BaseTest.php
+++ b/apps/dav/tests/unit/CalDAV/Activity/Provider/BaseTest.php
@@ -1,55 +1,26 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @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>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have 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\Tests\unit\CalDAV\Activity\Provider;
use OCA\DAV\CalDAV\Activity\Provider\Base;
use OCP\Activity\IEvent;
-use OCP\Activity\IProvider;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IURLGenerator;
-use OCP\IUser;
use OCP\IUserManager;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class BaseTest extends TestCase {
-
- /** @var IUserManager|MockObject */
- protected $userManager;
-
- /** @var IGroupManager|MockObject */
- protected $groupManager;
-
- /** @var IURLGenerator|MockObject */
- protected $url;
-
- /** @var IProvider|Base|MockObject */
- protected $provider;
+ protected IUserManager&MockObject $userManager;
+ protected IGroupManager&MockObject $groupManager;
+ protected IURLGenerator&MockObject $url;
+ protected Base&MockObject $provider;
protected function setUp(): void {
parent::setUp();
@@ -62,38 +33,31 @@ class BaseTest extends TestCase {
$this->groupManager,
$this->url,
])
- ->setMethods(['parse'])
+ ->onlyMethods(['parse'])
->getMock();
}
- public function dataSetSubjects() {
+ public static function dataSetSubjects(): array {
return [
- ['abc', [], 'abc'],
- ['{actor} created {calendar}', ['actor' => ['name' => 'abc'], 'calendar' => ['name' => 'xyz']], 'abc created xyz'],
+ ['abc', []],
+ ['{actor} created {calendar}', ['actor' => ['name' => 'abc'], 'calendar' => ['name' => 'xyz']]],
];
}
- /**
- * @dataProvider dataSetSubjects
- * @param string $subject
- * @param array $parameters
- * @param string $parsedSubject
- */
- public function testSetSubjects(string $subject, array $parameters, string $parsedSubject) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSetSubjects')]
+ public function testSetSubjects(string $subject, array $parameters): void {
$event = $this->createMock(IEvent::class);
$event->expects($this->once())
->method('setRichSubject')
->with($subject, $parameters)
->willReturnSelf();
- $event->expects($this->once())
- ->method('setParsedSubject')
- ->with($parsedSubject)
- ->willReturnSelf();
+ $event->expects($this->never())
+ ->method('setParsedSubject');
$this->invokePrivate($this->provider, 'setSubjects', [$event, $subject, $parameters]);
}
- public function dataGenerateCalendarParameter() {
+ public static function dataGenerateCalendarParameter(): array {
return [
[['id' => 23, 'uri' => 'foo', 'name' => 'bar'], 'bar'],
[['id' => 42, 'uri' => 'foo', 'name' => 'Personal'], 'Personal'],
@@ -102,12 +66,8 @@ class BaseTest extends TestCase {
];
}
- /**
- * @dataProvider dataGenerateCalendarParameter
- * @param array $data
- * @param string $name
- */
- public function testGenerateCalendarParameter(array $data, string $name) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataGenerateCalendarParameter')]
+ public function testGenerateCalendarParameter(array $data, string $name): void {
$l = $this->createMock(IL10N::class);
$l->expects($this->any())
->method('t')
@@ -122,19 +82,15 @@ class BaseTest extends TestCase {
], $this->invokePrivate($this->provider, 'generateCalendarParameter', [$data, $l]));
}
- public function dataGenerateLegacyCalendarParameter() {
+ public static function dataGenerateLegacyCalendarParameter(): array {
return [
[23, 'c1'],
[42, 'c2'],
];
}
- /**
- * @dataProvider dataGenerateLegacyCalendarParameter
- * @param int $id
- * @param string $name
- */
- public function testGenerateLegacyCalendarParameter(int $id, string $name) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataGenerateLegacyCalendarParameter')]
+ public function testGenerateLegacyCalendarParameter(int $id, string $name): void {
$this->assertEquals([
'type' => 'calendar',
'id' => $id,
@@ -142,59 +98,19 @@ class BaseTest extends TestCase {
], $this->invokePrivate($this->provider, 'generateLegacyCalendarParameter', [$id, $name]));
}
- public function dataGenerateGroupParameter() {
+ public static function dataGenerateGroupParameter(): array {
return [
['g1'],
['g2'],
];
}
- /**
- * @dataProvider dataGenerateGroupParameter
- * @param string $gid
- */
- public function testGenerateGroupParameter(string $gid) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataGenerateGroupParameter')]
+ public function testGenerateGroupParameter(string $gid): void {
$this->assertEquals([
'type' => 'user-group',
'id' => $gid,
'name' => $gid,
], $this->invokePrivate($this->provider, 'generateGroupParameter', [$gid]));
}
-
- public function dataGenerateUserParameter() {
- $u1 = $this->createMock(IUser::class);
- $u1->expects($this->any())
- ->method('getDisplayName')
- ->willReturn('User 1');
- return [
- ['u1', 'User 1', $u1],
- ['u2', 'u2', null],
- ];
- }
-
- /**
- * @dataProvider dataGenerateUserParameter
- * @param string $uid
- * @param string $displayName
- * @param IUser|null $user
- */
- public function testGenerateUserParameter(string $uid, string $displayName, ?IUser $user) {
- $this->userManager->expects($this->once())
- ->method('get')
- ->with($uid)
- ->willReturn($user);
-
- $this->assertEquals([
- 'type' => 'user',
- 'id' => $uid,
- 'name' => $displayName,
- ], $this->invokePrivate($this->provider, 'generateUserParameter', [$uid]));
-
- // Test caching (only 1 user manager invocation allowed)
- $this->assertEquals([
- 'type' => 'user',
- 'id' => $uid,
- 'name' => $displayName,
- ], $this->invokePrivate($this->provider, 'generateUserParameter', [$uid]));
- }
}
diff --git a/apps/dav/tests/unit/CalDAV/Activity/Provider/EventTest.php b/apps/dav/tests/unit/CalDAV/Activity/Provider/EventTest.php
index cb86b80d311..4fd38c1aed2 100644
--- a/apps/dav/tests/unit/CalDAV/Activity/Provider/EventTest.php
+++ b/apps/dav/tests/unit/CalDAV/Activity/Provider/EventTest.php
@@ -1,33 +1,16 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020 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: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Tests\unit\CalDAV\Activity\Provider;
use InvalidArgumentException;
-use OCA\DAV\CalDAV\Activity\Provider\Base;
use OCA\DAV\CalDAV\Activity\Provider\Event;
use OCP\Activity\IEventMerger;
use OCP\Activity\IManager;
-use OCP\Activity\IProvider;
use OCP\App\IAppManager;
use OCP\IGroupManager;
use OCP\IURLGenerator;
@@ -38,30 +21,14 @@ use Test\TestCase;
use TypeError;
class EventTest extends TestCase {
-
- /** @var IUserManager|MockObject */
- protected $userManager;
-
- /** @var IGroupManager|MockObject */
- protected $groupManager;
-
- /** @var IURLGenerator|MockObject */
- protected $url;
-
- /** @var IProvider|Base|MockObject */
- protected $provider;
-
- /** @var IAppManager|MockObject */
- protected $appManager;
-
- /** @var IFactory|MockObject */
- protected $i10nFactory;
-
- /** @var IManager|MockObject */
- protected $activityManager;
-
- /** @var IEventMerger|MockObject */
- protected $eventMerger;
+ protected IUserManager&MockObject $userManager;
+ protected IGroupManager&MockObject $groupManager;
+ protected IURLGenerator&MockObject $url;
+ protected IAppManager&MockObject $appManager;
+ protected IFactory&MockObject $i10nFactory;
+ protected IManager&MockObject $activityManager;
+ protected IEventMerger&MockObject $eventMerger;
+ protected Event&MockObject $provider;
protected function setUp(): void {
parent::setUp();
@@ -82,11 +49,11 @@ class EventTest extends TestCase {
$this->eventMerger,
$this->appManager
])
- ->setMethods(['parse'])
+ ->onlyMethods(['parse'])
->getMock();
}
- public function dataGenerateObjectParameter() {
+ public static function dataGenerateObjectParameter(): array {
$link = [
'object_uri' => 'someuuid.ics',
'calendar_uri' => 'personal',
@@ -100,21 +67,13 @@ class EventTest extends TestCase {
];
}
- /**
- * @dataProvider dataGenerateObjectParameter
- * @param int $id
- * @param string $name
- * @param array|null $link
- * @param bool $calendarAppEnabled
- */
- public function testGenerateObjectParameter(int $id, string $name, ?array $link, bool $calendarAppEnabled = true) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataGenerateObjectParameter')]
+ public function testGenerateObjectParameter(int $id, string $name, ?array $link, bool $calendarAppEnabled = true): void {
+ $affectedUser = 'otheruser';
if ($link) {
+ $affectedUser = $link['owner'];
$generatedLink = [
- 'view' => 'dayGridMonth',
- 'timeRange' => 'now',
- 'mode' => 'sidebar',
'objectId' => base64_encode('/remote.php/dav/calendars/' . $link['owner'] . '/' . $link['calendar_uri'] . '/' . $link['object_uri']),
- 'recurrenceId' => 'next'
];
$this->appManager->expects($this->once())
->method('isEnabledForUser')
@@ -122,8 +81,10 @@ class EventTest extends TestCase {
->willReturn($calendarAppEnabled);
if ($calendarAppEnabled) {
$this->url->expects($this->once())
+ ->method('getWebroot');
+ $this->url->expects($this->once())
->method('linkToRouteAbsolute')
- ->with('calendar.view.indexview.timerange.edit', $generatedLink)
+ ->with('calendar.view.indexdirect.edit', $generatedLink)
->willReturn('fullLink');
}
}
@@ -139,10 +100,80 @@ class EventTest extends TestCase {
if ($link && $calendarAppEnabled) {
$result['link'] = 'fullLink';
}
- $this->assertEquals($result, $this->invokePrivate($this->provider, 'generateObjectParameter', [$objectParameter]));
+ $this->assertEquals($result, $this->invokePrivate($this->provider, 'generateObjectParameter', [$objectParameter, $affectedUser]));
+ }
+
+ public static function generateObjectParameterLinkEncodingDataProvider(): array {
+ return [
+ [ // Shared calendar
+ [
+ 'object_uri' => 'someuuid.ics',
+ 'calendar_uri' => 'personal',
+ 'owner' => 'sharer'
+ ],
+ base64_encode('/remote.php/dav/calendars/sharee/personal_shared_by_sharer/someuuid.ics'),
+ ],
+ [ // Shared calendar with umlauts
+ [
+ 'object_uri' => 'someuuid.ics',
+ 'calendar_uri' => 'umlaut_äüöß',
+ 'owner' => 'sharer'
+ ],
+ base64_encode('/remote.php/dav/calendars/sharee/umlaut_%c3%a4%c3%bc%c3%b6%c3%9f_shared_by_sharer/someuuid.ics'),
+ ],
+ [ // Shared calendar with umlauts and mixed casing
+ [
+ 'object_uri' => 'someuuid.ics',
+ 'calendar_uri' => 'Umlaut_äüöß',
+ 'owner' => 'sharer'
+ ],
+ base64_encode('/remote.php/dav/calendars/sharee/Umlaut_%c3%a4%c3%bc%c3%b6%c3%9f_shared_by_sharer/someuuid.ics'),
+ ],
+ [ // Owned calendar with umlauts
+ [
+ 'object_uri' => 'someuuid.ics',
+ 'calendar_uri' => 'umlaut_äüöß',
+ 'owner' => 'sharee'
+ ],
+ base64_encode('/remote.php/dav/calendars/sharee/umlaut_%c3%a4%c3%bc%c3%b6%c3%9f/someuuid.ics'),
+ ],
+ [ // Owned calendar with umlauts and mixed casing
+ [
+ 'object_uri' => 'someuuid.ics',
+ 'calendar_uri' => 'Umlaut_äüöß',
+ 'owner' => 'sharee'
+ ],
+ base64_encode('/remote.php/dav/calendars/sharee/Umlaut_%c3%a4%c3%bc%c3%b6%c3%9f/someuuid.ics'),
+ ],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('generateObjectParameterLinkEncodingDataProvider')]
+ public function testGenerateObjectParameterLinkEncoding(array $link, string $objectId): void {
+ $generatedLink = [
+ 'objectId' => $objectId,
+ ];
+ $this->appManager->expects($this->once())
+ ->method('isEnabledForUser')
+ ->with('calendar')
+ ->willReturn(true);
+ $this->url->expects($this->once())
+ ->method('getWebroot');
+ $this->url->expects($this->once())
+ ->method('linkToRouteAbsolute')
+ ->with('calendar.view.indexdirect.edit', $generatedLink)
+ ->willReturn('fullLink');
+ $objectParameter = ['id' => 42, 'name' => 'calendar', 'link' => $link];
+ $result = [
+ 'type' => 'calendar-event',
+ 'id' => 42,
+ 'name' => 'calendar',
+ 'link' => 'fullLink',
+ ];
+ $this->assertEquals($result, $this->invokePrivate($this->provider, 'generateObjectParameter', [$objectParameter, 'sharee']));
}
- public function dataGenerateObjectParameterThrows() {
+ public static function dataGenerateObjectParameterThrows(): array {
return [
['event', TypeError::class],
[['name' => 'event']],
@@ -150,14 +181,10 @@ class EventTest extends TestCase {
];
}
- /**
- * @dataProvider dataGenerateObjectParameterThrows
- * @param mixed $eventData
- * @param string $exception
- */
- public function testGenerateObjectParameterThrows($eventData, string $exception = InvalidArgumentException::class) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataGenerateObjectParameterThrows')]
+ public function testGenerateObjectParameterThrows(string|array $eventData, string $exception = InvalidArgumentException::class): void {
$this->expectException($exception);
- $this->invokePrivate($this->provider, 'generateObjectParameter', [$eventData]);
+ $this->invokePrivate($this->provider, 'generateObjectParameter', [$eventData, 'no_user']);
}
}
diff --git a/apps/dav/tests/unit/CalDAV/Activity/Setting/GenericTest.php b/apps/dav/tests/unit/CalDAV/Activity/Setting/GenericTest.php
index b4c3506565a..23126b6bbcf 100644
--- a/apps/dav/tests/unit/CalDAV/Activity/Setting/GenericTest.php
+++ b/apps/dav/tests/unit/CalDAV/Activity/Setting/GenericTest.php
@@ -1,25 +1,9 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016 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: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Tests\unit\CalDAV\Activity\Setting;
@@ -27,10 +11,11 @@ use OCA\DAV\CalDAV\Activity\Setting\Calendar;
use OCA\DAV\CalDAV\Activity\Setting\Event;
use OCA\DAV\CalDAV\Activity\Setting\Todo;
use OCP\Activity\ISetting;
+use OCP\Server;
use Test\TestCase;
class GenericTest extends TestCase {
- public function dataSettings() {
+ public static function dataSettings(): array {
return [
[Calendar::class],
[Event::class],
@@ -38,85 +23,61 @@ class GenericTest extends TestCase {
];
}
- /**
- * @dataProvider dataSettings
- * @param string $settingClass
- */
- public function testImplementsInterface($settingClass) {
- $setting = \OC::$server->query($settingClass);
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')]
+ public function testImplementsInterface(string $settingClass): void {
+ $setting = Server::get($settingClass);
$this->assertInstanceOf(ISetting::class, $setting);
}
- /**
- * @dataProvider dataSettings
- * @param string $settingClass
- */
- public function testGetIdentifier($settingClass) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')]
+ public function testGetIdentifier(string $settingClass): void {
/** @var ISetting $setting */
- $setting = \OC::$server->query($settingClass);
+ $setting = Server::get($settingClass);
$this->assertIsString($setting->getIdentifier());
}
- /**
- * @dataProvider dataSettings
- * @param string $settingClass
- */
- public function testGetName($settingClass) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')]
+ public function testGetName(string $settingClass): void {
/** @var ISetting $setting */
- $setting = \OC::$server->query($settingClass);
+ $setting = Server::get($settingClass);
$this->assertIsString($setting->getName());
}
- /**
- * @dataProvider dataSettings
- * @param string $settingClass
- */
- public function testGetPriority($settingClass) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')]
+ public function testGetPriority(string $settingClass): void {
/** @var ISetting $setting */
- $setting = \OC::$server->query($settingClass);
+ $setting = Server::get($settingClass);
$priority = $setting->getPriority();
$this->assertIsInt($setting->getPriority());
$this->assertGreaterThanOrEqual(0, $priority);
$this->assertLessThanOrEqual(100, $priority);
}
- /**
- * @dataProvider dataSettings
- * @param string $settingClass
- */
- public function testCanChangeStream($settingClass) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')]
+ public function testCanChangeStream(string $settingClass): void {
/** @var ISetting $setting */
- $setting = \OC::$server->query($settingClass);
+ $setting = Server::get($settingClass);
$this->assertIsBool($setting->canChangeStream());
}
- /**
- * @dataProvider dataSettings
- * @param string $settingClass
- */
- public function testIsDefaultEnabledStream($settingClass) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')]
+ public function testIsDefaultEnabledStream(string $settingClass): void {
/** @var ISetting $setting */
- $setting = \OC::$server->query($settingClass);
+ $setting = Server::get($settingClass);
$this->assertIsBool($setting->isDefaultEnabledStream());
}
- /**
- * @dataProvider dataSettings
- * @param string $settingClass
- */
- public function testCanChangeMail($settingClass) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')]
+ public function testCanChangeMail(string $settingClass): void {
/** @var ISetting $setting */
- $setting = \OC::$server->query($settingClass);
+ $setting = Server::get($settingClass);
$this->assertIsBool($setting->canChangeMail());
}
- /**
- * @dataProvider dataSettings
- * @param string $settingClass
- */
- public function testIsDefaultEnabledMail($settingClass) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')]
+ public function testIsDefaultEnabledMail(string $settingClass): void {
/** @var ISetting $setting */
- $setting = \OC::$server->query($settingClass);
+ $setting = Server::get($settingClass);
$this->assertIsBool($setting->isDefaultEnabledMail());
}
}
diff --git a/apps/dav/tests/unit/CalDAV/AppCalendar/AppCalendarTest.php b/apps/dav/tests/unit/CalDAV/AppCalendar/AppCalendarTest.php
new file mode 100644
index 00000000000..84879e87238
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/AppCalendar/AppCalendarTest.php
@@ -0,0 +1,120 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\AppCalendar;
+
+use OCA\DAV\CalDAV\AppCalendar\AppCalendar;
+use OCP\Calendar\ICalendar;
+use OCP\Calendar\ICreateFromString;
+use OCP\Constants;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+use function rewind;
+
+class AppCalendarTest extends TestCase {
+ private string $principal = 'principals/users/foo';
+
+ private AppCalendar $appCalendar;
+ private AppCalendar $writeableAppCalendar;
+
+ private ICalendar&MockObject $calendar;
+ private ICalendar&MockObject $writeableCalendar;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->calendar = $this->getMockBuilder(ICalendar::class)->getMock();
+ $this->calendar->method('getPermissions')
+ ->willReturn(Constants::PERMISSION_READ);
+
+ $this->writeableCalendar = $this->getMockBuilder(ICreateFromString::class)->getMock();
+ $this->writeableCalendar->method('getPermissions')
+ ->willReturn(Constants::PERMISSION_READ | Constants::PERMISSION_CREATE);
+
+ $this->appCalendar = new AppCalendar('dav-wrapper', $this->calendar, $this->principal);
+ $this->writeableAppCalendar = new AppCalendar('dav-wrapper', $this->writeableCalendar, $this->principal);
+ }
+
+ public function testGetPrincipal():void {
+ // Check that the correct name is returned
+ $this->assertEquals($this->principal, $this->appCalendar->getOwner());
+ $this->assertEquals($this->principal, $this->writeableAppCalendar->getOwner());
+ }
+
+ public function testDelete(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $this->expectExceptionMessage('Deleting an entry is not implemented');
+
+ $this->appCalendar->delete();
+ }
+
+ public function testCreateFile(): void {
+ $calls = [
+ ['some-name', 'data'],
+ ['other-name', ''],
+ ['name', 'some data'],
+ ];
+ $this->writeableCalendar->expects($this->exactly(3))
+ ->method('createFromString')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
+
+ // pass data
+ $this->assertNull($this->writeableAppCalendar->createFile('some-name', 'data'));
+ // null is empty string
+ $this->assertNull($this->writeableAppCalendar->createFile('other-name', null));
+ // resource to data
+ $fp = fopen('php://memory', 'r+');
+ fwrite($fp, 'some data');
+ rewind($fp);
+ $this->assertNull($this->writeableAppCalendar->createFile('name', $fp));
+ fclose($fp);
+ }
+
+ public function testCreateFile_readOnly(): void {
+ // If writing is not supported
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $this->expectExceptionMessage('Creating a new entry is not allowed');
+
+ $this->appCalendar->createFile('some-name', 'data');
+ }
+
+ public function testSetACL(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $this->expectExceptionMessage('Setting ACL is not supported on this node');
+
+ $this->appCalendar->setACL([]);
+ }
+
+ public function testGetACL():void {
+ $expectedRO = [
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->principal,
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}write-properties',
+ 'principal' => $this->principal,
+ 'protected' => true,
+ ]
+ ];
+ $expectedRW = $expectedRO;
+ $expectedRW[] = [
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->principal,
+ 'protected' => true,
+ ];
+
+ // Check that the correct ACL is returned (default be only readable)
+ $this->assertEquals($expectedRO, $this->appCalendar->getACL());
+ $this->assertEquals($expectedRW, $this->writeableAppCalendar->getACL());
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/AppCalendar/CalendarObjectTest.php b/apps/dav/tests/unit/CalDAV/AppCalendar/CalendarObjectTest.php
new file mode 100644
index 00000000000..3d72d5c97b8
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/AppCalendar/CalendarObjectTest.php
@@ -0,0 +1,170 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\AppCalendar;
+
+use OCA\DAV\CalDAV\AppCalendar\AppCalendar;
+use OCA\DAV\CalDAV\AppCalendar\CalendarObject;
+use OCP\Calendar\ICalendar;
+use OCP\Calendar\ICreateFromString;
+use OCP\Constants;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\VObject\Component\VCalendar;
+use Test\TestCase;
+
+class CalendarObjectTest extends TestCase {
+ private CalendarObject $calendarObject;
+ private AppCalendar&MockObject $calendar;
+ private ICalendar&MockObject $backend;
+ private VCalendar&MockObject $vobject;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->calendar = $this->createMock(AppCalendar::class);
+ $this->calendar->method('getOwner')->willReturn('owner');
+ $this->calendar->method('getGroup')->willReturn('group');
+
+ $this->backend = $this->createMock(ICalendar::class);
+ $this->vobject = $this->createMock(VCalendar::class);
+ $this->calendarObject = new CalendarObject($this->calendar, $this->backend, $this->vobject);
+ }
+
+ public function testGetOwner(): void {
+ $this->assertEquals($this->calendarObject->getOwner(), 'owner');
+ }
+
+ public function testGetGroup(): void {
+ $this->assertEquals($this->calendarObject->getGroup(), 'group');
+ }
+
+ public function testGetACL(): void {
+ $this->calendar->expects($this->exactly(2))
+ ->method('getPermissions')
+ ->willReturnOnConsecutiveCalls(Constants::PERMISSION_READ, Constants::PERMISSION_ALL);
+
+ // read only
+ $this->assertEquals($this->calendarObject->getACL(), [
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => 'owner',
+ 'protected' => true,
+ ]
+ ]);
+
+ // write permissions
+ $this->assertEquals($this->calendarObject->getACL(), [
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => 'owner',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}write-content',
+ 'principal' => 'owner',
+ 'protected' => true,
+ ]
+ ]);
+ }
+
+ public function testSetACL(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $this->calendarObject->setACL([]);
+ }
+
+ public function testPut_readOnlyBackend(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $this->calendarObject->put('foo');
+ }
+
+ public function testPut_noPermissions(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+
+ $backend = $this->createMock(ICreateFromString::class);
+ $calendarObject = new CalendarObject($this->calendar, $backend, $this->vobject);
+
+ $this->calendar->expects($this->once())
+ ->method('getPermissions')
+ ->willReturn(Constants::PERMISSION_READ);
+
+ $calendarObject->put('foo');
+ }
+
+ public function testPut(): void {
+ $backend = $this->createMock(ICreateFromString::class);
+ $calendarObject = new CalendarObject($this->calendar, $backend, $this->vobject);
+
+ $this->vobject->expects($this->once())
+ ->method('getBaseComponent')
+ ->willReturn((object)['UID' => 'someid']);
+ $this->calendar->expects($this->once())
+ ->method('getPermissions')
+ ->willReturn(Constants::PERMISSION_ALL);
+
+ $backend->expects($this->once())
+ ->method('createFromString')
+ ->with('someid.ics', 'foo');
+ $calendarObject->put('foo');
+ }
+
+ public function testGet(): void {
+ $this->vobject->expects($this->once())
+ ->method('serialize')
+ ->willReturn('foo');
+ $this->assertEquals($this->calendarObject->get(), 'foo');
+ }
+
+ public function testDelete_notWriteable(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $this->calendarObject->delete();
+ }
+
+ public function testDelete_noPermission(): void {
+ $backend = $this->createMock(ICreateFromString::class);
+ $calendarObject = new CalendarObject($this->calendar, $backend, $this->vobject);
+
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $calendarObject->delete();
+ }
+
+ public function testDelete(): void {
+ $backend = $this->createMock(ICreateFromString::class);
+ $calendarObject = new CalendarObject($this->calendar, $backend, $this->vobject);
+
+ $components = [(new VCalendar(['VEVENT' => ['UID' => 'someid']]))->getBaseComponent()];
+
+ $this->calendar->expects($this->once())
+ ->method('getPermissions')
+ ->willReturn(Constants::PERMISSION_DELETE);
+ $this->vobject->expects($this->once())
+ ->method('getBaseComponents')
+ ->willReturn($components);
+ $this->vobject->expects($this->once())
+ ->method('getBaseComponent')
+ ->willReturn($components[0]);
+
+ $backend->expects($this->once())
+ ->method('createFromString')
+ ->with('someid.ics', self::callback(fn ($data): bool => preg_match('/BEGIN:VEVENT(.|\r\n)+STATUS:CANCELLED/', $data) === 1));
+
+ $calendarObject->delete();
+ }
+
+ public function testGetName(): void {
+ $this->vobject->expects($this->exactly(2))
+ ->method('getBaseComponent')
+ ->willReturnOnConsecutiveCalls((object)['UID' => 'someid'], (object)['UID' => 'someid', 'X-FILENAME' => 'real-filename.ics']);
+
+ $this->assertEquals($this->calendarObject->getName(), 'someid.ics');
+ $this->assertEquals($this->calendarObject->getName(), 'real-filename.ics');
+ }
+
+ public function testSetName(): void {
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $this->calendarObject->setName('Some name');
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/BirthdayCalendar/EnablePluginTest.php b/apps/dav/tests/unit/CalDAV/BirthdayCalendar/EnablePluginTest.php
index 99e5f2e8e54..a5811271ce2 100644
--- a/apps/dav/tests/unit/CalDAV/BirthdayCalendar/EnablePluginTest.php
+++ b/apps/dav/tests/unit/CalDAV/BirthdayCalendar/EnablePluginTest.php
@@ -1,28 +1,9 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author François Freitag <mail@franek.fr>
- * @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\Tests\unit\CalDAV\BirthdayCalendar;
@@ -31,21 +12,16 @@ use OCA\DAV\CalDAV\BirthdayService;
use OCA\DAV\CalDAV\Calendar;
use OCA\DAV\CalDAV\CalendarHome;
use OCP\IConfig;
+use OCP\IUser;
+use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class EnablePluginTest extends TestCase {
-
- /** @var \Sabre\DAV\Server|\PHPUnit\Framework\MockObject\MockObject */
- protected $server;
-
- /** @var \OCP\IConfig|\PHPUnit\Framework\MockObject\MockObject */
- protected $config;
-
- /** @var BirthdayService |\PHPUnit\Framework\MockObject\MockObject */
- protected $birthdayService;
-
- /** @var \OCA\DAV\CalDAV\BirthdayCalendar\EnablePlugin $plugin */
- protected $plugin;
+ protected \Sabre\DAV\Server&MockObject $server;
+ protected IConfig&MockObject $config;
+ protected BirthdayService&MockObject $birthdayService;
+ protected IUser&MockObject $user;
+ protected EnablePlugin $plugin;
protected $request;
@@ -61,26 +37,27 @@ class EnablePluginTest extends TestCase {
$this->config = $this->createMock(IConfig::class);
$this->birthdayService = $this->createMock(BirthdayService::class);
+ $this->user = $this->createMock(IUser::class);
- $this->plugin = new EnablePlugin($this->config, $this->birthdayService);
+ $this->plugin = new EnablePlugin($this->config, $this->birthdayService, $this->user);
$this->plugin->initialize($this->server);
$this->request = $this->createMock(\Sabre\HTTP\RequestInterface::class);
$this->response = $this->createMock(\Sabre\HTTP\ResponseInterface::class);
}
- public function testGetFeatures() {
+ public function testGetFeatures(): void {
$this->assertEquals(['nc-enable-birthday-calendar'], $this->plugin->getFeatures());
}
- public function testGetName() {
+ public function testGetName(): void {
$this->assertEquals('nc-enable-birthday-calendar', $this->plugin->getPluginName());
}
- public function testInitialize() {
+ public function testInitialize(): void {
$server = $this->createMock(\Sabre\DAV\Server::class);
- $plugin = new EnablePlugin($this->config, $this->birthdayService);
+ $plugin = new EnablePlugin($this->config, $this->birthdayService, $this->user);
$server->expects($this->once())
->method('on')
@@ -89,7 +66,7 @@ class EnablePluginTest extends TestCase {
$plugin->initialize($server);
}
- public function testHttpPostNoCalendarHome() {
+ public function testHttpPostNoCalendarHome(): void {
$calendar = $this->createMock(Calendar::class);
$this->server->expects($this->once())
@@ -109,7 +86,7 @@ class EnablePluginTest extends TestCase {
$this->plugin->httpPost($this->request, $this->response);
}
- public function testHttpPostWrongRequest() {
+ public function testHttpPostWrongRequest(): void {
$calendarHome = $this->createMock(CalendarHome::class);
$this->server->expects($this->once())
@@ -130,7 +107,7 @@ class EnablePluginTest extends TestCase {
$this->server->xml->expects($this->once())
->method('parse')
- ->willReturnCallback(function ($requestBody, $url, &$documentType) {
+ ->willReturnCallback(function ($requestBody, $url, &$documentType): void {
$documentType = '{http://nextcloud.com/ns}disable-birthday-calendar';
});
@@ -143,7 +120,7 @@ class EnablePluginTest extends TestCase {
$this->plugin->httpPost($this->request, $this->response);
}
- public function testHttpPost() {
+ public function testHttpPostNotAuthorized(): void {
$calendarHome = $this->createMock(CalendarHome::class);
$this->server->expects($this->once())
@@ -168,10 +145,63 @@ class EnablePluginTest extends TestCase {
$this->server->xml->expects($this->once())
->method('parse')
- ->willReturnCallback(function ($requestBody, $url, &$documentType) {
+ ->willReturnCallback(function ($requestBody, $url, &$documentType): void {
$documentType = '{http://nextcloud.com/ns}enable-birthday-calendar';
});
+ $this->user->expects(self::once())
+ ->method('getUID')
+ ->willReturn('admin');
+
+ $this->server->httpResponse->expects($this->once())
+ ->method('setStatus')
+ ->with(403);
+
+ $this->config->expects($this->never())
+ ->method('setUserValue');
+
+ $this->birthdayService->expects($this->never())
+ ->method('syncUser');
+
+
+ $result = $this->plugin->httpPost($this->request, $this->response);
+
+ $this->assertEquals(false, $result);
+ }
+
+ public function testHttpPost(): void {
+ $calendarHome = $this->createMock(CalendarHome::class);
+
+ $this->server->expects($this->once())
+ ->method('getRequestUri')
+ ->willReturn('/bar/foo');
+ $this->server->tree->expects($this->once())
+ ->method('getNodeForPath')
+ ->with('/bar/foo')
+ ->willReturn($calendarHome);
+
+ $calendarHome->expects($this->once())
+ ->method('getOwner')
+ ->willReturn('principals/users/BlaBlub');
+
+ $this->request->expects($this->once())
+ ->method('getBodyAsString')
+ ->willReturn('<nc:enable-birthday-calendar xmlns:nc="http://nextcloud.com/ns"/>');
+
+ $this->request->expects($this->once())
+ ->method('getUrl')
+ ->willReturn('url_abc');
+
+ $this->server->xml->expects($this->once())
+ ->method('parse')
+ ->willReturnCallback(function ($requestBody, $url, &$documentType): void {
+ $documentType = '{http://nextcloud.com/ns}enable-birthday-calendar';
+ });
+
+ $this->user->expects(self::exactly(3))
+ ->method('getUID')
+ ->willReturn('BlaBlub');
+
$this->config->expects($this->once())
->method('setUserValue')
->with('BlaBlub', 'dav', 'generateBirthdayCalendar', 'yes');
diff --git a/apps/dav/tests/unit/CalDAV/CachedSubscriptionImplTest.php b/apps/dav/tests/unit/CalDAV/CachedSubscriptionImplTest.php
new file mode 100644
index 00000000000..935d8314f29
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/CachedSubscriptionImplTest.php
@@ -0,0 +1,80 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use OCA\DAV\CalDAV\CachedSubscription;
+use OCA\DAV\CalDAV\CachedSubscriptionImpl;
+use OCA\DAV\CalDAV\CalDavBackend;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class CachedSubscriptionImplTest extends TestCase {
+ private CachedSubscription&MockObject $cachedSubscription;
+ private array $cachedSubscriptionInfo;
+ private CalDavBackend&MockObject $backend;
+ private CachedSubscriptionImpl $cachedSubscriptionImpl;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->cachedSubscription = $this->createMock(CachedSubscription::class);
+ $this->cachedSubscriptionInfo = [
+ 'id' => 'fancy_id_123',
+ '{DAV:}displayname' => 'user readable name 123',
+ '{http://apple.com/ns/ical/}calendar-color' => '#AABBCC',
+ 'uri' => '/this/is/a/uri',
+ 'source' => 'https://test.localhost/calendar1',
+ ];
+ $this->backend = $this->createMock(CalDavBackend::class);
+
+ $this->cachedSubscriptionImpl = new CachedSubscriptionImpl(
+ $this->cachedSubscription,
+ $this->cachedSubscriptionInfo,
+ $this->backend
+ );
+ }
+
+ public function testGetKey(): void {
+ $this->assertEquals($this->cachedSubscriptionImpl->getKey(), 'fancy_id_123');
+ }
+
+ public function testGetDisplayname(): void {
+ $this->assertEquals($this->cachedSubscriptionImpl->getDisplayName(), 'user readable name 123');
+ }
+
+ public function testGetDisplayColor(): void {
+ $this->assertEquals($this->cachedSubscriptionImpl->getDisplayColor(), '#AABBCC');
+ }
+
+ public function testGetSource(): void {
+ $this->assertEquals($this->cachedSubscriptionImpl->getSource(), 'https://test.localhost/calendar1');
+ }
+
+ public function testSearch(): void {
+ $this->backend->expects($this->once())
+ ->method('search')
+ ->with($this->cachedSubscriptionInfo, 'abc', ['def'], ['ghi'], 42, 1337)
+ ->willReturn(['SEARCHRESULTS']);
+
+ $result = $this->cachedSubscriptionImpl->search('abc', ['def'], ['ghi'], 42, 1337);
+ $this->assertEquals($result, ['SEARCHRESULTS']);
+ }
+
+ public function testGetPermissionRead(): void {
+ $this->cachedSubscription->expects($this->once())
+ ->method('getACL')
+ ->with()
+ ->willReturn([
+ ['privilege' => '{DAV:}read']
+ ]);
+
+ $this->assertEquals(1, $this->cachedSubscriptionImpl->getPermissions());
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/CachedSubscriptionObjectTest.php b/apps/dav/tests/unit/CalDAV/CachedSubscriptionObjectTest.php
index 74e3784aefa..03a2c9f20ee 100644
--- a/apps/dav/tests/unit/CalDAV/CachedSubscriptionObjectTest.php
+++ b/apps/dav/tests/unit/CalDAV/CachedSubscriptionObjectTest.php
@@ -1,26 +1,9 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018 Georg Ehrke
- *
- * @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\Tests\unit\CalDAV;
@@ -28,7 +11,7 @@ use OCA\DAV\CalDAV\CachedSubscriptionObject;
use OCA\DAV\CalDAV\CalDavBackend;
class CachedSubscriptionObjectTest extends \Test\TestCase {
- public function testGet() {
+ public function testGet(): void {
$backend = $this->createMock(CalDavBackend::class);
$calendarInfo = [
'{http://owncloud.org/ns}owner-principal' => 'user1',
@@ -51,8 +34,8 @@ class CachedSubscriptionObjectTest extends \Test\TestCase {
$this->assertEquals('BEGIN...', $calendarObject->get());
}
-
- public function testPut() {
+
+ public function testPut(): void {
$this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class);
$this->expectExceptionMessage('Creating objects in a cached subscription is not allowed');
@@ -71,8 +54,8 @@ class CachedSubscriptionObjectTest extends \Test\TestCase {
$calendarObject->put('');
}
-
- public function testDelete() {
+
+ public function testDelete(): void {
$this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class);
$this->expectExceptionMessage('Deleting objects in a cached subscription is not allowed');
diff --git a/apps/dav/tests/unit/CalDAV/CachedSubscriptionProviderTest.php b/apps/dav/tests/unit/CalDAV/CachedSubscriptionProviderTest.php
new file mode 100644
index 00000000000..58d5ca7835c
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/CachedSubscriptionProviderTest.php
@@ -0,0 +1,72 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use OCA\DAV\CalDAV\CachedSubscriptionImpl;
+use OCA\DAV\CalDAV\CachedSubscriptionProvider;
+use OCA\DAV\CalDAV\CalDavBackend;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class CachedSubscriptionProviderTest extends TestCase {
+
+ private CalDavBackend&MockObject $backend;
+ private CachedSubscriptionProvider $provider;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->backend = $this->createMock(CalDavBackend::class);
+ $this->backend
+ ->expects(self::once())
+ ->method('getSubscriptionsForUser')
+ ->with('user-principal-123')
+ ->willReturn([
+ [
+ 'id' => 'subscription-1',
+ 'uri' => 'subscription-1',
+ 'principaluris' => 'user-principal-123',
+ 'source' => 'https://localhost/subscription-1',
+ // A subscription array has actually more properties.
+ ],
+ [
+ 'id' => 'subscription-2',
+ 'uri' => 'subscription-2',
+ 'principaluri' => 'user-principal-123',
+ 'source' => 'https://localhost/subscription-2',
+ // A subscription array has actually more properties.
+ ]
+ ]);
+
+ $this->provider = new CachedSubscriptionProvider($this->backend);
+ }
+
+ public function testGetCalendars(): void {
+ $calendars = $this->provider->getCalendars(
+ 'user-principal-123',
+ []
+ );
+
+ $this->assertCount(2, $calendars);
+ $this->assertInstanceOf(CachedSubscriptionImpl::class, $calendars[0]);
+ $this->assertInstanceOf(CachedSubscriptionImpl::class, $calendars[1]);
+ }
+
+ public function testGetCalendarsFilterByUri(): void {
+ $calendars = $this->provider->getCalendars(
+ 'user-principal-123',
+ ['subscription-1']
+ );
+
+ $this->assertCount(1, $calendars);
+ $this->assertInstanceOf(CachedSubscriptionImpl::class, $calendars[0]);
+ $this->assertEquals('subscription-1', $calendars[0]->getUri());
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/CachedSubscriptionTest.php b/apps/dav/tests/unit/CalDAV/CachedSubscriptionTest.php
index dac5090dccc..ba0da422290 100644
--- a/apps/dav/tests/unit/CalDAV/CachedSubscriptionTest.php
+++ b/apps/dav/tests/unit/CalDAV/CachedSubscriptionTest.php
@@ -1,26 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2018 Georg Ehrke
- *
- * @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\Tests\unit\CalDAV;
@@ -30,7 +12,7 @@ use OCA\DAV\CalDAV\CalDavBackend;
use Sabre\DAV\PropPatch;
class CachedSubscriptionTest extends \Test\TestCase {
- public function testGetACL() {
+ public function testGetACL(): void {
$backend = $this->createMock(CalDavBackend::class);
$calendarInfo = [
'{http://owncloud.org/ns}owner-principal' => 'user1',
@@ -61,10 +43,15 @@ class CachedSubscriptionTest extends \Test\TestCase {
'principal' => '{DAV:}authenticated',
'protected' => true,
],
+ [
+ 'privilege' => '{DAV:}write-properties',
+ 'principal' => 'user1',
+ 'protected' => 'true'
+ ]
], $calendar->getACL());
}
- public function testGetChildACL() {
+ public function testGetChildACL(): void {
$backend = $this->createMock(CalDavBackend::class);
$calendarInfo = [
'{http://owncloud.org/ns}owner-principal' => 'user1',
@@ -93,7 +80,7 @@ class CachedSubscriptionTest extends \Test\TestCase {
], $calendar->getChildACL());
}
- public function testGetOwner() {
+ public function testGetOwner(): void {
$backend = $this->createMock(CalDavBackend::class);
$calendarInfo = [
'{http://owncloud.org/ns}owner-principal' => 'user1',
@@ -106,7 +93,7 @@ class CachedSubscriptionTest extends \Test\TestCase {
$this->assertEquals('user1', $calendar->getOwner());
}
- public function testDelete() {
+ public function testDelete(): void {
$backend = $this->createMock(CalDavBackend::class);
$calendarInfo = [
'{http://owncloud.org/ns}owner-principal' => 'user1',
@@ -123,7 +110,7 @@ class CachedSubscriptionTest extends \Test\TestCase {
$calendar->delete();
}
- public function testPropPatch() {
+ public function testPropPatch(): void {
$backend = $this->createMock(CalDavBackend::class);
$calendarInfo = [
'{http://owncloud.org/ns}owner-principal' => 'user1',
@@ -141,8 +128,8 @@ class CachedSubscriptionTest extends \Test\TestCase {
$calendar->propPatch($propPatch);
}
-
- public function testGetChild() {
+
+ public function testGetChild(): void {
$this->expectException(\Sabre\DAV\Exception\NotFound::class);
$this->expectExceptionMessage('Calendar object not found');
@@ -154,17 +141,21 @@ class CachedSubscriptionTest extends \Test\TestCase {
'uri' => 'cal',
];
- $backend->expects($this->at(0))
- ->method('getCalendarObject')
- ->with(666, 'foo1', 1)
- ->willReturn([
+ $calls = [
+ [666, 'foo1', 1, [
'id' => 99,
'uri' => 'foo1'
- ]);
- $backend->expects($this->at(1))
+ ]],
+ [666, 'foo2', 1, null],
+ ];
+ $backend->expects($this->exactly(2))
->method('getCalendarObject')
- ->with(666, 'foo2', 1)
- ->willReturn(null);
+ ->willReturnCallback(function () use (&$calls) {
+ $expected = array_shift($calls);
+ $return = array_pop($expected);
+ $this->assertEquals($expected, func_get_args());
+ return $return;
+ });
$calendar = new CachedSubscription($backend, $calendarInfo);
@@ -174,7 +165,7 @@ class CachedSubscriptionTest extends \Test\TestCase {
$calendar->getChild('foo2');
}
- public function testGetChildren() {
+ public function testGetChildren(): void {
$backend = $this->createMock(CalDavBackend::class);
$calendarInfo = [
'{http://owncloud.org/ns}owner-principal' => 'user1',
@@ -183,7 +174,7 @@ class CachedSubscriptionTest extends \Test\TestCase {
'uri' => 'cal',
];
- $backend->expects($this->at(0))
+ $backend->expects($this->once())
->method('getCalendarObjects')
->with(666, 1)
->willReturn([
@@ -205,7 +196,7 @@ class CachedSubscriptionTest extends \Test\TestCase {
$this->assertInstanceOf(CachedSubscriptionObject::class, $res[1]);
}
- public function testGetMultipleChildren() {
+ public function testGetMultipleChildren(): void {
$backend = $this->createMock(CalDavBackend::class);
$calendarInfo = [
'{http://owncloud.org/ns}owner-principal' => 'user1',
@@ -214,7 +205,7 @@ class CachedSubscriptionTest extends \Test\TestCase {
'uri' => 'cal',
];
- $backend->expects($this->at(0))
+ $backend->expects($this->once())
->method('getMultipleCalendarObjects')
->with(666, ['foo1', 'foo2'], 1)
->willReturn([
@@ -236,8 +227,8 @@ class CachedSubscriptionTest extends \Test\TestCase {
$this->assertInstanceOf(CachedSubscriptionObject::class, $res[1]);
}
-
- public function testCreateFile() {
+
+ public function testCreateFile(): void {
$this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class);
$this->expectExceptionMessage('Creating objects in cached subscription is not allowed');
@@ -253,7 +244,7 @@ class CachedSubscriptionTest extends \Test\TestCase {
$calendar->createFile('foo', []);
}
- public function testChildExists() {
+ public function testChildExists(): void {
$backend = $this->createMock(CalDavBackend::class);
$calendarInfo = [
'{http://owncloud.org/ns}owner-principal' => 'user1',
@@ -262,17 +253,21 @@ class CachedSubscriptionTest extends \Test\TestCase {
'uri' => 'cal',
];
- $backend->expects($this->at(0))
- ->method('getCalendarObject')
- ->with(666, 'foo1', 1)
- ->willReturn([
+ $calls = [
+ [666, 'foo1', 1, [
'id' => 99,
'uri' => 'foo1'
- ]);
- $backend->expects($this->at(1))
+ ]],
+ [666, 'foo2', 1, null],
+ ];
+ $backend->expects($this->exactly(2))
->method('getCalendarObject')
- ->with(666, 'foo2', 1)
- ->willReturn(null);
+ ->willReturnCallback(function () use (&$calls) {
+ $expected = array_shift($calls);
+ $return = array_pop($expected);
+ $this->assertEquals($expected, func_get_args());
+ return $return;
+ });
$calendar = new CachedSubscription($backend, $calendarInfo);
@@ -280,7 +275,7 @@ class CachedSubscriptionTest extends \Test\TestCase {
$this->assertEquals(false, $calendar->childExists('foo2'));
}
- public function testCalendarQuery() {
+ public function testCalendarQuery(): void {
$backend = $this->createMock(CalDavBackend::class);
$calendarInfo = [
'{http://owncloud.org/ns}owner-principal' => 'user1',
diff --git a/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php b/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php
index 3a5cf56409c..f9205d5d322 100644
--- a/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php
+++ b/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php
@@ -1,60 +1,41 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright Copyright (c) 2017 Georg Ehrke
- *
- * @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 leith abdulla <online-nextcloud@eleith.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>
- *
- * @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\Tests\unit\CalDAV;
+use DateInterval;
use DateTime;
+use DateTimeImmutable;
use DateTimeZone;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\Calendar;
+use OCA\DAV\DAV\Sharing\Plugin as SharingPlugin;
use OCA\DAV\Events\CalendarDeletedEvent;
use OCP\IConfig;
use OCP\IL10N;
+use Psr\Log\NullLogger;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\PropPatch;
use Sabre\DAV\Xml\Property\Href;
use Sabre\DAVACL\IACL;
+use function time;
/**
* Class CalDavBackendTest
*
* @group DB
- *
- * @package OCA\DAV\Tests\unit\CalDAV
*/
class CalDavBackendTest extends AbstractCalDavBackend {
- public function testCalendarOperations() {
+ public function testCalendarOperations(): void {
$calendarId = $this->createTestCalendar();
- // update it's display name
+ // update its display name
$patch = new PropPatch([
'{DAV:}displayname' => 'Unit test',
'{urn:ietf:params:xml:ns:caldav}calendar-description' => 'Calendar used for unit testing'
@@ -78,7 +59,7 @@ class CalDavBackendTest extends AbstractCalDavBackend {
self::assertEmpty($calendars);
}
- public function providesSharingData() {
+ public static function providesSharingData(): array {
return [
[true, true, true, false, [
[
@@ -89,6 +70,9 @@ class CalDavBackendTest extends AbstractCalDavBackend {
'href' => 'principal:' . self::UNIT_TEST_GROUP,
'readOnly' => true
]
+ ], [
+ self::UNIT_TEST_USER1,
+ self::UNIT_TEST_GROUP,
]],
[true, true, true, false, [
[
@@ -99,6 +83,9 @@ class CalDavBackendTest extends AbstractCalDavBackend {
'href' => 'principal:' . self::UNIT_TEST_GROUP2,
'readOnly' => false,
],
+ ], [
+ self::UNIT_TEST_GROUP,
+ self::UNIT_TEST_GROUP2,
]],
[true, true, true, true, [
[
@@ -109,50 +96,48 @@ class CalDavBackendTest extends AbstractCalDavBackend {
'href' => 'principal:' . self::UNIT_TEST_GROUP2,
'readOnly' => true,
],
+ ], [
+ self::UNIT_TEST_GROUP,
+ self::UNIT_TEST_GROUP2,
]],
[true, false, false, false, [
[
'href' => 'principal:' . self::UNIT_TEST_USER1,
'readOnly' => true
],
+ ], [
+ self::UNIT_TEST_USER1,
]],
];
}
- /**
- * @dataProvider providesSharingData
- */
- public function testCalendarSharing($userCanRead, $userCanWrite, $groupCanRead, $groupCanWrite, $add) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesSharingData')]
+ public function testCalendarSharing($userCanRead, $userCanWrite, $groupCanRead, $groupCanWrite, $add, $principals): void {
+ $logger = $this->createMock(\Psr\Log\LoggerInterface::class);
+ $config = $this->createMock(IConfig::class);
- /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject $l10n */
$l10n = $this->createMock(IL10N::class);
- $l10n
- ->expects($this->any())
+ $l10n->expects($this->any())
->method('t')
->willReturnCallback(function ($text, $parameters = []) {
return vsprintf($text, $parameters);
});
- $logger = $this->createMock(\Psr\Log\LoggerInterface::class);
-
- $config = $this->createMock(IConfig::class);
-
$this->userManager->expects($this->any())
->method('userExists')
->willReturn(true);
-
$this->groupManager->expects($this->any())
->method('groupExists')
->willReturn(true);
+ $this->principal->expects(self::atLeastOnce())
+ ->method('findByUri')
+ ->willReturnOnConsecutiveCalls(...$principals);
$calendarId = $this->createTestCalendar();
$calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
$this->assertCount(1, $calendars);
$calendar = new Calendar($this->backend, $calendars[0], $l10n, $config, $logger);
- $this->legacyDispatcher->expects($this->at(0))
- ->method('dispatch')
- ->with('\OCA\DAV\CalDAV\CalDavBackend::updateShares');
$this->backend->updateShares($calendar, $add, []);
$calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER1);
$this->assertCount(1, $calendars);
@@ -206,7 +191,7 @@ EOD;
self::assertEmpty($calendars);
}
- public function testCalendarObjectsOperations() {
+ public function testCalendarObjectsOperations(): void {
$calendarId = $this->createTestCalendar();
// create a card
@@ -232,13 +217,13 @@ EOD;
->method('dispatchTyped');
$this->backend->createCalendarObject($calendarId, $uri, $calData);
- // get all the cards
+ // get all the calendar objects
$calendarObjects = $this->backend->getCalendarObjects($calendarId);
$this->assertCount(1, $calendarObjects);
$this->assertEquals($calendarId, $calendarObjects[0]['calendarid']);
$this->assertArrayHasKey('classification', $calendarObjects[0]);
- // get the cards
+ // get the calendar objects
$calendarObject = $this->backend->getCalendarObject($calendarId, $uri);
$this->assertNotNull($calendarObject);
$this->assertArrayHasKey('id', $calendarObject);
@@ -247,6 +232,7 @@ EOD;
$this->assertArrayHasKey('etag', $calendarObject);
$this->assertArrayHasKey('size', $calendarObject);
$this->assertArrayHasKey('classification', $calendarObject);
+ $this->assertArrayHasKey('{' . SharingPlugin::NS_NEXTCLOUD . '}deleted-at', $calendarObject);
$this->assertEquals($calData, $calendarObject['calendardata']);
// update the card
@@ -280,7 +266,7 @@ EOD;
}
- public function testMultipleCalendarObjectsWithSameUID() {
+ public function testMultipleCalendarObjectsWithSameUID(): void {
$this->expectException(\Sabre\DAV\Exception\BadRequest::class);
$this->expectExceptionMessage('Calendar object with uid already exists in this calendar collection.');
@@ -309,7 +295,7 @@ EOD;
$this->backend->createCalendarObject($calendarId, $uri1, $calData);
}
- public function testMultiCalendarObjects() {
+ public function testMultiCalendarObjects(): void {
$calendarId = $this->createTestCalendar();
// create an event
@@ -415,16 +401,19 @@ EOD;
$this->assertCount(0, $calendarObjects);
}
- /**
- * @dataProvider providesCalendarQueryParameters
- */
- public function testCalendarQuery($expectedEventsInResult, $propFilters, $compFilter) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesCalendarQueryParameters')]
+ public function testCalendarQuery($expectedEventsInResult, $propFilters, $compFilter): void {
$calendarId = $this->createTestCalendar();
$events = [];
$events[0] = $this->createEvent($calendarId, '20130912T130000Z', '20130912T140000Z');
$events[1] = $this->createEvent($calendarId, '20130912T150000Z', '20130912T170000Z');
$events[2] = $this->createEvent($calendarId, '20130912T173000Z', '20130912T220000Z');
- $events[3] = $this->createEvent($calendarId, '21130912T130000Z', '22130912T130000Z');
+ if (PHP_INT_SIZE > 8) {
+ $events[3] = $this->createEvent($calendarId, '21130912T130000Z', '22130912T130000Z');
+ } else {
+ /* On 32bit we do not support events after 2038 */
+ $events[3] = $this->createEvent($calendarId, '20370912T130000Z', '20370912T130000Z');
+ }
$result = $this->backend->calendarQuery($calendarId, [
'name' => '',
@@ -438,7 +427,7 @@ EOD;
$this->assertEqualsCanonicalizing($expectedEventsInResult, $result);
}
- public function testGetCalendarObjectByUID() {
+ public function testGetCalendarObjectByUID(): void {
$calendarId = $this->createTestCalendar();
$uri = static::getUniqueID('calobj');
$calData = <<<'EOD'
@@ -465,33 +454,105 @@ EOD;
$this->assertNotNull($co);
}
- public function providesCalendarQueryParameters() {
+ public static function providesCalendarQueryParameters(): array {
return [
'all' => [[0, 1, 2, 3], [], []],
'only-todos' => [[], ['name' => 'VTODO'], []],
'only-events' => [[0, 1, 2, 3], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => null, 'end' => null], 'prop-filters' => []]],],
'start' => [[1, 2, 3], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => new DateTime('2013-09-12 14:00:00', new DateTimeZone('UTC')), 'end' => null], 'prop-filters' => []]],],
'end' => [[0], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => null, 'end' => new DateTime('2013-09-12 14:00:00', new DateTimeZone('UTC'))], 'prop-filters' => []]],],
- 'future' => [[3], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => new DateTime('2099-09-12 14:00:00', new DateTimeZone('UTC')), 'end' => null], 'prop-filters' => []]],],
+ 'future' => [[3], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => new DateTime('2036-09-12 14:00:00', new DateTimeZone('UTC')), 'end' => null], 'prop-filters' => []]],],
];
}
- public function testSyncSupport() {
- $calendarId = $this->createTestCalendar();
+ public function testCalendarSynchronization(): void {
- // fist call without synctoken
- $changes = $this->backend->getChangesForCalendar($calendarId, '', 1);
- $syncToken = $changes['syncToken'];
+ // construct calendar for testing
+ $calendarId = $this->createTestCalendar();
- // add a change
- $event = $this->createEvent($calendarId, '20130912T130000Z', '20130912T140000Z');
+ /** test fresh sync state with NO events in calendar */
+ // construct test state
+ $stateTest = ['syncToken' => 1, 'added' => [], 'modified' => [], 'deleted' => []];
+ // retrieve live state
+ $stateLive = $this->backend->getChangesForCalendar($calendarId, '', 1);
+ // test live state
+ $this->assertEquals($stateTest, $stateLive, 'Failed test fresh sync state with NO events in calendar');
+
+ /** test delta sync state with NO events in calendar */
+ // construct test state
+ $stateTest = ['syncToken' => 1, 'added' => [], 'modified' => [], 'deleted' => []];
+ // retrieve live state
+ $stateLive = $this->backend->getChangesForCalendar($calendarId, '2', 1);
+ // test live state
+ $this->assertEquals($stateTest, $stateLive, 'Failed test delta sync state with NO events in calendar');
+
+ /** add events to calendar */
+ $event1 = $this->createEvent($calendarId, '20240701T130000Z', '20240701T140000Z');
+ $event2 = $this->createEvent($calendarId, '20240701T140000Z', '20240701T150000Z');
+ $event3 = $this->createEvent($calendarId, '20240701T150000Z', '20240701T160000Z');
+
+ /** test fresh sync state with events in calendar */
+ // construct expected state
+ $stateTest = ['syncToken' => 4, 'added' => [$event1, $event2, $event3], 'modified' => [], 'deleted' => []];
+ sort($stateTest['added']);
+ // retrieve live state
+ $stateLive = $this->backend->getChangesForCalendar($calendarId, '', 1);
+ // sort live state results
+ sort($stateLive['added']);
+ sort($stateLive['modified']);
+ sort($stateLive['deleted']);
+ // test live state
+ $this->assertEquals($stateTest, $stateLive, 'Failed test fresh sync state with events in calendar');
+
+ /** test delta sync state with events in calendar */
+ // construct expected state
+ $stateTest = ['syncToken' => 4, 'added' => [$event2, $event3], 'modified' => [], 'deleted' => []];
+ sort($stateTest['added']);
+ // retrieve live state
+ $stateLive = $this->backend->getChangesForCalendar($calendarId, '2', 1);
+ // sort live state results
+ sort($stateLive['added']);
+ sort($stateLive['modified']);
+ sort($stateLive['deleted']);
+ // test live state
+ $this->assertEquals($stateTest, $stateLive, 'Failed test delta sync state with events in calendar');
+
+ /** modify/delete events in calendar */
+ $this->deleteEvent($calendarId, $event1);
+ $this->modifyEvent($calendarId, $event2, '20250701T140000Z', '20250701T150000Z');
+
+ /** test fresh sync state with modified/deleted events in calendar */
+ // construct expected state
+ $stateTest = ['syncToken' => 6, 'added' => [$event2, $event3], 'modified' => [], 'deleted' => []];
+ sort($stateTest['added']);
+ // retrieve live state
+ $stateLive = $this->backend->getChangesForCalendar($calendarId, '', 1);
+ // sort live state results
+ sort($stateLive['added']);
+ sort($stateLive['modified']);
+ sort($stateLive['deleted']);
+ // test live state
+ $this->assertEquals($stateTest, $stateLive, 'Failed test fresh sync state with modified/deleted events in calendar');
+
+ /** test delta sync state with modified/deleted events in calendar */
+ // construct expected state
+ $stateTest = ['syncToken' => 6, 'added' => [$event3], 'modified' => [$event2], 'deleted' => [$event1]];
+ // retrieve live state
+ $stateLive = $this->backend->getChangesForCalendar($calendarId, '3', 1);
+ // test live state
+ $this->assertEquals($stateTest, $stateLive, 'Failed test delta sync state with modified/deleted events in calendar');
+
+ /** test delta sync state with modified/deleted events in calendar and invalid token */
+ // construct expected state
+ $stateTest = ['syncToken' => 6, 'added' => [], 'modified' => [], 'deleted' => []];
+ // retrieve live state
+ $stateLive = $this->backend->getChangesForCalendar($calendarId, '6', 1);
+ // test live state
+ $this->assertEquals($stateTest, $stateLive, 'Failed test delta sync state with modified/deleted events in calendar and invalid token');
- // look for changes
- $changes = $this->backend->getChangesForCalendar($calendarId, $syncToken, 1);
- $this->assertEquals($event, $changes['added'][0]);
}
- public function testPublications() {
+ public function testPublications(): void {
$this->dispatcher->expects(self::atLeastOnce())
->method('dispatchTyped');
@@ -523,7 +584,7 @@ EOD;
$this->backend->getPublicCalendar($publicCalendarURI);
}
- public function testSubscriptions() {
+ public function testSubscriptions(): void {
$id = $this->backend->createSubscription(self::UNIT_TEST_USER, 'Subscription', [
'{http://calendarserver.org/ns/}source' => new Href('test-source'),
'{http://apple.com/ns/ical/}calendar-color' => '#1C4587',
@@ -554,7 +615,7 @@ EOD;
$this->assertCount(0, $subscriptions);
}
- public function providesSchedulingData() {
+ public static function providesSchedulingData(): array {
$data = <<<EOS
BEGIN:VCALENDAR
VERSION:2.0
@@ -627,10 +688,10 @@ EOS;
}
/**
- * @dataProvider providesSchedulingData
* @param $objectData
*/
- public function testScheduling($objectData) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesSchedulingData')]
+ public function testScheduling($objectData): void {
$this->backend->createSchedulingObject(self::UNIT_TEST_USER, 'Sample Schedule', $objectData);
$sos = $this->backend->getSchedulingObjects(self::UNIT_TEST_USER);
@@ -645,29 +706,43 @@ EOS;
$this->assertCount(0, $sos);
}
- /**
- * @dataProvider providesCalDataForGetDenormalizedData
- */
- public function testGetDenormalizedData($expected, $key, $calData) {
- $actual = $this->backend->getDenormalizedData($calData);
- $this->assertEquals($expected, $actual[$key]);
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesCalDataForGetDenormalizedData')]
+ public function testGetDenormalizedData($expected, $key, $calData): void {
+ try {
+ $actual = $this->backend->getDenormalizedData($calData);
+ $this->assertEquals($expected, $actual[$key]);
+ } catch (\Throwable $e) {
+ if (($e->getMessage() === 'Epoch doesn\'t fit in a PHP integer') && (PHP_INT_SIZE < 8)) {
+ $this->markTestSkipped('This fail on 32bits because of PHP limitations in DateTime');
+ }
+ throw $e;
+ }
}
- public function providesCalDataForGetDenormalizedData() {
+ public static function providesCalDataForGetDenormalizedData(): array {
return [
'first occurrence before unix epoch starts' => [0, 'firstOccurence', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:413F269B-B51B-46B1-AFB6-40055C53A4DC\r\nDTSTAMP:20160309T095056Z\r\nDTSTART;VALUE=DATE:16040222\r\nDTEND;VALUE=DATE:16040223\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:SUMMARY\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"],
'no first occurrence because yearly' => [null, 'firstOccurence', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:413F269B-B51B-46B1-AFB6-40055C53A4DC\r\nDTSTAMP:20160309T095056Z\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:SUMMARY\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"],
+
'last occurrence is max when only last VEVENT in group is weekly' => [(new DateTime(CalDavBackend::MAX_DATE))->getTimestamp(), 'lastOccurence', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.3.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nDTSTART;TZID=America/Los_Angeles:20200812T103000\r\nDTEND;TZID=America/Los_Angeles:20200812T110000\r\nDTSTAMP:20200927T180638Z\r\nUID:asdfasdfasdf@google.com\r\nRECURRENCE-ID;TZID=America/Los_Angeles:20200811T123000\r\nCREATED:20200626T181848Z\r\nLAST-MODIFIED:20200922T192707Z\r\nSUMMARY:Weekly 1:1\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nDTSTART;TZID=America/Los_Angeles:20200728T123000\r\nDTEND;TZID=America/Los_Angeles:20200728T130000\r\nEXDATE;TZID=America/Los_Angeles:20200818T123000\r\nRRULE:FREQ=WEEKLY;BYDAY=TU\r\nDTSTAMP:20200927T180638Z\r\nUID:asdfasdfasdf@google.com\r\nCREATED:20200626T181848Z\r\nDESCRIPTION:Setting up recurring time on our calendars\r\nLAST-MODIFIED:20200922T192707Z\r\nSUMMARY:Weekly 1:1\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"],
- 'first occurrence is found when not first VEVENT in group' => [(new DateTime('2020-09-01T110000', new DateTimeZone("America/Los_Angeles")))->getTimestamp(), 'firstOccurence', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.3.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nDTSTART;TZID=America/Los_Angeles:20201013T110000\r\nDTEND;TZID=America/Los_Angeles:20201013T120000\r\nDTSTAMP:20200927T180638Z\r\nUID:asdf0000@google.com\r\nRECURRENCE-ID;TZID=America/Los_Angeles:20201013T110000\r\nCREATED:20160330T034726Z\r\nLAST-MODIFIED:20200925T042014Z\r\nSTATUS:CONFIRMED\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nDTSTART;TZID=America/Los_Angeles:20200901T110000\r\nDTEND;TZID=America/Los_Angeles:20200901T120000\r\nRRULE:FREQ=WEEKLY;BYDAY=TU\r\nEXDATE;TZID=America/Los_Angeles:20200922T110000\r\nEXDATE;TZID=America/Los_Angeles:20200915T110000\r\nEXDATE;TZID=America/Los_Angeles:20200908T110000\r\nDTSTAMP:20200927T180638Z\r\nUID:asdf0000@google.com\r\nCREATED:20160330T034726Z\r\nLAST-MODIFIED:20200915T162810Z\r\nSTATUS:CONFIRMED\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"],
+
+ 'last occurrence before unix epoch starts' => [0, 'lastOccurence', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.3.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE:19110324\r\nDTEND;VALUE=DATE:19110325\r\nDTSTAMP:20200927T180638Z\r\nUID:asdfasdfasdf@google.com\r\nCREATED:20200626T181848Z\r\nDESCRIPTION:Very old event\r\nLAST-MODIFIED:20200922T192707Z\r\nSUMMARY:Some old event\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"],
+
+ 'first occurrence is found when not first VEVENT in group' => [(new DateTime('2020-09-01T110000', new DateTimeZone('America/Los_Angeles')))->getTimestamp(), 'firstOccurence', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.3.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nDTSTART;TZID=America/Los_Angeles:20201013T110000\r\nDTEND;TZID=America/Los_Angeles:20201013T120000\r\nDTSTAMP:20200927T180638Z\r\nUID:asdf0000@google.com\r\nRECURRENCE-ID;TZID=America/Los_Angeles:20201013T110000\r\nCREATED:20160330T034726Z\r\nLAST-MODIFIED:20200925T042014Z\r\nSTATUS:CONFIRMED\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nDTSTART;TZID=America/Los_Angeles:20200901T110000\r\nDTEND;TZID=America/Los_Angeles:20200901T120000\r\nRRULE:FREQ=WEEKLY;BYDAY=TU\r\nEXDATE;TZID=America/Los_Angeles:20200922T110000\r\nEXDATE;TZID=America/Los_Angeles:20200915T110000\r\nEXDATE;TZID=America/Los_Angeles:20200908T110000\r\nDTSTAMP:20200927T180638Z\r\nUID:asdf0000@google.com\r\nCREATED:20160330T034726Z\r\nLAST-MODIFIED:20200915T162810Z\r\nSTATUS:CONFIRMED\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"],
+
'CLASS:PRIVATE' => [CalDavBackend::CLASSIFICATION_PRIVATE, 'classification', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//dmfs.org//mimedir.icalendar//EN\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nX-LIC-LOCATION:Europe/Berlin\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nDTSTART;TZID=Europe/Berlin:20160419T130000\r\nSUMMARY:Test\r\nCLASS:PRIVATE\r\nTRANSP:OPAQUE\r\nSTATUS:CONFIRMED\r\nDTEND;TZID=Europe/Berlin:20160419T140000\r\nLAST-MODIFIED:20160419T074202Z\r\nDTSTAMP:20160419T074202Z\r\nCREATED:20160419T074202Z\r\nUID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310\r\nEND:VEVENT\r\nEND:VCALENDAR"],
+
'CLASS:PUBLIC' => [CalDavBackend::CLASSIFICATION_PUBLIC, 'classification', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//dmfs.org//mimedir.icalendar//EN\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nX-LIC-LOCATION:Europe/Berlin\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nDTSTART;TZID=Europe/Berlin:20160419T130000\r\nSUMMARY:Test\r\nCLASS:PUBLIC\r\nTRANSP:OPAQUE\r\nSTATUS:CONFIRMED\r\nDTEND;TZID=Europe/Berlin:20160419T140000\r\nLAST-MODIFIED:20160419T074202Z\r\nDTSTAMP:20160419T074202Z\r\nCREATED:20160419T074202Z\r\nUID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310\r\nEND:VEVENT\r\nEND:VCALENDAR"],
+
'CLASS:CONFIDENTIAL' => [CalDavBackend::CLASSIFICATION_CONFIDENTIAL, 'classification', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//dmfs.org//mimedir.icalendar//EN\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nX-LIC-LOCATION:Europe/Berlin\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nDTSTART;TZID=Europe/Berlin:20160419T130000\r\nSUMMARY:Test\r\nCLASS:CONFIDENTIAL\r\nTRANSP:OPAQUE\r\nSTATUS:CONFIRMED\r\nDTEND;TZID=Europe/Berlin:20160419T140000\r\nLAST-MODIFIED:20160419T074202Z\r\nDTSTAMP:20160419T074202Z\r\nCREATED:20160419T074202Z\r\nUID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310\r\nEND:VEVENT\r\nEND:VCALENDAR"],
+
'no class set -> public' => [CalDavBackend::CLASSIFICATION_PUBLIC, 'classification', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//dmfs.org//mimedir.icalendar//EN\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nX-LIC-LOCATION:Europe/Berlin\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nDTSTART;TZID=Europe/Berlin:20160419T130000\r\nSUMMARY:Test\r\nTRANSP:OPAQUE\r\nDTEND;TZID=Europe/Berlin:20160419T140000\r\nLAST-MODIFIED:20160419T074202Z\r\nDTSTAMP:20160419T074202Z\r\nCREATED:20160419T074202Z\r\nUID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310\r\nEND:VEVENT\r\nEND:VCALENDAR"],
+
'unknown class -> private' => [CalDavBackend::CLASSIFICATION_PRIVATE, 'classification', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//dmfs.org//mimedir.icalendar//EN\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nX-LIC-LOCATION:Europe/Berlin\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nDTSTART;TZID=Europe/Berlin:20160419T130000\r\nSUMMARY:Test\r\nCLASS:VERTRAULICH\r\nTRANSP:OPAQUE\r\nSTATUS:CONFIRMED\r\nDTEND;TZID=Europe/Berlin:20160419T140000\r\nLAST-MODIFIED:20160419T074202Z\r\nDTSTAMP:20160419T074202Z\r\nCREATED:20160419T074202Z\r\nUID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310\r\nEND:VEVENT\r\nEND:VCALENDAR"],
];
}
- public function testCalendarSearch() {
+ public function testCalendarSearch(): void {
$calendarId = $this->createTestCalendar();
$uri = static::getUniqueID('calobj');
@@ -797,10 +872,8 @@ EOD;
$this->assertEquals(count($search5), 0);
}
- /**
- * @dataProvider searchDataProvider
- */
- public function testSearch(bool $isShared, array $searchOptions, int $count) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('searchDataProvider')]
+ public function testSearch(bool $isShared, array $searchOptions, int $count): void {
$calendarId = $this->createTestCalendar();
$uris = [];
@@ -899,7 +972,7 @@ EOD;
$this->assertCount($count, $result);
}
- public function searchDataProvider() {
+ public static function searchDataProvider(): array {
return [
[false, [], 4],
[true, ['timerange' => ['start' => new DateTime('2013-09-12 13:00:00'), 'end' => new DateTime('2013-09-12 14:00:00')]], 2],
@@ -907,7 +980,7 @@ EOD;
];
}
- public function testSameUriSameIdForDifferentCalendarTypes() {
+ public function testSameUriSameIdForDifferentCalendarTypes(): void {
$calendarId = $this->createTestCalendar();
$subscriptionId = $this->createTestSubscription();
@@ -953,7 +1026,7 @@ EOD;
$this->assertEquals($calData2, $this->backend->getCalendarObject($subscriptionId, $uri, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION)['calendardata']);
}
- public function testPurgeAllCachedEventsForSubscription() {
+ public function testPurgeAllCachedEventsForSubscription(): void {
$subscriptionId = $this->createTestSubscription();
$uri = static::getUniqueID('calobj');
$calData = <<<EOD
@@ -979,7 +1052,7 @@ EOD;
$this->assertEquals(null, $this->backend->getCalendarObject($subscriptionId, $uri, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION));
}
- public function testCalendarMovement() {
+ public function testCalendarMovement(): void {
$this->backend->createCalendar(self::UNIT_TEST_USER, 'Example', []);
$this->assertCount(1, $this->backend->getCalendarsForUser(self::UNIT_TEST_USER));
@@ -1228,6 +1301,9 @@ EOD;
$this->groupManager->expects($this->any())
->method('groupExists')
->willReturn(true);
+ $this->principal->expects(self::atLeastOnce())
+ ->method('findByUri')
+ ->willReturn(self::UNIT_TEST_USER);
$me = self::UNIT_TEST_USER;
$sharer = self::UNIT_TEST_USER1;
@@ -1274,4 +1350,536 @@ EOD;
$this->assertEquals($sharerPrivate, $sharerSearchResults[1]['calendardata']);
$this->assertEquals($sharerConfidential, $sharerSearchResults[2]['calendardata']);
}
+
+ /**
+ * @throws \OCP\DB\Exception
+ * @throws \Sabre\DAV\Exception\BadRequest
+ */
+ public function testPruneOutdatedSyncTokens(): void {
+ $calendarId = $this->createTestCalendar();
+ $changes = $this->backend->getChangesForCalendar($calendarId, '', 1);
+ $syncToken = $changes['syncToken'];
+
+ $uri = static::getUniqueID('calobj');
+ $calData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ $this->backend->createCalendarObject($calendarId, $uri, $calData);
+
+ // update the card
+ $calData = <<<'EOD'
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:123 Event 🙈
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+ATTENDEE;CN=test:mailto:foo@bar.com
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $this->backend->updateCalendarObject($calendarId, $uri, $calData);
+
+ // Keep everything
+ $deleted = $this->backend->pruneOutdatedSyncTokens(0, 0);
+ self::assertSame(0, $deleted);
+
+ $deleted = $this->backend->pruneOutdatedSyncTokens(0, time());
+ // At least one from the object creation and one from the object update
+ $this->assertGreaterThanOrEqual(2, $deleted);
+ $changes = $this->backend->getChangesForCalendar($calendarId, $syncToken, 1);
+ $this->assertEmpty($changes['added']);
+ $this->assertEmpty($changes['modified']);
+ $this->assertEmpty($changes['deleted']);
+
+ // Test that objects remain
+
+ // Currently changes are empty
+ $changes = $this->backend->getChangesForCalendar($calendarId, $syncToken, 100);
+ $this->assertEquals(0, count($changes['added'] + $changes['modified'] + $changes['deleted']));
+
+ // Create card
+ $uri = static::getUniqueID('calobj');
+ $calData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20230910T125139Z
+UID:47d15e3ec9
+LAST-MODIFIED;VALUE=DATE-TIME:20230910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20230910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20230912T130000Z
+DTEND;VALUE=DATE-TIME:20230912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $this->backend->createCalendarObject($calendarId, $uri, $calData);
+
+ // We now have one add
+ $changes = $this->backend->getChangesForCalendar($calendarId, $syncToken, 100);
+ $this->assertEquals(1, count($changes['added']));
+ $this->assertEmpty($changes['modified']);
+ $this->assertEmpty($changes['deleted']);
+
+ // update the card
+ $calData = <<<'EOD'
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20230910T125139Z
+UID:47d15e3ec9
+LAST-MODIFIED;VALUE=DATE-TIME:20230910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20230910T125139Z
+SUMMARY:123 Event 🙈
+DTSTART;VALUE=DATE-TIME:20230912T130000Z
+DTEND;VALUE=DATE-TIME:20230912T140000Z
+ATTENDEE;CN=test:mailto:foo@bar.com
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $this->backend->updateCalendarObject($calendarId, $uri, $calData);
+
+ // One add, one modify, but shortened to modify
+ $changes = $this->backend->getChangesForCalendar($calendarId, $syncToken, 100);
+ $this->assertEmpty($changes['added']);
+ $this->assertEquals(1, count($changes['modified']));
+ $this->assertEmpty($changes['deleted']);
+
+ // Delete all but last change
+ $deleted = $this->backend->pruneOutdatedSyncTokens(1, time());
+ $this->assertEquals(1, $deleted); // We had two changes before, now one
+
+ // Only update should remain
+ $changes = $this->backend->getChangesForCalendar($calendarId, $syncToken, 100);
+ $this->assertEmpty($changes['added']);
+ $this->assertEquals(1, count($changes['modified']));
+ $this->assertEmpty($changes['deleted']);
+
+ // Check that no crash occurs when prune is called without current changes
+ $deleted = $this->backend->pruneOutdatedSyncTokens(1, time());
+ self::assertSame(0, $deleted);
+ }
+
+ public function testSearchAndExpandRecurrences(): void {
+ $calendarId = $this->createTestCalendar();
+ $calendarInfo = [
+ 'id' => $calendarId,
+ 'principaluri' => 'user1',
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ ];
+
+ $calData = <<<'EOD'
+BEGIN:VCALENDAR
+PRODID:-//IDN nextcloud.com//Calendar app 4.5.0-alpha.2//EN
+CALSCALE:GREGORIAN
+VERSION:2.0
+BEGIN:VEVENT
+CREATED:20230921T133401Z
+DTSTAMP:20230921T133448Z
+LAST-MODIFIED:20230921T133448Z
+SEQUENCE:2
+UID:7b7d5d12-683c-48ce-973a-b3e1cb0bae2a
+DTSTART;VALUE=DATE:20230912
+DTEND;VALUE=DATE:20230913
+STATUS:CONFIRMED
+SUMMARY:Daily Event
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $uri = static::getUniqueID('calobj');
+ $this->backend->createCalendarObject($calendarId, $uri, $calData);
+
+ $start = new DateTimeImmutable('2023-09-20T00:00:00Z');
+ $end = $start->add(new DateInterval('P14D'));
+
+ $results = $this->backend->search(
+ $calendarInfo,
+ '',
+ [],
+ [
+ 'timerange' => [
+ 'start' => $start,
+ 'end' => $end,
+ ]
+ ],
+ null,
+ null,
+ );
+
+ $this->assertCount(1, $results);
+ $this->assertCount(14, $results[0]['objects']);
+ foreach ($results as $result) {
+ foreach ($result['objects'] as $object) {
+ $this->assertEquals($object['UID'][0], '7b7d5d12-683c-48ce-973a-b3e1cb0bae2a');
+ $this->assertEquals($object['SUMMARY'][0], 'Daily Event');
+ $this->assertGreaterThanOrEqual(
+ $start->getTimestamp(),
+ $object['DTSTART'][0]->getTimestamp(),
+ 'Recurrence starting before requested start',
+ );
+ $this->assertLessThanOrEqual(
+ $end->getTimestamp(),
+ $object['DTSTART'][0]->getTimestamp(),
+ 'Recurrence starting after requested end',
+ );
+ }
+ }
+ }
+
+ public function testRestoreChanges(): void {
+ $calendarId = $this->createTestCalendar();
+ $uri1 = static::getUniqueID('calobj1') . '.ics';
+ $calData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $this->backend->createCalendarObject($calendarId, $uri1, $calData);
+ $calData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event – UPDATED
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $this->backend->updateCalendarObject($calendarId, $uri1, $calData);
+ $uri2 = static::getUniqueID('calobj2') . '.ics';
+ $calData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec9
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $this->backend->createCalendarObject($calendarId, $uri2, $calData);
+ $changesBefore = $this->backend->getChangesForCalendar($calendarId, null, 1);
+ $this->backend->deleteCalendarObject($calendarId, $uri2);
+ $uri3 = static::getUniqueID('calobj3') . '.ics';
+ $calData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3e10
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $this->backend->createCalendarObject($calendarId, $uri3, $calData);
+ $deleteChanges = $this->db->getQueryBuilder();
+ $deleteChanges->delete('calendarchanges')
+ ->where($deleteChanges->expr()->eq('calendarid', $deleteChanges->createNamedParameter($calendarId)));
+ $deleteChanges->executeStatement();
+
+ $this->backend->restoreChanges($calendarId);
+
+ $changesAfter = $this->backend->getChangesForCalendar($calendarId, $changesBefore['syncToken'], 1);
+ self::assertEquals([], $changesAfter['added']);
+ self::assertEqualsCanonicalizing([$uri1, $uri3], $changesAfter['modified']);
+ self::assertEquals([$uri2], $changesAfter['deleted']);
+ }
+
+ public function testSearchWithLimitAndTimeRange(): void {
+ $calendarId = $this->createTestCalendar();
+ $calendarInfo = [
+ 'id' => $calendarId,
+ 'principaluri' => 'user1',
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ ];
+
+ $testFiles = [
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-1.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-2.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-3.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-4.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-5.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-6.ics',
+ ];
+
+ foreach ($testFiles as $testFile) {
+ $objectUri = static::getUniqueID('search-limit-timerange-');
+ $calendarData = \file_get_contents($testFile);
+ $this->backend->createCalendarObject($calendarId, $objectUri, $calendarData);
+ }
+
+ $start = new DateTimeImmutable('2024-05-06T00:00:00Z');
+ $end = $start->add(new DateInterval('P14D'));
+
+ $results = $this->backend->search(
+ $calendarInfo,
+ '',
+ [],
+ [
+ 'timerange' => [
+ 'start' => $start,
+ 'end' => $end,
+ ]
+ ],
+ 4,
+ null,
+ );
+
+ $this->assertCount(2, $results);
+
+ $this->assertEquals('Cake Tasting', $results[0]['objects'][0]['SUMMARY'][0]);
+ $this->assertGreaterThanOrEqual(
+ $start->getTimestamp(),
+ $results[0]['objects'][0]['DTSTART'][0]->getTimestamp(),
+ 'Recurrence starting before requested start',
+ );
+
+ $this->assertEquals('Pasta Day', $results[1]['objects'][0]['SUMMARY'][0]);
+ $this->assertGreaterThanOrEqual(
+ $start->getTimestamp(),
+ $results[1]['objects'][0]['DTSTART'][0]->getTimestamp(),
+ 'Recurrence starting before requested start',
+ );
+ }
+
+ public function testSearchWithLimitAndTimeRangeShouldNotReturnMoreObjectsThenLimit(): void {
+ $calendarId = $this->createTestCalendar();
+ $calendarInfo = [
+ 'id' => $calendarId,
+ 'principaluri' => 'user1',
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ ];
+
+ $testFiles = [
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-1.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-2.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-3.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-4.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-5.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-6.ics',
+ ];
+
+ foreach ($testFiles as $testFile) {
+ $objectUri = static::getUniqueID('search-limit-timerange-');
+ $calendarData = \file_get_contents($testFile);
+ $this->backend->createCalendarObject($calendarId, $objectUri, $calendarData);
+ }
+
+ $start = new DateTimeImmutable('2024-05-06T00:00:00Z');
+ $end = $start->add(new DateInterval('P14D'));
+
+ $results = $this->backend->search(
+ $calendarInfo,
+ '',
+ [],
+ [
+ 'timerange' => [
+ 'start' => $start,
+ 'end' => $end,
+ ]
+ ],
+ 1,
+ null,
+ );
+
+ $this->assertCount(1, $results);
+
+ $this->assertEquals('Cake Tasting', $results[0]['objects'][0]['SUMMARY'][0]);
+ $this->assertGreaterThanOrEqual(
+ $start->getTimestamp(),
+ $results[0]['objects'][0]['DTSTART'][0]->getTimestamp(),
+ 'Recurrence starting before requested start',
+ );
+ }
+
+ public function testSearchWithLimitAndTimeRangeShouldReturnObjectsInTheSameOrder(): void {
+ $calendarId = $this->createTestCalendar();
+ $calendarInfo = [
+ 'id' => $calendarId,
+ 'principaluri' => 'user1',
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ ];
+
+ $testFiles = [
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-1.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-2.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-3.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-4.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-6.ics', // <-- intentional!
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-5.ics',
+ ];
+
+ foreach ($testFiles as $testFile) {
+ $objectUri = static::getUniqueID('search-limit-timerange-');
+ $calendarData = \file_get_contents($testFile);
+ $this->backend->createCalendarObject($calendarId, $objectUri, $calendarData);
+ }
+
+ $start = new DateTimeImmutable('2024-05-06T00:00:00Z');
+ $end = $start->add(new DateInterval('P14D'));
+
+ $results = $this->backend->search(
+ $calendarInfo,
+ '',
+ [],
+ [
+ 'timerange' => [
+ 'start' => $start,
+ 'end' => $end,
+ ]
+ ],
+ 2,
+ null,
+ );
+
+ $this->assertCount(2, $results);
+
+ $this->assertEquals('Cake Tasting', $results[0]['objects'][0]['SUMMARY'][0]);
+ $this->assertGreaterThanOrEqual(
+ $start->getTimestamp(),
+ $results[0]['objects'][0]['DTSTART'][0]->getTimestamp(),
+ 'Recurrence starting before requested start',
+ );
+
+ $this->assertEquals('Pasta Day', $results[1]['objects'][0]['SUMMARY'][0]);
+ $this->assertGreaterThanOrEqual(
+ $start->getTimestamp(),
+ $results[1]['objects'][0]['DTSTART'][0]->getTimestamp(),
+ 'Recurrence starting before requested start',
+ );
+ }
+
+ public function testSearchShouldReturnObjectsInTheSameOrderMissingDate(): void {
+ $calendarId = $this->createTestCalendar();
+ $calendarInfo = [
+ 'id' => $calendarId,
+ 'principaluri' => 'user1',
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ ];
+
+ $testFiles = [
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-6.ics', // <-- intentional!
+ __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-5.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-missing-start-1.ics',
+ __DIR__ . '/../test_fixtures/caldav-search-missing-start-2.ics',
+ ];
+
+ foreach ($testFiles as $testFile) {
+ $objectUri = static::getUniqueID('search-return-objects-in-same-order-');
+ $calendarData = \file_get_contents($testFile);
+ $this->backend->createCalendarObject($calendarId, $objectUri, $calendarData);
+ }
+
+ $results = $this->backend->search(
+ $calendarInfo,
+ '',
+ [],
+ [],
+ 4,
+ null,
+ );
+
+ $this->assertCount(4, $results);
+
+ $this->assertEquals('Cake Tasting', $results[0]['objects'][0]['SUMMARY'][0]);
+ $this->assertEquals('Pasta Day', $results[1]['objects'][0]['SUMMARY'][0]);
+ $this->assertEquals('Missing DTSTART 1', $results[2]['objects'][0]['SUMMARY'][0]);
+ $this->assertEquals('Missing DTSTART 2', $results[3]['objects'][0]['SUMMARY'][0]);
+ }
+
+ public function testUnshare(): void {
+ $principalGroup = 'principal:' . self::UNIT_TEST_GROUP;
+ $principalUser = 'principal:' . self::UNIT_TEST_USER;
+
+ $l10n = $this->createMock(IL10N::class);
+ $l10n->method('t')
+ ->willReturnCallback(fn ($text, $parameters = []) => vsprintf($text, $parameters));
+ $config = $this->createMock(IConfig::class);
+ $logger = new NullLogger();
+
+ $this->principal->expects($this->exactly(2))
+ ->method('findByUri')
+ ->willReturnMap([
+ [$principalGroup, '', self::UNIT_TEST_GROUP],
+ [$principalUser, '', self::UNIT_TEST_USER],
+ ]);
+ $this->groupManager->expects($this->once())
+ ->method('groupExists')
+ ->willReturn(true);
+ $this->dispatcher->expects($this->exactly(2))
+ ->method('dispatchTyped');
+
+ $calendarId = $this->createTestCalendar();
+ $calendarInfo = $this->backend->getCalendarById($calendarId);
+
+ $calendar = new Calendar($this->backend, $calendarInfo, $l10n, $config, $logger);
+
+ $this->backend->updateShares(
+ shareable: $calendar,
+ add: [
+ ['href' => $principalGroup, 'readOnly' => false]
+ ],
+ remove: []
+ );
+
+ $this->backend->unshare(
+ shareable: $calendar,
+ principal: $principalUser
+ );
+
+ }
}
diff --git a/apps/dav/tests/unit/CalDAV/CalendarHomeTest.php b/apps/dav/tests/unit/CalDAV/CalendarHomeTest.php
index ee3bc2b0859..e25cc099bd6 100644
--- a/apps/dav/tests/unit/CalDAV/CalendarHomeTest.php
+++ b/apps/dav/tests/unit/CalDAV/CalendarHomeTest.php
@@ -1,31 +1,15 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright Copyright (c) 2017, Georg Ehrke
- *
- * @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 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\Tests\unit\CalDAV;
use OCA\DAV\AppInfo\PluginManager;
+use OCA\DAV\CalDAV\CachedSubscription;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\CalendarHome;
use OCA\DAV\CalDAV\Integration\ExternalCalendar;
@@ -35,25 +19,16 @@ use OCA\DAV\CalDAV\Trashbin\TrashbinHome;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Sabre\CalDAV\Schedule\Inbox;
+use Sabre\CalDAV\Subscriptions\Subscription;
use Sabre\DAV\MkCol;
use Test\TestCase;
class CalendarHomeTest extends TestCase {
-
- /** @var CalDavBackend | MockObject */
- private $backend;
-
- /** @var array */
- private $principalInfo = [];
-
- /** @var PluginManager */
- private $pluginManager;
-
- /** @var CalendarHome */
- private $calendarHome;
-
- /** @var MockObject|LoggerInterface */
- private $logger;
+ private CalDavBackend&MockObject $backend;
+ private array $principalInfo = [];
+ private PluginManager&MockObject $pluginManager;
+ private LoggerInterface&MockObject $logger;
+ private CalendarHome $calendarHome;
protected function setUp(): void {
parent::setUp();
@@ -68,18 +43,18 @@ class CalendarHomeTest extends TestCase {
$this->calendarHome = new CalendarHome(
$this->backend,
$this->principalInfo,
- $this->logger
+ $this->logger,
+ false
);
// Replace PluginManager with our mock
$reflection = new \ReflectionClass($this->calendarHome);
$reflectionProperty = $reflection->getProperty('pluginManager');
- $reflectionProperty->setAccessible(true);
$reflectionProperty->setValue($this->calendarHome, $this->pluginManager);
}
- public function testCreateCalendarValidName() {
- /** @var MkCol | MockObject $mkCol */
+ public function testCreateCalendarValidName(): void {
+ /** @var MkCol&MockObject $mkCol */
$mkCol = $this->createMock(MkCol::class);
$mkCol->method('getResourceType')
@@ -88,28 +63,28 @@ class CalendarHomeTest extends TestCase {
$mkCol->method('getRemainingValues')
->willReturn(['... properties ...']);
- $this->backend->expects($this->once())
+ $this->backend->expects(self::once())
->method('createCalendar')
->with('user-principal-123', 'name123', ['... properties ...']);
$this->calendarHome->createExtendedCollection('name123', $mkCol);
}
- public function testCreateCalendarReservedName() {
+ public function testCreateCalendarReservedName(): void {
$this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class);
$this->expectExceptionMessage('The resource you tried to create has a reserved name');
- /** @var MkCol | MockObject $mkCol */
+ /** @var MkCol&MockObject $mkCol */
$mkCol = $this->createMock(MkCol::class);
$this->calendarHome->createExtendedCollection('contact_birthdays', $mkCol);
}
- public function testCreateCalendarReservedNameAppGenerated() {
+ public function testCreateCalendarReservedNameAppGenerated(): void {
$this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class);
$this->expectExceptionMessage('The resource you tried to create has a reserved name');
- /** @var MkCol | MockObject $mkCol */
+ /** @var MkCol&MockObject $mkCol */
$mkCol = $this->createMock(MkCol::class);
$this->calendarHome->createExtendedCollection('app-generated--example--foo-1', $mkCol);
@@ -117,33 +92,33 @@ class CalendarHomeTest extends TestCase {
public function testGetChildren():void {
$this->backend
- ->expects($this->at(0))
+ ->expects(self::once())
->method('getCalendarsForUser')
->with('user-principal-123')
->willReturn([]);
$this->backend
- ->expects($this->at(1))
+ ->expects(self::once())
->method('getSubscriptionsForUser')
->with('user-principal-123')
->willReturn([]);
$calendarPlugin1 = $this->createMock(ICalendarProvider::class);
$calendarPlugin1
- ->expects($this->once())
+ ->expects(self::once())
->method('fetchAllForCalendarHome')
->with('user-principal-123')
->willReturn(['plugin1calendar1', 'plugin1calendar2']);
$calendarPlugin2 = $this->createMock(ICalendarProvider::class);
$calendarPlugin2
- ->expects($this->once())
+ ->expects(self::once())
->method('fetchAllForCalendarHome')
->with('user-principal-123')
->willReturn(['plugin2calendar1', 'plugin2calendar2']);
$this->pluginManager
- ->expects($this->once())
+ ->expects(self::once())
->method('getCalendarPlugins')
->with()
->willReturn([$calendarPlugin1, $calendarPlugin2]);
@@ -162,19 +137,25 @@ class CalendarHomeTest extends TestCase {
public function testGetChildNonAppGenerated():void {
$this->backend
- ->expects($this->at(0))
+ ->expects(self::once())
+ ->method('getCalendarByUri')
+ ->with('user-principal-123')
+ ->willReturn([]);
+
+ $this->backend
+ ->expects(self::once())
->method('getCalendarsForUser')
->with('user-principal-123')
->willReturn([]);
$this->backend
- ->expects($this->at(1))
+ ->expects(self::once())
->method('getSubscriptionsForUser')
->with('user-principal-123')
->willReturn([]);
$this->pluginManager
- ->expects($this->never())
+ ->expects(self::never())
->method('getCalendarPlugins');
$this->expectException(\Sabre\DAV\Exception\NotFound::class);
@@ -185,51 +166,57 @@ class CalendarHomeTest extends TestCase {
public function testGetChildAppGenerated():void {
$this->backend
- ->expects($this->at(0))
+ ->expects(self::once())
+ ->method('getCalendarByUri')
+ ->with('user-principal-123')
+ ->willReturn([]);
+
+ $this->backend
+ ->expects(self::once())
->method('getCalendarsForUser')
->with('user-principal-123')
->willReturn([]);
$this->backend
- ->expects($this->at(1))
+ ->expects(self::once())
->method('getSubscriptionsForUser')
->with('user-principal-123')
->willReturn([]);
$calendarPlugin1 = $this->createMock(ICalendarProvider::class);
$calendarPlugin1
- ->expects($this->once())
+ ->expects(self::once())
->method('getAppId')
->with()
->willReturn('calendar_plugin_1');
$calendarPlugin1
- ->expects($this->never())
+ ->expects(self::never())
->method('hasCalendarInCalendarHome');
$calendarPlugin1
- ->expects($this->never())
+ ->expects(self::never())
->method('getCalendarInCalendarHome');
$externalCalendarMock = $this->createMock(ExternalCalendar::class);
$calendarPlugin2 = $this->createMock(ICalendarProvider::class);
$calendarPlugin2
- ->expects($this->once())
+ ->expects(self::once())
->method('getAppId')
->with()
->willReturn('calendar_plugin_2');
$calendarPlugin2
- ->expects($this->once())
+ ->expects(self::once())
->method('hasCalendarInCalendarHome')
->with('user-principal-123', 'calendar-uri-from-backend')
->willReturn(true);
$calendarPlugin2
- ->expects($this->once())
+ ->expects(self::once())
->method('getCalendarInCalendarHome')
->with('user-principal-123', 'calendar-uri-from-backend')
->willReturn($externalCalendarMock);
$this->pluginManager
- ->expects($this->once())
+ ->expects(self::once())
->method('getCalendarPlugins')
->with()
->willReturn([$calendarPlugin1, $calendarPlugin2]);
@@ -237,4 +224,124 @@ class CalendarHomeTest extends TestCase {
$actual = $this->calendarHome->getChild('app-generated--calendar_plugin_2--calendar-uri-from-backend');
$this->assertEquals($externalCalendarMock, $actual);
}
+
+ public function testGetChildrenSubscriptions(): void {
+ $this->backend
+ ->expects(self::once())
+ ->method('getCalendarsForUser')
+ ->with('user-principal-123')
+ ->willReturn([]);
+
+ $this->backend
+ ->expects(self::once())
+ ->method('getSubscriptionsForUser')
+ ->with('user-principal-123')
+ ->willReturn([
+ [
+ 'id' => 'subscription-1',
+ 'uri' => 'subscription-1',
+ 'principaluri' => 'user-principal-123',
+ 'source' => 'https://localhost/subscription-1',
+ // A subscription array has actually more properties.
+ ],
+ [
+ 'id' => 'subscription-2',
+ 'uri' => 'subscription-2',
+ 'principaluri' => 'user-principal-123',
+ 'source' => 'https://localhost/subscription-2',
+ // A subscription array has actually more properties.
+ ]
+ ]);
+
+ /*
+ * @FIXME: PluginManager should be injected via constructor.
+ */
+
+ $pluginManager = $this->createMock(PluginManager::class);
+ $pluginManager
+ ->expects(self::once())
+ ->method('getCalendarPlugins')
+ ->with()
+ ->willReturn([]);
+
+ $calendarHome = new CalendarHome(
+ $this->backend,
+ $this->principalInfo,
+ $this->logger,
+ false
+ );
+
+ $reflection = new \ReflectionClass($calendarHome);
+ $reflectionProperty = $reflection->getProperty('pluginManager');
+ $reflectionProperty->setValue($calendarHome, $pluginManager);
+
+ $actual = $calendarHome->getChildren();
+
+ $this->assertCount(5, $actual);
+ $this->assertInstanceOf(Inbox::class, $actual[0]);
+ $this->assertInstanceOf(Outbox::class, $actual[1]);
+ $this->assertInstanceOf(TrashbinHome::class, $actual[2]);
+ $this->assertInstanceOf(Subscription::class, $actual[3]);
+ $this->assertInstanceOf(Subscription::class, $actual[4]);
+ }
+
+ public function testGetChildrenCachedSubscriptions(): void {
+ $this->backend
+ ->expects(self::once())
+ ->method('getCalendarsForUser')
+ ->with('user-principal-123')
+ ->willReturn([]);
+
+ $this->backend
+ ->expects(self::once())
+ ->method('getSubscriptionsForUser')
+ ->with('user-principal-123')
+ ->willReturn([
+ [
+ 'id' => 'subscription-1',
+ 'uri' => 'subscription-1',
+ 'principaluris' => 'user-principal-123',
+ 'source' => 'https://localhost/subscription-1',
+ // A subscription array has actually more properties.
+ ],
+ [
+ 'id' => 'subscription-2',
+ 'uri' => 'subscription-2',
+ 'principaluri' => 'user-principal-123',
+ 'source' => 'https://localhost/subscription-2',
+ // A subscription array has actually more properties.
+ ]
+ ]);
+
+ /*
+ * @FIXME: PluginManager should be injected via constructor.
+ */
+
+ $pluginManager = $this->createMock(PluginManager::class);
+ $pluginManager
+ ->expects(self::once())
+ ->method('getCalendarPlugins')
+ ->with()
+ ->willReturn([]);
+
+ $calendarHome = new CalendarHome(
+ $this->backend,
+ $this->principalInfo,
+ $this->logger,
+ true
+ );
+
+ $reflection = new \ReflectionClass($calendarHome);
+ $reflectionProperty = $reflection->getProperty('pluginManager');
+ $reflectionProperty->setValue($calendarHome, $pluginManager);
+
+ $actual = $calendarHome->getChildren();
+
+ $this->assertCount(5, $actual);
+ $this->assertInstanceOf(Inbox::class, $actual[0]);
+ $this->assertInstanceOf(Outbox::class, $actual[1]);
+ $this->assertInstanceOf(TrashbinHome::class, $actual[2]);
+ $this->assertInstanceOf(CachedSubscription::class, $actual[3]);
+ $this->assertInstanceOf(CachedSubscription::class, $actual[4]);
+ }
}
diff --git a/apps/dav/tests/unit/CalDAV/CalendarImplTest.php b/apps/dav/tests/unit/CalDAV/CalendarImplTest.php
index af8c056cac7..d6a8f3b910e 100644
--- a/apps/dav/tests/unit/CalDAV/CalendarImplTest.php
+++ b/apps/dav/tests/unit/CalDAV/CalendarImplTest.php
@@ -1,77 +1,68 @@
<?php
+
+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 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\Tests\unit\CalDAV;
+use Generator;
+use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\Calendar;
use OCA\DAV\CalDAV\CalendarImpl;
+use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
+use OCA\DAV\CalDAV\Schedule\Plugin;
+use OCA\DAV\Connector\Sabre\Server;
+use OCP\Calendar\Exceptions\CalendarException;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VEvent;
+use Sabre\VObject\ITip\Message;
+use Sabre\VObject\Reader;
class CalendarImplTest extends \Test\TestCase {
-
- /** @var CalendarImpl */
- private $calendarImpl;
-
- /** @var Calendar | \PHPUnit\Framework\MockObject\MockObject */
- private $calendar;
-
- /** @var array */
- private $calendarInfo;
-
- /** @var CalDavBackend | \PHPUnit\Framework\MockObject\MockObject */
- private $backend;
+ private Calendar&MockObject $calendar;
+ private array $calendarInfo;
+ private CalDavBackend&MockObject $backend;
+ private CalendarImpl $calendarImpl;
+ private array $mockExportCollection;
protected function setUp(): void {
parent::setUp();
$this->calendar = $this->createMock(Calendar::class);
$this->calendarInfo = [
- 'id' => 'fancy_id_123',
+ 'id' => 1,
'{DAV:}displayname' => 'user readable name 123',
'{http://apple.com/ns/ical/}calendar-color' => '#AABBCC',
+ 'uri' => '/this/is/a/uri',
+ 'principaluri' => 'principal/users/foobar'
];
$this->backend = $this->createMock(CalDavBackend::class);
- $this->calendarImpl = new CalendarImpl($this->calendar,
- $this->calendarInfo, $this->backend);
+ $this->calendarImpl = new CalendarImpl(
+ $this->calendar,
+ $this->calendarInfo,
+ $this->backend
+ );
}
- public function testGetKey() {
- $this->assertEquals($this->calendarImpl->getKey(), 'fancy_id_123');
+ public function testGetKey(): void {
+ $this->assertEquals($this->calendarImpl->getKey(), 1);
}
- public function testGetDisplayname() {
- $this->assertEquals($this->calendarImpl->getDisplayName(),'user readable name 123');
+ public function testGetDisplayname(): void {
+ $this->assertEquals($this->calendarImpl->getDisplayName(), 'user readable name 123');
}
- public function testGetDisplayColor() {
+ public function testGetDisplayColor(): void {
$this->assertEquals($this->calendarImpl->getDisplayColor(), '#AABBCC');
}
- public function testSearch() {
+ public function testSearch(): void {
$this->backend->expects($this->once())
->method('search')
->with($this->calendarInfo, 'abc', ['def'], ['ghi'], 42, 1337)
@@ -81,48 +72,237 @@ class CalendarImplTest extends \Test\TestCase {
$this->assertEquals($result, ['SEARCHRESULTS']);
}
- public function testGetPermissionRead() {
+ public function testGetPermissionRead(): void {
$this->calendar->expects($this->once())
->method('getACL')
->with()
->willReturn([
- ['privilege' => '{DAV:}read']
+ ['privilege' => '{DAV:}read', 'principal' => 'principal/users/foobar'],
+ ['privilege' => '{DAV:}read', 'principal' => 'principal/users/other'],
+ ['privilege' => '{DAV:}write', 'principal' => 'principal/users/other'],
+ ['privilege' => '{DAV:}all', 'principal' => 'principal/users/other'],
]);
$this->assertEquals(1, $this->calendarImpl->getPermissions());
}
- public function testGetPermissionWrite() {
+ public function testGetPermissionWrite(): void {
$this->calendar->expects($this->once())
->method('getACL')
->with()
->willReturn([
- ['privilege' => '{DAV:}write']
+ ['privilege' => '{DAV:}write', 'principal' => 'principal/users/foobar'],
+ ['privilege' => '{DAV:}read', 'principal' => 'principal/users/other'],
+ ['privilege' => '{DAV:}all', 'principal' => 'principal/users/other'],
]);
$this->assertEquals(6, $this->calendarImpl->getPermissions());
}
- public function testGetPermissionReadWrite() {
+ public function testGetPermissionReadWrite(): void {
$this->calendar->expects($this->once())
->method('getACL')
->with()
->willReturn([
- ['privilege' => '{DAV:}read'],
- ['privilege' => '{DAV:}write']
+ ['privilege' => '{DAV:}write', 'principal' => 'principal/users/foobar'],
+ ['privilege' => '{DAV:}read', 'principal' => 'principal/users/foobar'],
+ ['privilege' => '{DAV:}all', 'principal' => 'principal/users/other'],
]);
$this->assertEquals(7, $this->calendarImpl->getPermissions());
}
- public function testGetPermissionAll() {
+ public function testGetPermissionAll(): void {
$this->calendar->expects($this->once())
->method('getACL')
->with()
->willReturn([
- ['privilege' => '{DAV:}all']
+ ['privilege' => '{DAV:}all', 'principal' => 'principal/users/foobar'],
]);
$this->assertEquals(31, $this->calendarImpl->getPermissions());
}
+
+ public function testHandleImipMessage(): void {
+ $message = <<<EOF
+BEGIN:VCALENDAR
+PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
+METHOD:REPLY
+VERSION:2.0
+BEGIN:VEVENT
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:lewis@stardew-tent-living.com
+ORGANIZER:mailto:pierre@generalstore.com
+UID:aUniqueUid
+SEQUENCE:2
+REQUEST-STATUS:2.0;Success
+END:VEVENT
+END:VCALENDAR
+EOF;
+
+ /** @var CustomPrincipalPlugin|MockObject $authPlugin */
+ $authPlugin = $this->createMock(CustomPrincipalPlugin::class);
+ $authPlugin->expects(self::once())
+ ->method('setCurrentPrincipal')
+ ->with($this->calendar->getPrincipalURI());
+
+ /** @var \Sabre\DAVACL\Plugin|MockObject $aclPlugin */
+ $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class);
+
+ /** @var Plugin|MockObject $schedulingPlugin */
+ $schedulingPlugin = $this->createMock(Plugin::class);
+ $iTipMessage = $this->getITipMessage($message);
+ $iTipMessage->recipient = 'mailto:lewis@stardew-tent-living.com';
+
+ $server = $this->createMock(Server::class);
+ $server->expects($this->any())
+ ->method('getPlugin')
+ ->willReturnMap([
+ ['auth', $authPlugin],
+ ['acl', $aclPlugin],
+ ['caldav-schedule', $schedulingPlugin]
+ ]);
+ $server->expects(self::once())
+ ->method('emit');
+
+ $invitationResponseServer = $this->createPartialMock(InvitationResponseServer::class, ['getServer', 'isExternalAttendee']);
+ $invitationResponseServer->server = $server;
+ $invitationResponseServer->expects($this->any())
+ ->method('getServer')
+ ->willReturn($server);
+ $invitationResponseServer->expects(self::once())
+ ->method('isExternalAttendee')
+ ->willReturn(false);
+
+ $calendarImpl = $this->getMockBuilder(CalendarImpl::class)
+ ->setConstructorArgs([$this->calendar, $this->calendarInfo, $this->backend])
+ ->onlyMethods(['getInvitationResponseServer'])
+ ->getMock();
+ $calendarImpl->expects($this->once())
+ ->method('getInvitationResponseServer')
+ ->willReturn($invitationResponseServer);
+
+ $calendarImpl->handleIMipMessage('filename.ics', $message);
+ }
+
+ public function testHandleImipMessageNoCalendarUri(): void {
+ /** @var CustomPrincipalPlugin|MockObject $authPlugin */
+ $authPlugin = $this->createMock(CustomPrincipalPlugin::class);
+ $authPlugin->expects(self::once())
+ ->method('setCurrentPrincipal')
+ ->with($this->calendar->getPrincipalURI());
+ unset($this->calendarInfo['uri']);
+
+ /** @var Plugin|MockObject $schedulingPlugin */
+ $schedulingPlugin = $this->createMock(Plugin::class);
+
+ /** @var \Sabre\DAVACL\Plugin|MockObject $schedulingPlugin */
+ $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class);
+
+ $server
+ = $this->createMock(Server::class);
+ $server->expects($this->any())
+ ->method('getPlugin')
+ ->willReturnMap([
+ ['auth', $authPlugin],
+ ['acl', $aclPlugin],
+ ['caldav-schedule', $schedulingPlugin]
+ ]);
+ $server->expects(self::never())
+ ->method('emit');
+
+ $invitationResponseServer = $this->createPartialMock(InvitationResponseServer::class, ['getServer']);
+ $invitationResponseServer->server = $server;
+ $invitationResponseServer->expects($this->any())
+ ->method('getServer')
+ ->willReturn($server);
+
+ $calendarImpl = $this->getMockBuilder(CalendarImpl::class)
+ ->setConstructorArgs([$this->calendar, $this->calendarInfo, $this->backend])
+ ->onlyMethods(['getInvitationResponseServer'])
+ ->getMock();
+ $calendarImpl->expects($this->once())
+ ->method('getInvitationResponseServer')
+ ->willReturn($invitationResponseServer);
+
+ $message = <<<EOF
+BEGIN:VCALENDAR
+PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
+METHOD:REPLY
+VERSION:2.0
+BEGIN:VEVENT
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:lewis@stardew-tent-living.com
+ORGANIZER:mailto:pierre@generalstore.com
+UID:aUniqueUid
+SEQUENCE:2
+REQUEST-STATUS:2.0;Success
+END:VEVENT
+END:VCALENDAR
+EOF;
+
+ $this->expectException(CalendarException::class);
+ $calendarImpl->handleIMipMessage('filename.ics', $message);
+ }
+
+ private function getITipMessage($calendarData): Message {
+ $iTipMessage = new Message();
+ /** @var VCalendar $vObject */
+ $vObject = Reader::read($calendarData);
+ /** @var VEvent $vEvent */
+ $vEvent = $vObject->{'VEVENT'};
+ $orgaizer = $vEvent->{'ORGANIZER'}->getValue();
+ $attendee = $vEvent->{'ATTENDEE'}->getValue();
+
+ $iTipMessage->method = $vObject->{'METHOD'}->getValue();
+ $iTipMessage->recipient = $orgaizer;
+ $iTipMessage->sender = $attendee;
+ $iTipMessage->uid = isset($vEvent->{'UID'}) ? $vEvent->{'UID'}->getValue() : '';
+ $iTipMessage->component = 'VEVENT';
+ $iTipMessage->sequence = isset($vEvent->{'SEQUENCE'}) ? (int)$vEvent->{'SEQUENCE'}->getValue() : 0;
+ $iTipMessage->message = $vObject;
+ return $iTipMessage;
+ }
+
+ protected function mockExportGenerator(): Generator {
+ foreach ($this->mockExportCollection as $entry) {
+ yield $entry;
+ }
+ }
+
+ public function testExport(): void {
+ // Arrange
+ // construct calendar with a 1 hour event and same start/end time zones
+ $vCalendar = new VCalendar();
+ /** @var VEvent $vEvent */
+ $vEvent = $vCalendar->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ // construct data store return
+ $this->mockExportCollection[] = [
+ 'id' => 1,
+ 'calendardata' => $vCalendar->serialize()
+ ];
+ $this->backend->expects($this->once())
+ ->method('exportCalendar')
+ ->with(1, $this->backend::CALENDAR_TYPE_CALENDAR, null)
+ ->willReturn($this->mockExportGenerator());
+
+ // Act
+ foreach ($this->calendarImpl->export(null) as $entry) {
+ $exported[] = $entry;
+ }
+
+ // Assert
+ $this->assertCount(1, $exported, 'Invalid exported items count');
+ }
+
}
diff --git a/apps/dav/tests/unit/CalDAV/CalendarManagerTest.php b/apps/dav/tests/unit/CalDAV/CalendarManagerTest.php
index f16a06f953d..e8159ffe07c 100644
--- a/apps/dav/tests/unit/CalDAV/CalendarManagerTest.php
+++ b/apps/dav/tests/unit/CalDAV/CalendarManagerTest.php
@@ -1,27 +1,9 @@
<?php
+
+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 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\Tests\unit\CalDAV;
@@ -36,21 +18,11 @@ use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
class CalendarManagerTest extends \Test\TestCase {
-
- /** @var CalDavBackend | MockObject */
- private $backend;
-
- /** @var IL10N | MockObject */
- private $l10n;
-
- /** @var IConfig|MockObject */
- private $config;
-
- /** @var CalendarManager */
- private $manager;
-
- /** @var MockObject|LoggerInterface */
- private $logger;
+ private CalDavBackend&MockObject $backend;
+ private IL10N&MockObject $l10n;
+ private IConfig&MockObject $config;
+ private LoggerInterface&MockObject $logger;
+ private CalendarManager $manager;
protected function setUp(): void {
parent::setUp();
@@ -66,7 +38,7 @@ class CalendarManagerTest extends \Test\TestCase {
);
}
- public function testSetupCalendarProvider() {
+ public function testSetupCalendarProvider(): void {
$this->backend->expects($this->once())
->method('getCalendarsForUser')
->with('principals/users/user123')
@@ -75,24 +47,18 @@ class CalendarManagerTest extends \Test\TestCase {
['id' => 456, 'uri' => 'blablub2'],
]);
- /** @var IManager | MockObject $calendarManager */
+ /** @var IManager&MockObject $calendarManager */
$calendarManager = $this->createMock(Manager::class);
- $calendarManager->expects($this->at(0))
+ $registeredIds = [];
+ $calendarManager->expects($this->exactly(2))
->method('registerCalendar')
- ->willReturnCallback(function () {
- $parameter = func_get_arg(0);
+ ->willReturnCallback(function ($parameter) use (&$registeredIds): void {
$this->assertInstanceOf(CalendarImpl::class, $parameter);
- $this->assertEquals(123, $parameter->getKey());
- });
-
- $calendarManager->expects($this->at(1))
- ->method('registerCalendar')
- ->willReturnCallback(function () {
- $parameter = func_get_arg(0);
- $this->assertInstanceOf(CalendarImpl::class, $parameter);
- $this->assertEquals(456, $parameter->getKey());
+ $registeredIds[] = $parameter->getKey();
});
$this->manager->setupCalendarProvider($calendarManager, 'user123');
+
+ $this->assertEquals(['123','456'], $registeredIds);
}
}
diff --git a/apps/dav/tests/unit/CalDAV/CalendarTest.php b/apps/dav/tests/unit/CalDAV/CalendarTest.php
index 95cedb6da6a..b0d3c35bfe7 100644
--- a/apps/dav/tests/unit/CalDAV/CalendarTest.php
+++ b/apps/dav/tests/unit/CalDAV/CalendarTest.php
@@ -1,30 +1,10 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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 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: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Tests\unit\CalDAV;
@@ -40,20 +20,13 @@ use Sabre\VObject\Reader;
use Test\TestCase;
class CalendarTest extends TestCase {
-
- /** @var IL10N */
- protected $l10n;
-
- /** @var IConfig */
- protected $config;
-
- /** @var MockObject|LoggerInterface */
- protected $logger;
+ protected IL10N&MockObject $l10n;
+ protected IConfig&MockObject $config;
+ protected LoggerInterface&MockObject $logger;
protected function setUp(): void {
parent::setUp();
- $this->l10n = $this->getMockBuilder(IL10N::class)
- ->disableOriginalConstructor()->getMock();
+ $this->l10n = $this->createMock(IL10N::class);
$this->config = $this->createMock(IConfig::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->l10n
@@ -64,13 +37,14 @@ class CalendarTest extends TestCase {
});
}
- public function testDelete() {
- /** @var MockObject | CalDavBackend $backend */
- $backend = $this->getMockBuilder(CalDavBackend::class)->disableOriginalConstructor()->getMock();
- $backend->expects($this->once())->method('updateShares');
- $backend->expects($this->any())->method('getShares')->willReturn([
- ['href' => 'principal:user2']
- ]);
+ public function testDelete(): void {
+ /** @var CalDavBackend&MockObject $backend */
+ $backend = $this->createMock(CalDavBackend::class);
+ $backend->expects($this->never())
+ ->method('updateShares');
+ $backend->expects($this->once())
+ ->method('unshare');
+
$calendarInfo = [
'{http://owncloud.org/ns}owner-principal' => 'user1',
'principaluri' => 'user2',
@@ -82,15 +56,14 @@ class CalendarTest extends TestCase {
}
- public function testDeleteFromGroup() {
- $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ public function testDeleteFromGroup(): void {
+ /** @var CalDavBackend&MockObject $backend */
+ $backend = $this->createMock(CalDavBackend::class);
+ $backend->expects($this->never())
+ ->method('updateShares');
+ $backend->expects($this->once())
+ ->method('unshare');
- /** @var MockObject | CalDavBackend $backend */
- $backend = $this->getMockBuilder(CalDavBackend::class)->disableOriginalConstructor()->getMock();
- $backend->expects($this->never())->method('updateShares');
- $backend->expects($this->any())->method('getShares')->willReturn([
- ['href' => 'principal:group2']
- ]);
$calendarInfo = [
'{http://owncloud.org/ns}owner-principal' => 'user1',
'principaluri' => 'user2',
@@ -101,8 +74,8 @@ class CalendarTest extends TestCase {
$c->delete();
}
- public function testDeleteOwn() {
- /** @var MockObject | CalDavBackend $backend */
+ public function testDeleteOwn(): void {
+ /** @var CalDavBackend&MockObject $backend */
$backend = $this->createMock(CalDavBackend::class);
$backend->expects($this->never())->method('updateShares');
$backend->expects($this->never())->method('getShares');
@@ -122,8 +95,8 @@ class CalendarTest extends TestCase {
$c->delete();
}
- public function testDeleteBirthdayCalendar() {
- /** @var MockObject | CalDavBackend $backend */
+ public function testDeleteBirthdayCalendar(): void {
+ /** @var CalDavBackend&MockObject $backend */
$backend = $this->createMock(CalDavBackend::class);
$backend->expects($this->once())->method('deleteCalendar')
->with(666);
@@ -137,13 +110,14 @@ class CalendarTest extends TestCase {
'principaluri' => 'principals/users/user1',
'id' => 666,
'uri' => 'contact_birthdays',
+ '{DAV:}displayname' => 'Test',
];
$c = new Calendar($backend, $calendarInfo, $this->l10n, $this->config, $this->logger);
$c->delete();
}
- public function dataPropPatch() {
+ public static function dataPropPatch(): array {
return [
['user1', 'user2', [], true],
['user1', 'user2', [
@@ -170,12 +144,10 @@ class CalendarTest extends TestCase {
];
}
- /**
- * @dataProvider dataPropPatch
- */
- public function testPropPatch($ownerPrincipal, $principalUri, $mutations, $shared) {
- /** @var MockObject | CalDavBackend $backend */
- $backend = $this->getMockBuilder(CalDavBackend::class)->disableOriginalConstructor()->getMock();
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataPropPatch')]
+ public function testPropPatch(string $ownerPrincipal, string $principalUri, array $mutations, bool $shared): void {
+ /** @var CalDavBackend&MockObject $backend */
+ $backend = $this->createMock(CalDavBackend::class);
$calendarInfo = [
'{http://owncloud.org/ns}owner-principal' => $ownerPrincipal,
'principaluri' => $principalUri,
@@ -194,18 +166,17 @@ class CalendarTest extends TestCase {
$this->addToAssertionCount(1);
}
- /**
- * @dataProvider providesReadOnlyInfo
- */
- public function testAcl($expectsWrite, $readOnlyValue, $hasOwnerSet, $uri = 'default') {
- /** @var MockObject | CalDavBackend $backend */
- $backend = $this->getMockBuilder(CalDavBackend::class)->disableOriginalConstructor()->getMock();
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesReadOnlyInfo')]
+ public function testAcl($expectsWrite, $readOnlyValue, $hasOwnerSet, $uri = 'default'): void {
+ /** @var CalDavBackend&MockObject $backend */
+ $backend = $this->createMock(CalDavBackend::class);
$backend->expects($this->any())->method('applyShareAcl')->willReturnArgument(1);
$calendarInfo = [
'principaluri' => 'user2',
'id' => 666,
'uri' => $uri
];
+ $calendarInfo['{DAV:}displayname'] = 'Test';
if (!is_null($readOnlyValue)) {
$calendarInfo['{http://owncloud.org/ns}read-only'] = $readOnlyValue;
}
@@ -283,7 +254,7 @@ class CalendarTest extends TestCase {
$this->assertEquals($expectedAcl, $childAcl);
}
- public function providesReadOnlyInfo() {
+ public static function providesReadOnlyInfo(): array {
return [
'read-only property not set' => [true, null, true],
'read-only property is false' => [true, false, true],
@@ -295,18 +266,14 @@ class CalendarTest extends TestCase {
];
}
- /**
- * @dataProvider providesConfidentialClassificationData
- * @param int $expectedChildren
- * @param bool $isShared
- */
- public function testPrivateClassification($expectedChildren, $isShared) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesConfidentialClassificationData')]
+ public function testPrivateClassification(int $expectedChildren, bool $isShared): void {
$calObject0 = ['uri' => 'event-0', 'classification' => CalDavBackend::CLASSIFICATION_PUBLIC];
$calObject1 = ['uri' => 'event-1', 'classification' => CalDavBackend::CLASSIFICATION_CONFIDENTIAL];
$calObject2 = ['uri' => 'event-2', 'classification' => CalDavBackend::CLASSIFICATION_PRIVATE];
- /** @var MockObject | CalDavBackend $backend */
- $backend = $this->getMockBuilder(CalDavBackend::class)->disableOriginalConstructor()->getMock();
+ /** @var CalDavBackend&MockObject $backend */
+ $backend = $this->createMock(CalDavBackend::class);
$backend->expects($this->any())->method('getCalendarObjects')->willReturn([
$calObject0, $calObject1, $calObject2
]);
@@ -337,12 +304,8 @@ class CalendarTest extends TestCase {
$this->assertEquals(!$isShared, $c->childExists('event-2'));
}
- /**
- * @dataProvider providesConfidentialClassificationData
- * @param int $expectedChildren
- * @param bool $isShared
- */
- public function testConfidentialClassification($expectedChildren, $isShared) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesConfidentialClassificationData')]
+ public function testConfidentialClassification(int $expectedChildren, bool $isShared): void {
$start = '20160609';
$end = '20160610';
@@ -392,8 +355,8 @@ EOD;
$calObject1 = ['uri' => 'event-1', 'classification' => CalDavBackend::CLASSIFICATION_CONFIDENTIAL, 'calendardata' => $calData];
$calObject2 = ['uri' => 'event-2', 'classification' => CalDavBackend::CLASSIFICATION_PRIVATE];
- /** @var MockObject | CalDavBackend $backend */
- $backend = $this->getMockBuilder(CalDavBackend::class)->disableOriginalConstructor()->getMock();
+ /** @var CalDavBackend&MockObject $backend */
+ $backend = $this->createMock(CalDavBackend::class);
$backend->expects($this->any())->method('getCalendarObjects')->willReturn([
$calObject0, $calObject1, $calObject2
]);
@@ -440,7 +403,7 @@ EOD;
$l10n->expects($this->once())
->method('t')
->with('Busy')
- ->willReturn("Translated busy");
+ ->willReturn('Translated busy');
} else {
$l10n->expects($this->never())
->method('t');
@@ -457,14 +420,14 @@ EOD;
}
}
- public function providesConfidentialClassificationData() {
+ public static function providesConfidentialClassificationData(): array {
return [
[3, false],
[2, true]
];
}
- public function testRemoveVAlarms() {
+ public function testRemoveVAlarms(): void {
$publicObjectData = <<<EOD
BEGIN:VCALENDAR
VERSION:2.0
@@ -560,7 +523,7 @@ EOD;
'classification' => CalDavBackend::CLASSIFICATION_CONFIDENTIAL,
'calendardata' => $confidentialObjectData];
- /** @var MockObject | CalDavBackend $backend */
+ /** @var CalDavBackend&MockObject $backend */
$backend = $this->createMock(CalDavBackend::class);
$backend->expects($this->any())
->method('getCalendarObjects')
@@ -639,7 +602,7 @@ EOD;
$this->fixLinebreak($confidentialObjectCleaned));
}
- private function fixLinebreak($str) {
+ private function fixLinebreak(string $str): string {
return preg_replace('~(*BSR_ANYCRLF)\R~', "\r\n", $str);
}
}
diff --git a/apps/dav/tests/unit/CalDAV/DefaultCalendarValidatorTest.php b/apps/dav/tests/unit/CalDAV/DefaultCalendarValidatorTest.php
new file mode 100644
index 00000000000..194009827da
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/DefaultCalendarValidatorTest.php
@@ -0,0 +1,171 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use OCA\DAV\CalDAV\Calendar;
+use OCA\DAV\CalDAV\DefaultCalendarValidator;
+use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
+use Test\TestCase;
+
+class DefaultCalendarValidatorTest extends TestCase {
+ private DefaultCalendarValidator $validator;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->validator = new DefaultCalendarValidator();
+ }
+
+ public function testValidateScheduleDefaultCalendar(): void {
+ $node = $this->createMock(Calendar::class);
+ $node->expects(self::once())
+ ->method('isSubscription')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('canWrite')
+ ->willReturn(true);
+ $node->expects(self::once())
+ ->method('isShared')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('isDeleted')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('getProperties')
+ ->willReturn([
+ '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VEVENT']),
+ ]);
+
+ $this->validator->validateScheduleDefaultCalendar($node);
+ }
+
+ public function testValidateScheduleDefaultCalendarWithEmptyProperties(): void {
+ $node = $this->createMock(Calendar::class);
+ $node->expects(self::once())
+ ->method('isSubscription')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('canWrite')
+ ->willReturn(true);
+ $node->expects(self::once())
+ ->method('isShared')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('isDeleted')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('getProperties')
+ ->willReturn([]);
+
+ $this->validator->validateScheduleDefaultCalendar($node);
+ }
+
+ public function testValidateScheduleDefaultCalendarWithSubscription(): void {
+ $node = $this->createMock(Calendar::class);
+ $node->expects(self::once())
+ ->method('isSubscription')
+ ->willReturn(true);
+ $node->expects(self::never())
+ ->method('canWrite');
+ $node->expects(self::never())
+ ->method('isShared');
+ $node->expects(self::never())
+ ->method('isDeleted');
+ $node->expects(self::never())
+ ->method('getProperties');
+
+ $this->expectException(\Sabre\DAV\Exception::class);
+ $this->validator->validateScheduleDefaultCalendar($node);
+ }
+
+ public function testValidateScheduleDefaultCalendarWithoutWrite(): void {
+ $node = $this->createMock(Calendar::class);
+ $node->expects(self::once())
+ ->method('isSubscription')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('canWrite')
+ ->willReturn(false);
+ $node->expects(self::never())
+ ->method('isShared');
+ $node->expects(self::never())
+ ->method('isDeleted');
+ $node->expects(self::never())
+ ->method('getProperties');
+
+ $this->expectException(\Sabre\DAV\Exception::class);
+ $this->validator->validateScheduleDefaultCalendar($node);
+ }
+
+ public function testValidateScheduleDefaultCalendarWithShared(): void {
+ $node = $this->createMock(Calendar::class);
+ $node->expects(self::once())
+ ->method('isSubscription')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('canWrite')
+ ->willReturn(true);
+ $node->expects(self::once())
+ ->method('isShared')
+ ->willReturn(true);
+ $node->expects(self::never())
+ ->method('isDeleted');
+ $node->expects(self::never())
+ ->method('getProperties');
+
+ $this->expectException(\Sabre\DAV\Exception::class);
+ $this->validator->validateScheduleDefaultCalendar($node);
+ }
+
+ public function testValidateScheduleDefaultCalendarWithDeleted(): void {
+ $node = $this->createMock(Calendar::class);
+ $node->expects(self::once())
+ ->method('isSubscription')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('canWrite')
+ ->willReturn(true);
+ $node->expects(self::once())
+ ->method('isShared')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('isDeleted')
+ ->willReturn(true);
+ $node->expects(self::never())
+ ->method('getProperties');
+
+ $this->expectException(\Sabre\DAV\Exception::class);
+ $this->validator->validateScheduleDefaultCalendar($node);
+ }
+
+ public function testValidateScheduleDefaultCalendarWithoutVeventSupport(): void {
+ $node = $this->createMock(Calendar::class);
+ $node->expects(self::once())
+ ->method('isSubscription')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('canWrite')
+ ->willReturn(true);
+ $node->expects(self::once())
+ ->method('isShared')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('isDeleted')
+ ->willReturn(false);
+ $node->expects(self::once())
+ ->method('getProperties')
+ ->willReturn([
+ '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO']),
+ ]);
+
+ $this->expectException(\Sabre\DAV\Exception::class);
+ $this->validator->validateScheduleDefaultCalendar($node);
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/EventComparisonServiceTest.php b/apps/dav/tests/unit/CalDAV/EventComparisonServiceTest.php
new file mode 100644
index 00000000000..90b6f9ec0db
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/EventComparisonServiceTest.php
@@ -0,0 +1,188 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use OCA\DAV\CalDAV\EventComparisonService;
+use Sabre\VObject\Component\VCalendar;
+use Test\TestCase;
+
+class EventComparisonServiceTest extends TestCase {
+ private EventComparisonService $eventComparisonService;
+
+ protected function setUp(): void {
+ $this->eventComparisonService = new EventComparisonService();
+ }
+
+ public function testNoModifiedEvent(): void {
+ $vCalendarOld = new VCalendar();
+ $vCalendarNew = new VCalendar();
+
+ $vEventOld = $vCalendarOld->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z',
+ ]);
+ $vEventOld->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $vEventOld->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+
+ $vEventNew = $vCalendarNew->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z',
+ ]);
+ $vEventNew->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $vEventNew->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+
+ $result = $this->eventComparisonService->findModified($vCalendarNew, $vCalendarOld);
+ $this->assertEmpty($result['old']);
+ $this->assertEmpty($result['new']);
+ }
+
+ public function testNewEvent(): void {
+ $vCalendarOld = null;
+ $vCalendarNew = new VCalendar();
+
+ $vEventNew = $vCalendarNew->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z',
+ ]);
+ $vEventNew->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $vEventNew->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+
+ $result = $this->eventComparisonService->findModified($vCalendarNew, $vCalendarOld);
+ $this->assertNull($result['old']);
+ $this->assertEquals([$vEventNew], $result['new']);
+ }
+
+ public function testModifiedUnmodifiedEvent(): void {
+ $vCalendarOld = new VCalendar();
+ $vCalendarNew = new VCalendar();
+
+ $vEventOld1 = $vCalendarOld->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ ]);
+ $vEventOld1->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $vEventOld1->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+
+ $vEventOld2 = $vCalendarOld->add('VEVENT', [
+ 'UID' => 'uid-1235',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ ]);
+ $vEventOld2->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $vEventOld2->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+
+ $vEventNew1 = $vCalendarNew->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ ]);
+ $vEventNew1->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $vEventNew1->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+
+ $vEventNew2 = $vCalendarNew->add('VEVENT', [
+ 'UID' => 'uid-1235',
+ 'LAST-MODIFIED' => 123457,
+ 'SEQUENCE' => 3,
+ 'SUMMARY' => 'Fellowship meeting 2',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ ]);
+ $vEventNew2->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $vEventNew2->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+
+ $result = $this->eventComparisonService->findModified($vCalendarNew, $vCalendarOld);
+ $this->assertEquals([$vEventOld2], $result['old']);
+ $this->assertEquals([$vEventNew2], $result['new']);
+ }
+
+ // First test to certify fix for issue nextcloud/server#41084
+ public function testSequenceNumberIncrementDetectedForFirstModificationToEventWithoutZeroInit(): void {
+ $vCalendarOld = new VCalendar();
+ $vCalendarNew = new VCalendar();
+
+ $vEventOld = $vCalendarOld->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ // 'SEQUENCE' => 0, // sequence number may not be set to zero during event creation and instead fully omitted
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z',
+ ]);
+ $vEventOld->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $vEventOld->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+
+ $vEventNew = $vCalendarNew->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 1,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z',
+ ]);
+ $vEventNew->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $vEventNew->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+
+ $result = $this->eventComparisonService->findModified($vCalendarNew, $vCalendarOld);
+ $this->assertEquals([$vEventOld], $result['old']);
+ $this->assertEquals([$vEventNew], $result['new']);
+ }
+
+ // Second test to certify fix for issue nextcloud/server#41084
+ public function testSequenceNumberIncrementDetectedForFirstModificationToEventWithZeroInit(): void {
+ $vCalendarOld = new VCalendar();
+ $vCalendarNew = new VCalendar();
+
+ $vEventOld = $vCalendarOld->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 0,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z',
+ ]);
+ $vEventOld->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $vEventOld->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+
+ $vEventNew = $vCalendarNew->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 1,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z',
+ ]);
+ $vEventNew->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $vEventNew->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+
+ $result = $this->eventComparisonService->findModified($vCalendarNew, $vCalendarOld);
+ $this->assertEquals([$vEventOld], $result['old']);
+ $this->assertEquals([$vEventNew], $result['new']);
+ }
+
+
+}
diff --git a/apps/dav/tests/unit/CalDAV/EventReaderTest.php b/apps/dav/tests/unit/CalDAV/EventReaderTest.php
new file mode 100644
index 00000000000..3bd4f9d85c2
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/EventReaderTest.php
@@ -0,0 +1,1087 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use DateTimeZone;
+use OCA\DAV\CalDAV\EventReader;
+use Sabre\VObject\Component\VCalendar;
+use Test\TestCase;
+
+class EventReaderTest extends TestCase {
+
+ private VCalendar $vCalendar1a;
+ private VCalendar $vCalendar1b;
+ private VCalendar $vCalendar1c;
+ private VCalendar $vCalendar1d;
+ private VCalendar $vCalendar1e;
+ private VCalendar $vCalendar2;
+ private VCalendar $vCalendar3;
+
+ protected function setUp(): void {
+
+ parent::setUp();
+
+ // construct calendar with a 1 hour event and same start/end time zones
+ $this->vCalendar1a = new VCalendar();
+ $vEvent = $this->vCalendar1a->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a 1 hour event and different start/end time zones
+ $this->vCalendar1b = new VCalendar();
+ $vEvent = $this->vCalendar1b->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Vancouver']);
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a 1 hour event and global time zone
+ $this->vCalendar1c = new VCalendar();
+ // time zone component
+ $vTimeZone = $this->vCalendar1c->add('VTIMEZONE');
+ $vTimeZone->add('TZID', 'America/Toronto');
+ // event component
+ $vEvent = $this->vCalendar1c->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000');
+ $vEvent->add('DTEND', '20240701T090000');
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a 1 hour event and no time zone
+ $this->vCalendar1d = new VCalendar();
+ $vEvent = $this->vCalendar1d->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000');
+ $vEvent->add('DTEND', '20240701T090000');
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a 1 hour event and Microsoft time zone
+ $this->vCalendar1e = new VCalendar();
+ $vEvent = $this->vCalendar1e->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'Eastern Standard Time']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'Eastern Standard Time']);
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a full day event
+ $this->vCalendar2 = new VCalendar();
+ // time zone component
+ $vTimeZone = $this->vCalendar2->add('VTIMEZONE');
+ $vTimeZone->add('TZID', 'America/Toronto');
+ // event component
+ $vEvent = $this->vCalendar2->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701');
+ $vEvent->add('DTEND', '20240702');
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a multi day event
+ $this->vCalendar3 = new VCalendar();
+ // time zone component
+ $vTimeZone = $this->vCalendar3->add('VTIMEZONE');
+ $vTimeZone->add('TZID', 'America/Toronto');
+ // event component
+ $vEvent = $this->vCalendar3->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701');
+ $vEvent->add('DTEND', '20240706');
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ }
+
+ public function testConstructFromCalendarString(): void {
+
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a->serialize(), '96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ // test object creation
+ $this->assertInstanceOf(EventReader::class, $er);
+
+ }
+
+ public function testConstructFromCalendarObject(): void {
+
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a, '96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ // test object creation
+ $this->assertInstanceOf(EventReader::class, $er);
+
+ }
+
+ public function testConstructFromEventObject(): void {
+
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a->VEVENT[0]);
+ // test object creation
+ $this->assertInstanceOf(EventReader::class, $er);
+
+ }
+
+ public function testStartDateTime(): void {
+
+ /** test day part event with same start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a, $this->vCalendar1a->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->startDateTime());
+
+ /** test day part event with different start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1b, $this->vCalendar1b->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->startDateTime());
+
+ /** test day part event with global time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1c, $this->vCalendar1c->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->startDateTime());
+
+ /** test day part event with no time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1d, $this->vCalendar1d->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('UTC')))), $er->startDateTime());
+
+ /** test day part event with microsoft time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1e, $this->vCalendar1e->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->startDateTime());
+
+ /** test full day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar2, $this->vCalendar2->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T000000', (new DateTimeZone('America/Toronto')))), $er->startDateTime());
+
+ /** test multi day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar3, $this->vCalendar3->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T000000', (new DateTimeZone('America/Toronto')))), $er->startDateTime());
+
+ }
+
+ public function testStartTimeZone(): void {
+
+ /** test day part event with same start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a, $this->vCalendar1a->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->startTimeZone());
+
+ /** test day part event with different start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1b, $this->vCalendar1b->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->startTimeZone());
+
+ /** test day part event with global time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1c, $this->vCalendar1c->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->startTimeZone());
+
+ /** test day part event with no time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1d, $this->vCalendar1d->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('UTC')), $er->startTimeZone());
+
+ /** test day part event with microsoft time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1e, $this->vCalendar1e->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->startTimeZone());
+
+ /** test full day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar2, $this->vCalendar2->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->startTimeZone());
+
+ /** test multi day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar3, $this->vCalendar3->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->startTimeZone());
+
+ }
+
+ public function testEndDate(): void {
+
+ /** test day part event with same start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a, $this->vCalendar1a->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T090000', (new DateTimeZone('America/Toronto')))), $er->endDateTime());
+
+ /** test day part event with different start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1b, $this->vCalendar1b->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T090000', (new DateTimeZone('America/Vancouver')))), $er->endDateTime());
+
+ /** test day part event with global time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1c, $this->vCalendar1c->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T090000', (new DateTimeZone('America/Toronto')))), $er->endDateTime());
+
+ /** test day part event with no time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1d, $this->vCalendar1d->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T090000', (new DateTimeZone('UTC')))), $er->endDateTime());
+
+ /** test day part event with microsoft time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1e, $this->vCalendar1e->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240701T090000', (new DateTimeZone('America/Toronto')))), $er->endDateTime());
+
+ /** test full day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar2, $this->vCalendar2->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240702T000000', (new DateTimeZone('America/Toronto')))), $er->endDateTime());
+
+ /** test multi day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar3, $this->vCalendar3->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240706T000000', (new DateTimeZone('America/Toronto')))), $er->endDateTime());
+
+ }
+
+ public function testEndTimeZone(): void {
+
+ /** test day part event with same start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a, $this->vCalendar1a->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->endTimeZone());
+
+ /** test day part event with different start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1b, $this->vCalendar1b->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Vancouver')), $er->endTimeZone());
+
+ /** test day part event with global time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1c, $this->vCalendar1c->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->endTimeZone());
+
+ /** test day part event with no time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1d, $this->vCalendar1d->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('UTC')), $er->endTimeZone());
+
+ /** test day part event with microsoft time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1e, $this->vCalendar1e->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->endTimeZone());
+
+ /** test full day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar2, $this->vCalendar2->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->endTimeZone());
+
+ /** test multi day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar3, $this->vCalendar3->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new DateTimeZone('America/Toronto')), $er->endTimeZone());
+
+ }
+
+ public function testEntireDay(): void {
+
+ /** test day part event with same start/end time zone */
+ // construct event reader
+ $er = new EventReader($this->vCalendar1a, $this->vCalendar1a->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertFalse($er->entireDay());
+
+ /** test full day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar2, $this->vCalendar2->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->entireDay());
+
+ /** test multi day event */
+ // construct event reader
+ $er = new EventReader($this->vCalendar3, $this->vCalendar3->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->entireDay());
+
+ }
+
+ public function testRecurs(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertFalse($er->recurs());
+
+ /** test rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->recurs());
+
+ /** test rdate recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->recurs());
+
+ }
+
+ public function testRecurringPattern(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertNull($er->recurringPattern());
+
+ /** test absolute rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('A', $er->recurringPattern());
+
+ /** test relative rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=MO;BYSETPOS=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('R', $er->recurringPattern());
+
+ /** test rdate recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('A', $er->recurringPattern());
+
+ }
+
+ public function testRecurringPrecision(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertNull($er->recurringPrecision());
+
+ /** test daily rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('daily', $er->recurringPrecision());
+
+ /** test weekly rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('weekly', $er->recurringPrecision());
+
+ /** test monthly rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8,15');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('monthly', $er->recurringPrecision());
+
+ /** test yearly rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYMONTHDAY=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('yearly', $er->recurringPrecision());
+
+ /** test rdate recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals('fixed', $er->recurringPrecision());
+
+ }
+
+ public function testRecurringInterval(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertNull($er->recurringInterval());
+
+ /** test daily rrule recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(2, $er->recurringInterval());
+
+ /** test rdate recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertNull($er->recurringInterval());
+
+ }
+
+ public function testRecurringConcludes(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertFalse($er->recurringConcludes());
+
+ /** test rrule recurrance with no end */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertFalse($er->recurringConcludes());
+
+ /** test rrule recurrance with until date end */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;UNTIL=20240712T080000Z;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->recurringConcludes());
+
+ /** test rrule recurrance with iteration end */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->recurringConcludes());
+
+ /** test rdate recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->recurringConcludes());
+
+ /** test rdate (multiple property instances) recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->recurringConcludes());
+
+ /** test rrule and rdate recurrance with rdate as last date */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240706,20240715');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->recurringConcludes());
+
+ /** test rrule and rdate recurrance with rrule as last date */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=7;BYDAY=MO,WE,FR');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240706,20240713');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertTrue($er->recurringConcludes());
+
+ }
+
+ public function testRecurringConcludesAfter(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertNull($er->recurringConcludesAfter());
+
+ /** test rrule recurrance with count */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(6, $er->recurringConcludesAfter());
+
+ /** test rdate recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(2, $er->recurringConcludesAfter());
+
+ /** test rdate (multiple property instances) recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(2, $er->recurringConcludesAfter());
+
+ /** test rrule and rdate recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240706,20240715');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(8, $er->recurringConcludesAfter());
+
+ }
+
+ public function testRecurringConcludesOn(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertNull($er->recurringConcludesOn());
+
+ /** test rrule recurrance with no end */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertNull($er->recurringConcludesOn());
+
+ /** test rrule recurrance with until date end */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;UNTIL=20240712T080000Z;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+
+ // TODO: Fix until time zone
+ //$this->assertEquals((new \DateTime('20240712T080000', (new DateTimeZone('America/Toronto')))), $er->recurringConcludesOn());
+
+ /** test rdate recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240705T000000', (new DateTimeZone('America/Toronto')))), $er->recurringConcludesOn());
+
+ /** test rdate (multiple property instances) recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240705');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240705T000000', (new DateTimeZone('America/Toronto')))), $er->recurringConcludesOn());
+
+ /** test rrule and rdate recurrance with rdate as last date */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240706,20240715');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240715T000000', (new DateTimeZone('America/Toronto')))), $er->recurringConcludesOn());
+
+ /** test rrule and rdate recurrance with rrule as last date */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=7;BYDAY=MO,WE,FR');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240706,20240713');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals((new \DateTime('20240715T080000', (new DateTimeZone('America/Toronto')))), $er->recurringConcludesOn());
+
+ }
+
+ public function testRecurringDaysOfWeek(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringDaysOfWeek());
+
+ /** test rrule recurrance with weekly days*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;UNTIL=20240712T080000Z;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(['MO','WE','FR'], $er->recurringDaysOfWeek());
+
+ }
+
+ public function testRecurringDaysOfWeekNamed(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringDaysOfWeekNamed());
+
+ /** test rrule recurrance with weekly days*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;UNTIL=20240712T080000Z;BYDAY=MO,WE,FR');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(['Monday','Wednesday','Friday'], $er->recurringDaysOfWeekNamed());
+
+ }
+
+ public function testRecurringDaysOfMonth(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringDaysOfMonth());
+
+ /** test rrule recurrance with monthly absolute dates*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=6,13,20,27');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([6,13,20,27], $er->recurringDaysOfMonth());
+
+ }
+
+ public function testRecurringDaysOfYear(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringDaysOfYear());
+
+ /** test rrule recurrance with monthly absolute dates*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYYEARDAY=1,30,180,365');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([1,30,180,365], $er->recurringDaysOfYear());
+
+ }
+
+ public function testRecurringWeeksOfMonth(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringWeeksOfMonth());
+
+ /** test rrule recurrance with monthly days*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=MO;BYSETPOS=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([1], $er->recurringWeeksOfMonth());
+
+ }
+
+ public function testRecurringWeeksOfMonthNamed(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringWeeksOfMonthNamed());
+
+ /** test rrule recurrance with weekly days*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=MO;BYSETPOS=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(['First'], $er->recurringWeeksOfMonthNamed());
+
+ }
+
+ public function testRecurringWeeksOfYear(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringWeeksOfYear());
+
+ /** test rrule recurrance with monthly days*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;INTERVAL=1;BYWEEKNO=35,42;BYDAY=TU');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([35,42], $er->recurringWeeksOfYear());
+
+ }
+
+ public function testRecurringMonthsOfYear(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringMonthsOfYear());
+
+ /** test rrule recurrance with monthly days*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;INTERVAL=1;BYMONTH=7;BYMONTHDAY=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([7], $er->recurringMonthsOfYear());
+
+ }
+
+ public function testRecurringMonthsOfYearNamed(): void {
+
+ /** test no recurrance */
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals([], $er->recurringMonthsOfYearNamed());
+
+ /** test rrule recurrance with weekly days*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;INTERVAL=1;BYMONTH=7;BYMONTHDAY=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test set by constructor
+ $this->assertEquals(['July'], $er->recurringMonthsOfYearNamed());
+
+ }
+
+ public function testRecurringIterationDaily(): void {
+
+ /** test rrule recurrance with daily frequency*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=3;UNTIL=20240714T040000Z');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test initial recurrance
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240704T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240707T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240710T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240713T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance (This is past the last recurrance and should return null)
+ $er->recurrenceAdvance();
+ $this->assertNull($er->recurrenceDate());
+ // test rewind to initial recurrance
+ $er->recurrenceRewind();
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvanceTo((new \DateTime('20240709T080000')));
+ $this->assertEquals((new \DateTime('20240710T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+
+ }
+
+ public function testRecurringIterationWeekly(): void {
+
+ /** test rrule recurrance with weekly frequency*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20240713T040000Z');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test initial recurrance
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240703T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240705T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240708T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240710T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240712T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance (This is past the last recurrance and should return null)
+ $er->recurrenceAdvance();
+ $this->assertNull($er->recurrenceDate());
+ // test rewind to initial recurrance
+ $er->recurrenceRewind();
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvanceTo((new \DateTime('20240709T080000')));
+ $this->assertEquals((new \DateTime('20240710T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+
+ }
+
+ public function testRecurringIterationMonthlyAbsolute(): void {
+
+ /** test rrule recurrance with monthly absolute frequency on the 1st of each month*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;COUNT=3;BYMONTHDAY=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test initial recurrance
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240801T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240901T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance (This is past the last recurrance and should return null)
+ $er->recurrenceAdvance();
+ $this->assertNull($er->recurrenceDate());
+ // test rewind to initial recurrance
+ $er->recurrenceRewind();
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvanceTo((new \DateTime('20240809T080000')));
+ $this->assertEquals((new \DateTime('20240901T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+
+ }
+
+ public function testRecurringIterationMonthlyRelative(): void {
+
+ /** test rrule recurrance with monthly relative frequency on the first monday of each month*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;COUNT=3;BYDAY=MO;BYSETPOS=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test initial recurrance
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240805T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240902T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance (This is past the last recurrance and should return null)
+ $er->recurrenceAdvance();
+ $this->assertNull($er->recurrenceDate());
+ // test rewind to initial recurrance
+ $er->recurrenceRewind();
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvanceTo((new \DateTime('20240809T080000')));
+ $this->assertEquals((new \DateTime('20240902T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+
+ }
+
+ public function testRecurringIterationYearlyAbsolute(): void {
+
+ /** test rrule recurrance with yearly absolute frequency on the 1st of july*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;COUNT=3;BYMONTH=7');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test initial recurrance
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20250701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20260701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance (This is past the last recurrance and should return null)
+ $er->recurrenceAdvance();
+ $this->assertNull($er->recurrenceDate());
+ // test rewind to initial recurrance
+ $er->recurrenceRewind();
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvanceTo((new \DateTime('20250809T080000')));
+ $this->assertEquals((new \DateTime('20260701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+
+ }
+
+ public function testRecurringIterationYearlyRelative(): void {
+
+ /** test rrule recurrance with yearly relative frequency on the first monday of july*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;COUNT=3;BYMONTH=7;BYDAY=MO;BYSETPOS=1');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test initial recurrance
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20250707T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20260706T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance (This is past the last recurrance and should return null)
+ $er->recurrenceAdvance();
+ $this->assertNull($er->recurrenceDate());
+ // test rewind to initial recurrance
+ $er->recurrenceRewind();
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvanceTo((new \DateTime('20250809T080000')));
+ $this->assertEquals((new \DateTime('20260706T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+
+ }
+
+ public function testRecurringIterationFixed(): void {
+
+ /** test rrule recurrance with yearly relative frequency on the first monday of july*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703T080000,20240905T080000,20241231T080000');
+ // construct event reader
+ $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test initial recurrance
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240703T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20240905T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvance();
+ $this->assertEquals((new \DateTime('20241231T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance (This is past the last recurrance and should return null)
+ $er->recurrenceAdvance();
+ $this->assertNull($er->recurrenceDate());
+ // test rewind to initial recurrance
+ $er->recurrenceRewind();
+ $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+ // test next recurrance
+ $er->recurrenceAdvanceTo((new \DateTime('20240809T080000')));
+ $this->assertEquals((new \DateTime('20240905T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate());
+
+ }
+
+}
diff --git a/apps/dav/tests/unit/CalDAV/Export/ExportServiceTest.php b/apps/dav/tests/unit/CalDAV/Export/ExportServiceTest.php
new file mode 100644
index 00000000000..838dfc18f2f
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Export/ExportServiceTest.php
@@ -0,0 +1,81 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Export;
+
+use Generator;
+use OCA\DAV\CalDAV\Export\ExportService;
+use OCP\Calendar\CalendarExportOptions;
+use OCP\Calendar\ICalendarExport;
+use OCP\ServerVersion;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\VObject\Component\VCalendar;
+
+class ExportServiceTest extends \Test\TestCase {
+ private ServerVersion&MockObject $serverVersion;
+ private ExportService $service;
+ private ICalendarExport&MockObject $calendar;
+ private array $mockExportCollection;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->serverVersion = $this->createMock(ServerVersion::class);
+ $this->serverVersion->method('getVersionString')
+ ->willReturn('32.0.0.0');
+ $this->service = new ExportService($this->serverVersion);
+ $this->calendar = $this->createMock(ICalendarExport::class);
+
+ }
+
+ protected function mockGenerator(): Generator {
+ foreach ($this->mockExportCollection as $entry) {
+ yield $entry;
+ }
+ }
+
+ public function testExport(): void {
+ // Arrange
+ // construct calendar with a 1 hour event and same start/end time zones
+ $vCalendar = new VCalendar();
+ /** @var \Sabre\VObject\Component\VEvent $vEvent */
+ $vEvent = $vCalendar->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ // construct calendar return
+ $options = new CalendarExportOptions();
+ $this->mockExportCollection[] = $vCalendar;
+ $this->calendar->expects($this->once())
+ ->method('export')
+ ->with($options)
+ ->willReturn($this->mockGenerator());
+
+ // Act
+ $document = '';
+ foreach ($this->service->export($this->calendar, $options) as $chunk) {
+ $document .= $chunk;
+ }
+
+ // Assert
+ $this->assertStringContainsString('BEGIN:VCALENDAR', $document, 'Exported document calendar start missing');
+ $this->assertStringContainsString('BEGIN:VEVENT', $document, 'Exported document event start missing');
+ $this->assertStringContainsString('END:VEVENT', $document, 'Exported document event end missing');
+ $this->assertStringContainsString('END:VCALENDAR', $document, 'Exported document calendar end missing');
+
+ }
+
+}
diff --git a/apps/dav/tests/unit/CalDAV/Integration/ExternalCalendarTest.php b/apps/dav/tests/unit/CalDAV/Integration/ExternalCalendarTest.php
index 0131ab443a8..b2f479ac0e3 100644
--- a/apps/dav/tests/unit/CalDAV/Integration/ExternalCalendarTest.php
+++ b/apps/dav/tests/unit/CalDAV/Integration/ExternalCalendarTest.php
@@ -1,38 +1,24 @@
<?php
+
+declare(strict_types=1);
/**
- * @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\Tests\unit\CalDAV\Integration;
use OCA\DAV\CalDAV\Integration\ExternalCalendar;
+use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class ExternalCalendarTest extends TestCase {
- private $abstractExternalCalendar;
+ private ExternalCalendar&MockObject $abstractExternalCalendar;
protected function setUp(): void {
parent::setUp();
- $this->abstractExternalCalendar =
- $this->getMockForAbstractClass(ExternalCalendar::class, ['example-app-id', 'calendar-uri-in-backend']);
+ $this->abstractExternalCalendar
+ = $this->getMockForAbstractClass(ExternalCalendar::class, ['example-app-id', 'calendar-uri-in-backend']);
}
public function testGetName():void {
@@ -40,13 +26,13 @@ class ExternalCalendarTest extends TestCase {
$this->assertEquals('app-generated--example-app-id--calendar-uri-in-backend',
$this->abstractExternalCalendar->getName());
- // Check that the method is final and can't be overriden by other classes
+ // Check that the method is final and can't be overridden by other classes
$reflectionMethod = new \ReflectionMethod(ExternalCalendar::class, 'getName');
$this->assertTrue($reflectionMethod->isFinal());
}
public function testSetName():void {
- // Check that the method is final and can't be overriden by other classes
+ // Check that the method is final and can't be overridden by other classes
$reflectionMethod = new \ReflectionMethod(ExternalCalendar::class, 'setName');
$this->assertTrue($reflectionMethod->isFinal());
@@ -56,8 +42,8 @@ class ExternalCalendarTest extends TestCase {
$this->abstractExternalCalendar->setName('other-name');
}
- public function createDirectory():void {
- // Check that the method is final and can't be overriden by other classes
+ public function createDirectory(): void {
+ // Check that the method is final and can't be overridden by other classes
$reflectionMethod = new \ReflectionMethod(ExternalCalendar::class, 'createDirectory');
$this->assertTrue($reflectionMethod->isFinal());
@@ -80,9 +66,7 @@ class ExternalCalendarTest extends TestCase {
$this->assertTrue(ExternalCalendar::isAppGeneratedCalendar('app-generated--example--foo--2'));
}
- /**
- * @dataProvider splitAppGeneratedCalendarUriDataProvider
- */
+ #[\PHPUnit\Framework\Attributes\DataProvider('splitAppGeneratedCalendarUriDataProvider')]
public function testSplitAppGeneratedCalendarUriInvalid(string $name):void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Provided calendar uri was not app-generated');
@@ -90,7 +74,7 @@ class ExternalCalendarTest extends TestCase {
ExternalCalendar::splitAppGeneratedCalendarUri($name);
}
- public function splitAppGeneratedCalendarUriDataProvider():array {
+ public static function splitAppGeneratedCalendarUriDataProvider():array {
return [
['personal'],
['foo_shared_by_admin'],
diff --git a/apps/dav/tests/unit/CalDAV/Listener/CalendarPublicationListenerTest.php b/apps/dav/tests/unit/CalDAV/Listener/CalendarPublicationListenerTest.php
new file mode 100644
index 00000000000..3ba0b832593
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Listener/CalendarPublicationListenerTest.php
@@ -0,0 +1,55 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Listeners;
+
+use OCA\DAV\CalDAV\Activity\Backend;
+use OCA\DAV\Events\CalendarPublishedEvent;
+use OCA\DAV\Events\CalendarUnpublishedEvent;
+use OCA\DAV\Listener\CalendarPublicationListener;
+use OCP\EventDispatcher\Event;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class CalendarPublicationListenerTest extends TestCase {
+ private Backend&MockObject $activityBackend;
+ private LoggerInterface&MockObject $logger;
+ private CalendarPublicationListener $calendarPublicationListener;
+ private CalendarPublishedEvent&MockObject $publicationEvent;
+ private CalendarUnpublishedEvent&MockObject $unpublicationEvent;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->activityBackend = $this->createMock(Backend::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->publicationEvent = $this->createMock(CalendarPublishedEvent::class);
+ $this->unpublicationEvent = $this->createMock(CalendarUnpublishedEvent::class);
+ $this->calendarPublicationListener = new CalendarPublicationListener($this->activityBackend, $this->logger);
+ }
+
+ public function testInvalidEvent(): void {
+ $this->activityBackend->expects($this->never())->method('onCalendarPublication');
+ $this->logger->expects($this->never())->method('debug');
+ $this->calendarPublicationListener->handle(new Event());
+ }
+
+ public function testPublicationEvent(): void {
+ $this->publicationEvent->expects($this->once())->method('getCalendarData')->with()->willReturn([]);
+ $this->activityBackend->expects($this->once())->method('onCalendarPublication')->with([], true);
+ $this->logger->expects($this->once())->method('debug');
+ $this->calendarPublicationListener->handle($this->publicationEvent);
+ }
+
+ public function testUnPublicationEvent(): void {
+ $this->unpublicationEvent->expects($this->once())->method('getCalendarData')->with()->willReturn([]);
+ $this->activityBackend->expects($this->once())->method('onCalendarPublication')->with([], false);
+ $this->logger->expects($this->once())->method('debug');
+ $this->calendarPublicationListener->handle($this->unpublicationEvent);
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Listener/CalendarShareUpdateListenerTest.php b/apps/dav/tests/unit/CalDAV/Listener/CalendarShareUpdateListenerTest.php
new file mode 100644
index 00000000000..d5697a862db
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Listener/CalendarShareUpdateListenerTest.php
@@ -0,0 +1,48 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Listeners;
+
+use OCA\DAV\CalDAV\Activity\Backend;
+use OCA\DAV\Events\CalendarShareUpdatedEvent;
+use OCA\DAV\Listener\CalendarShareUpdateListener;
+use OCP\EventDispatcher\Event;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class CalendarShareUpdateListenerTest extends TestCase {
+ private Backend&MockObject $activityBackend;
+ private LoggerInterface&MockObject $logger;
+ private CalendarShareUpdateListener $calendarPublicationListener;
+ private CalendarShareUpdatedEvent&MockObject $event;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->activityBackend = $this->createMock(Backend::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->event = $this->createMock(CalendarShareUpdatedEvent::class);
+ $this->calendarPublicationListener = new CalendarShareUpdateListener($this->activityBackend, $this->logger);
+ }
+
+ public function testInvalidEvent(): void {
+ $this->activityBackend->expects($this->never())->method('onCalendarUpdateShares');
+ $this->logger->expects($this->never())->method('debug');
+ $this->calendarPublicationListener->handle(new Event());
+ }
+
+ public function testEvent(): void {
+ $this->event->expects($this->once())->method('getCalendarData')->with()->willReturn([]);
+ $this->event->expects($this->once())->method('getOldShares')->with()->willReturn([]);
+ $this->event->expects($this->once())->method('getAdded')->with()->willReturn([]);
+ $this->event->expects($this->once())->method('getRemoved')->with()->willReturn([]);
+ $this->activityBackend->expects($this->once())->method('onCalendarUpdateShares')->with([], [], [], []);
+ $this->logger->expects($this->once())->method('debug');
+ $this->calendarPublicationListener->handle($this->event);
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Listener/SubscriptionListenerTest.php b/apps/dav/tests/unit/CalDAV/Listener/SubscriptionListenerTest.php
new file mode 100644
index 00000000000..cbfdfd6b9b7
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Listener/SubscriptionListenerTest.php
@@ -0,0 +1,67 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Listeners;
+
+use OCA\DAV\BackgroundJob\RefreshWebcalJob;
+use OCA\DAV\CalDAV\Reminder\Backend;
+use OCA\DAV\CalDAV\WebcalCaching\RefreshWebcalService;
+use OCA\DAV\Events\SubscriptionCreatedEvent;
+use OCA\DAV\Events\SubscriptionDeletedEvent;
+use OCA\DAV\Listener\SubscriptionListener;
+use OCP\BackgroundJob\IJobList;
+use OCP\EventDispatcher\Event;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class SubscriptionListenerTest extends TestCase {
+ private RefreshWebcalService&MockObject $refreshWebcalService;
+ private Backend&MockObject $reminderBackend;
+ private IJobList&MockObject $jobList;
+ private LoggerInterface&MockObject $logger;
+ private SubscriptionListener $calendarPublicationListener;
+ private SubscriptionCreatedEvent&MockObject $subscriptionCreatedEvent;
+ private SubscriptionDeletedEvent&MockObject $subscriptionDeletedEvent;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->refreshWebcalService = $this->createMock(RefreshWebcalService::class);
+ $this->reminderBackend = $this->createMock(Backend::class);
+ $this->jobList = $this->createMock(IJobList::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->subscriptionCreatedEvent = $this->createMock(SubscriptionCreatedEvent::class);
+ $this->subscriptionDeletedEvent = $this->createMock(SubscriptionDeletedEvent::class);
+ $this->calendarPublicationListener = new SubscriptionListener($this->jobList, $this->refreshWebcalService, $this->reminderBackend, $this->logger);
+ }
+
+ public function testInvalidEvent(): void {
+ $this->refreshWebcalService->expects($this->never())->method('refreshSubscription');
+ $this->jobList->expects($this->never())->method('add');
+ $this->logger->expects($this->never())->method('debug');
+ $this->calendarPublicationListener->handle(new Event());
+ }
+
+ public function testCreateSubscriptionEvent(): void {
+ $this->subscriptionCreatedEvent->expects($this->once())->method('getSubscriptionId')->with()->willReturn(5);
+ $this->subscriptionCreatedEvent->expects($this->once())->method('getSubscriptionData')->with()->willReturn(['principaluri' => 'principaluri', 'uri' => 'uri']);
+ $this->refreshWebcalService->expects($this->once())->method('refreshSubscription')->with('principaluri', 'uri');
+ $this->jobList->expects($this->once())->method('add')->with(RefreshWebcalJob::class, ['principaluri' => 'principaluri', 'uri' => 'uri']);
+ $this->logger->expects($this->exactly(2))->method('debug');
+ $this->calendarPublicationListener->handle($this->subscriptionCreatedEvent);
+ }
+
+ public function testDeleteSubscriptionEvent(): void {
+ $this->subscriptionDeletedEvent->expects($this->once())->method('getSubscriptionId')->with()->willReturn(5);
+ $this->subscriptionDeletedEvent->expects($this->once())->method('getSubscriptionData')->with()->willReturn(['principaluri' => 'principaluri', 'uri' => 'uri']);
+ $this->jobList->expects($this->once())->method('remove')->with(RefreshWebcalJob::class, ['principaluri' => 'principaluri', 'uri' => 'uri']);
+ $this->reminderBackend->expects($this->once())->method('cleanRemindersForCalendar')->with(5);
+ $this->logger->expects($this->exactly(2))->method('debug');
+ $this->calendarPublicationListener->handle($this->subscriptionDeletedEvent);
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/OutboxTest.php b/apps/dav/tests/unit/CalDAV/OutboxTest.php
index 8edc0f30ce4..cc0a3f0405f 100644
--- a/apps/dav/tests/unit/CalDAV/OutboxTest.php
+++ b/apps/dav/tests/unit/CalDAV/OutboxTest.php
@@ -1,40 +1,20 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018, Georg Ehrke
- *
- * @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\Tests\unit\CalDAV;
use OCA\DAV\CalDAV\Outbox;
use OCP\IConfig;
+use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class OutboxTest extends TestCase {
-
- /** @var IConfig */
- private $config;
-
- /** @var Outbox */
- private $outbox;
+ private IConfig&MockObject $config;
+ private Outbox $outbox;
protected function setUp(): void {
parent::setUp();
@@ -43,7 +23,7 @@ class OutboxTest extends TestCase {
$this->outbox = new Outbox($this->config, 'user-principal-123');
}
- public function testGetACLFreeBusyEnabled() {
+ public function testGetACLFreeBusyEnabled(): void {
$this->config->expects($this->once())
->method('getAppValue')
->with('dav', 'disableFreeBusy', 'no')
@@ -78,7 +58,7 @@ class OutboxTest extends TestCase {
], $this->outbox->getACL());
}
- public function testGetACLFreeBusyDisabled() {
+ public function testGetACLFreeBusyDisabled(): void {
$this->config->expects($this->once())
->method('getAppValue')
->with('dav', 'disableFreeBusy', 'no')
diff --git a/apps/dav/tests/unit/CalDAV/PluginTest.php b/apps/dav/tests/unit/CalDAV/PluginTest.php
index b5da2199e1c..c5725a1fa81 100644
--- a/apps/dav/tests/unit/CalDAV/PluginTest.php
+++ b/apps/dav/tests/unit/CalDAV/PluginTest.php
@@ -1,27 +1,9 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
- *
- * @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>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have 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\Tests\unit\CalDAV;
@@ -29,8 +11,7 @@ use OCA\DAV\CalDAV\Plugin;
use Test\TestCase;
class PluginTest extends TestCase {
- /** @var Plugin */
- private $plugin;
+ private Plugin $plugin;
protected function setUp(): void {
parent::setUp();
@@ -38,7 +19,7 @@ class PluginTest extends TestCase {
$this->plugin = new Plugin();
}
- public function linkProvider() {
+ public static function linkProvider(): array {
return [
[
'principals/users/MyUserName',
@@ -55,17 +36,12 @@ class PluginTest extends TestCase {
];
}
- /**
- * @dataProvider linkProvider
- *
- * @param $input
- * @param $expected
- */
- public function testGetCalendarHomeForPrincipal($input, $expected) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('linkProvider')]
+ public function testGetCalendarHomeForPrincipal(string $input, string $expected): void {
$this->assertSame($expected, $this->plugin->getCalendarHomeForPrincipal($input));
}
- public function testGetCalendarHomeForUnknownPrincipal() {
+ public function testGetCalendarHomeForUnknownPrincipal(): void {
$this->assertNull($this->plugin->getCalendarHomeForPrincipal('FOO/BAR/BLUB'));
}
}
diff --git a/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php b/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php
index f3f53067d0d..6acceed6f64 100644
--- a/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php
+++ b/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php
@@ -1,32 +1,9 @@
<?php
+
+declare(strict_types=1);
/**
- * @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 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 Vinicius Cubas Brand <vinicius@eita.org.br>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have 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\Tests\unit\CalDAV;
@@ -37,13 +14,14 @@ use OCA\DAV\CalDAV\PublicCalendarRoot;
use OCA\DAV\Connector\Sabre\Principal;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
+use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IL10N;
-use OCP\ILogger;
use OCP\IUserManager;
use OCP\Security\ISecureRandom;
+use OCP\Server;
+use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Test\TestCase;
/**
@@ -55,39 +33,28 @@ use Test\TestCase;
*/
class PublicCalendarRootTest extends TestCase {
public const UNIT_TEST_USER = '';
- /** @var CalDavBackend */
- private $backend;
- /** @var PublicCalendarRoot */
- private $publicCalendarRoot;
- /** @var IL10N */
- private $l10n;
- /** @var Principal|\PHPUnit\Framework\MockObject\MockObject */
- private $principal;
- /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */
- protected $userManager;
- /** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject */
- protected $groupManager;
- /** @var IConfig */
- protected $config;
-
- /** @var ISecureRandom */
- private $random;
- /** @var ILogger */
- private $logger;
+ private CalDavBackend $backend;
+ private PublicCalendarRoot $publicCalendarRoot;
+ private IL10N&MockObject $l10n;
+ private Principal&MockObject $principal;
+ protected IUserManager&MockObject $userManager;
+ protected IGroupManager&MockObject $groupManager;
+ protected IConfig&MockObject $config;
+ private ISecureRandom $random;
+ private LoggerInterface&MockObject $logger;
protected function setUp(): void {
parent::setUp();
- $db = \OC::$server->getDatabaseConnection();
+ $db = Server::get(IDBConnection::class);
$this->principal = $this->createMock('OCA\DAV\Connector\Sabre\Principal');
$this->userManager = $this->createMock(IUserManager::class);
$this->groupManager = $this->createMock(IGroupManager::class);
- $this->random = \OC::$server->getSecureRandom();
- $this->logger = $this->createMock(ILogger::class);
- $this->psrLogger = $this->createMock(LoggerInterface::class);
+ $this->random = Server::get(ISecureRandom::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
$dispatcher = $this->createMock(IEventDispatcher::class);
- $legacyDispatcher = $this->createMock(EventDispatcherInterface::class);
$config = $this->createMock(IConfig::class);
+ $sharingBackend = $this->createMock(\OCA\DAV\CalDAV\Sharing\Backend::class);
$this->principal->expects($this->any())->method('getGroupMembership')
->withAnyParameters()
@@ -101,19 +68,18 @@ class PublicCalendarRootTest extends TestCase {
$db,
$this->principal,
$this->userManager,
- $this->groupManager,
$this->random,
$this->logger,
$dispatcher,
- $legacyDispatcher,
- $config
+ $config,
+ $sharingBackend,
+ false,
);
- $this->l10n = $this->getMockBuilder(IL10N::class)
- ->disableOriginalConstructor()->getMock();
+ $this->l10n = $this->createMock(IL10N::class);
$this->config = $this->createMock(IConfig::class);
$this->publicCalendarRoot = new PublicCalendarRoot($this->backend,
- $this->l10n, $this->config, $this->psrLogger);
+ $this->l10n, $this->config, $this->logger);
}
protected function tearDown(): void {
@@ -136,12 +102,12 @@ class PublicCalendarRootTest extends TestCase {
}
}
- public function testGetName() {
+ public function testGetName(): void {
$name = $this->publicCalendarRoot->getName();
$this->assertEquals('public-calendars', $name);
}
- public function testGetChild() {
+ public function testGetChild(): void {
$calendar = $this->createPublicCalendar();
$publicCalendars = $this->backend->getPublicCalendars();
@@ -154,24 +120,21 @@ class PublicCalendarRootTest extends TestCase {
$this->assertEquals($calendar, $calendarResult);
}
- public function testGetChildren() {
+ public function testGetChildren(): void {
$this->createPublicCalendar();
$calendarResults = $this->publicCalendarRoot->getChildren();
$this->assertSame([], $calendarResults);
}
- /**
- * @return Calendar
- */
- protected function createPublicCalendar() {
+ protected function createPublicCalendar(): Calendar {
$this->backend->createCalendar(self::UNIT_TEST_USER, 'Example', []);
$calendarInfo = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER)[0];
- $calendar = new PublicCalendar($this->backend, $calendarInfo, $this->l10n, $this->config, $this->psrLogger);
+ $calendar = new PublicCalendar($this->backend, $calendarInfo, $this->l10n, $this->config, $this->logger);
$publicUri = $calendar->setPublishStatus(true);
$calendarInfo = $this->backend->getPublicCalendar($publicUri);
- $calendar = new PublicCalendar($this->backend, $calendarInfo, $this->l10n, $this->config, $this->psrLogger);
+ $calendar = new PublicCalendar($this->backend, $calendarInfo, $this->l10n, $this->config, $this->logger);
return $calendar;
}
diff --git a/apps/dav/tests/unit/CalDAV/PublicCalendarTest.php b/apps/dav/tests/unit/CalDAV/PublicCalendarTest.php
index d7a281e9d27..98153a067fb 100644
--- a/apps/dav/tests/unit/CalDAV/PublicCalendarTest.php
+++ b/apps/dav/tests/unit/CalDAV/PublicCalendarTest.php
@@ -1,27 +1,9 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2017, Georg Ehrke
- *
- * @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\Tests\unit\CalDAV;
@@ -34,17 +16,13 @@ use Sabre\VObject\Reader;
class PublicCalendarTest extends CalendarTest {
- /**
- * @dataProvider providesConfidentialClassificationData
- * @param int $expectedChildren
- * @param bool $isShared
- */
- public function testPrivateClassification($expectedChildren, $isShared) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesConfidentialClassificationData')]
+ public function testPrivateClassification(int $expectedChildren, bool $isShared): void {
$calObject0 = ['uri' => 'event-0', 'classification' => CalDavBackend::CLASSIFICATION_PUBLIC];
$calObject1 = ['uri' => 'event-1', 'classification' => CalDavBackend::CLASSIFICATION_CONFIDENTIAL];
$calObject2 = ['uri' => 'event-2', 'classification' => CalDavBackend::CLASSIFICATION_PRIVATE];
- /** @var MockObject | CalDavBackend $backend */
+ /** @var CalDavBackend&MockObject $backend */
$backend = $this->getMockBuilder(CalDavBackend::class)->disableOriginalConstructor()->getMock();
$backend->expects($this->any())->method('getCalendarObjects')->willReturn([
$calObject0, $calObject1, $calObject2
@@ -64,11 +42,11 @@ class PublicCalendarTest extends CalendarTest {
'id' => 666,
'uri' => 'cal',
];
- /** @var MockObject | IConfig $config */
+ /** @var IConfig&MockObject $config */
$config = $this->createMock(IConfig::class);
- /** @var MockObject | LoggerInterface $logger */
+ /** @var LoggerInterface&MockObject $logger */
$logger = $this->createMock(LoggerInterface::class);
- $c = new PublicCalendar($backend, $calendarInfo, $this->l10n, $config,$logger);
+ $c = new PublicCalendar($backend, $calendarInfo, $this->l10n, $config, $logger);
$children = $c->getChildren();
$this->assertEquals(2, count($children));
$children = $c->getMultipleChildren(['event-0', 'event-1', 'event-2']);
@@ -77,12 +55,8 @@ class PublicCalendarTest extends CalendarTest {
$this->assertFalse($c->childExists('event-2'));
}
- /**
- * @dataProvider providesConfidentialClassificationData
- * @param int $expectedChildren
- * @param bool $isShared
- */
- public function testConfidentialClassification($expectedChildren, $isShared) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesConfidentialClassificationData')]
+ public function testConfidentialClassification(int $expectedChildren, bool $isShared): void {
$start = '20160609';
$end = '20160610';
@@ -132,7 +106,7 @@ EOD;
$calObject1 = ['uri' => 'event-1', 'classification' => CalDavBackend::CLASSIFICATION_CONFIDENTIAL, 'calendardata' => $calData];
$calObject2 = ['uri' => 'event-2', 'classification' => CalDavBackend::CLASSIFICATION_PRIVATE];
- /** @var MockObject | CalDavBackend $backend */
+ /** @var CalDavBackend&MockObject $backend */
$backend = $this->getMockBuilder(CalDavBackend::class)->disableOriginalConstructor()->getMock();
$backend->expects($this->any())->method('getCalendarObjects')->willReturn([
$calObject0, $calObject1, $calObject2
@@ -152,11 +126,11 @@ EOD;
'id' => 666,
'uri' => 'cal',
];
- /** @var MockObject | IConfig $config */
+ /** @var IConfig&MockObject $config */
$config = $this->createMock(IConfig::class);
- /** @var MockObject | LoggerInterface $logger */
+ /** @var LoggerInterface&MockObject $logger */
$logger = $this->createMock(LoggerInterface::class);
- $c = new PublicCalendar($backend, $calendarInfo, $this->l10n, $config,$logger);
+ $c = new PublicCalendar($backend, $calendarInfo, $this->l10n, $config, $logger);
$this->assertEquals(count($c->getChildren()), 2);
diff --git a/apps/dav/tests/unit/CalDAV/Publishing/PublisherTest.php b/apps/dav/tests/unit/CalDAV/Publishing/PublisherTest.php
index d6b97648436..5344ec5d7cd 100644
--- a/apps/dav/tests/unit/CalDAV/Publishing/PublisherTest.php
+++ b/apps/dav/tests/unit/CalDAV/Publishing/PublisherTest.php
@@ -1,27 +1,9 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016 Thomas Citharel <nextcloud@tcit.fr>
- *
- * @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>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have 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\Tests\unit\CalDAV\Publishing;
@@ -32,7 +14,7 @@ use Test\TestCase;
class PublisherTest extends TestCase {
public const NS_CALENDARSERVER = 'http://calendarserver.org/ns/';
- public function testSerializePublished() {
+ public function testSerializePublished(): void {
$publish = new Publisher('urltopublish', true);
$xml = $this->write([
@@ -48,7 +30,7 @@ class PublisherTest extends TestCase {
</x1:publish-url>', $xml);
}
- public function testSerializeNotPublished() {
+ public function testSerializeNotPublished(): void {
$publish = new Publisher('urltopublish', false);
$xml = $this->write([
@@ -63,9 +45,9 @@ class PublisherTest extends TestCase {
}
- protected $elementMap = [];
- protected $namespaceMap = ['DAV:' => 'd'];
- protected $contextUri = '/';
+ protected array $elementMap = [];
+ protected array $namespaceMap = ['DAV:' => 'd'];
+ protected string $contextUri = '/';
private function write($input) {
$writer = new Writer();
diff --git a/apps/dav/tests/unit/CalDAV/Publishing/PublishingTest.php b/apps/dav/tests/unit/CalDAV/Publishing/PublishingTest.php
index 2490bdc3639..ec2ae37a929 100644
--- a/apps/dav/tests/unit/CalDAV/Publishing/PublishingTest.php
+++ b/apps/dav/tests/unit/CalDAV/Publishing/PublishingTest.php
@@ -1,27 +1,9 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016 Thomas Citharel <nextcloud@tcit.fr>
- *
- * @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: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Tests\unit\CalDAV\Publishing;
@@ -30,38 +12,29 @@ use OCA\DAV\CalDAV\Publishing\PublishPlugin;
use OCP\IConfig;
use OCP\IRequest;
use OCP\IURLGenerator;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\Server;
use Sabre\DAV\SimpleCollection;
use Sabre\HTTP\Request;
use Sabre\HTTP\Response;
use Test\TestCase;
-class PluginTest extends TestCase {
-
- /** @var PublishPlugin */
- private $plugin;
- /** @var Server */
- private $server;
- /** @var Calendar | \PHPUnit\Framework\MockObject\MockObject */
- private $book;
- /** @var IConfig | \PHPUnit\Framework\MockObject\MockObject */
- private $config;
- /** @var IURLGenerator | \PHPUnit\Framework\MockObject\MockObject */
- private $urlGenerator;
+class PublishingTest extends TestCase {
+ private PublishPlugin $plugin;
+ private Server $server;
+ private Calendar&MockObject $book;
+ private IConfig&MockObject $config;
+ private IURLGenerator&MockObject $urlGenerator;
protected function setUp(): void {
parent::setUp();
- $this->config = $this->getMockBuilder(IConfig::class)->
- disableOriginalConstructor()->
- getMock();
+ $this->config = $this->createMock(IConfig::class);
$this->config->expects($this->any())->method('getSystemValue')
->with($this->equalTo('secret'))
->willReturn('mysecret');
- $this->urlGenerator = $this->getMockBuilder(IURLGenerator::class)->
- disableOriginalConstructor()->
- getMock();
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
/** @var IRequest $request */
$this->plugin = new PublishPlugin($this->config, $this->urlGenerator);
@@ -69,15 +42,15 @@ class PluginTest extends TestCase {
$root = new SimpleCollection('calendars');
$this->server = new Server($root);
/** @var SimpleCollection $node */
- $this->book = $this->getMockBuilder(Calendar::class)->
- disableOriginalConstructor()->
- getMock();
+ $this->book = $this->getMockBuilder(Calendar::class)
+ ->disableOriginalConstructor()
+ ->getMock();
$this->book->method('getName')->willReturn('cal1');
$root->addChild($this->book);
$this->plugin->initialize($this->server);
}
- public function testPublishing() {
+ public function testPublishing(): void {
$this->book->expects($this->once())->method('setPublishStatus')->with(true);
// setup request
@@ -88,7 +61,7 @@ class PluginTest extends TestCase {
$this->plugin->httpPost($request, $response);
}
- public function testUnPublishing() {
+ public function testUnPublishing(): void {
$this->book->expects($this->once())->method('setPublishStatus')->with(false);
// setup request
diff --git a/apps/dav/tests/unit/CalDAV/Reminder/BackendTest.php b/apps/dav/tests/unit/CalDAV/Reminder/BackendTest.php
index 15f949d9adb..356acf2dd7f 100644
--- a/apps/dav/tests/unit/CalDAV/Reminder/BackendTest.php
+++ b/apps/dav/tests/unit/CalDAV/Reminder/BackendTest.php
@@ -3,54 +3,27 @@
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\Tests\unit\CalDAV\Reminder;
use OCA\DAV\CalDAV\Reminder\Backend as ReminderBackend;
use OCP\AppFramework\Utility\ITimeFactory;
+use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class BackendTest extends TestCase {
-
- /**
- * Reminder Backend
- *
- * @var ReminderBackend|\PHPUnit\Framework\MockObject\MockObject
- */
- private $reminderBackend;
-
- /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */
- private $timeFactory;
+ private ReminderBackend $reminderBackend;
+ private ITimeFactory&MockObject $timeFactory;
protected function setUp(): void {
parent::setUp();
$query = self::$realDatabase->getQueryBuilder();
- $query->delete('calendar_reminders')->execute();
- $query->delete('calendarobjects')->execute();
- $query->delete('calendars')->execute();
+ $query->delete('calendar_reminders')->executeStatement();
+ $query->delete('calendarobjects')->executeStatement();
+ $query->delete('calendars')->executeStatement();
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->reminderBackend = new ReminderBackend(self::$realDatabase, $this->timeFactory);
@@ -60,9 +33,11 @@ class BackendTest extends TestCase {
protected function tearDown(): void {
$query = self::$realDatabase->getQueryBuilder();
- $query->delete('calendar_reminders')->execute();
- $query->delete('calendarobjects')->execute();
- $query->delete('calendars')->execute();
+ $query->delete('calendar_reminders')->executeStatement();
+ $query->delete('calendarobjects')->executeStatement();
+ $query->delete('calendars')->executeStatement();
+
+ parent::tearDown();
}
@@ -115,7 +90,7 @@ class BackendTest extends TestCase {
$this->assertCount(4, $rows);
- $this->reminderBackend->removeReminder((int) $rows[3]['id']);
+ $this->reminderBackend->removeReminder((int)$rows[3]['id']);
$query = self::$realDatabase->getQueryBuilder();
$rows = $query->select('*')
@@ -138,7 +113,7 @@ class BackendTest extends TestCase {
unset($rows[0]['id']);
unset($rows[1]['id']);
- $this->assertEquals($rows[0], [
+ $expected1 = [
'calendar_id' => 1,
'object_id' => 1,
'uid' => 'asd',
@@ -154,8 +129,8 @@ class BackendTest extends TestCase {
'calendardata' => 'Calendar data 123',
'displayname' => 'Displayname 123',
'principaluri' => 'principals/users/user001',
- ]);
- $this->assertEquals($rows[1], [
+ ];
+ $expected2 = [
'calendar_id' => 1,
'object_id' => 1,
'uid' => 'asd',
@@ -171,7 +146,9 @@ class BackendTest extends TestCase {
'calendardata' => 'Calendar data 123',
'displayname' => 'Displayname 123',
'principaluri' => 'principals/users/user001',
- ]);
+ ];
+
+ $this->assertEqualsCanonicalizing([$rows[0],$rows[1]], [$expected1,$expected2]);
}
public function testGetAllScheduledRemindersForEvent(): void {
@@ -249,18 +226,18 @@ class BackendTest extends TestCase {
]);
}
- public function testUpdateReminder() {
+ public function testUpdateReminder(): void {
$query = self::$realDatabase->getQueryBuilder();
$rows = $query->select('*')
->from('calendar_reminders')
- ->execute()
+ ->executeQuery()
->fetchAll();
$this->assertCount(4, $rows);
$this->assertEquals($rows[3]['notification_date'], 123600);
- $reminderId = (int) $rows[3]['id'];
+ $reminderId = (int)$rows[3]['id'];
$newNotificationDate = 123700;
$this->reminderBackend->updateReminder($reminderId, $newNotificationDate);
@@ -269,10 +246,10 @@ class BackendTest extends TestCase {
$row = $query->select('notification_date')
->from('calendar_reminders')
->where($query->expr()->eq('id', $query->createNamedParameter($reminderId)))
- ->execute()
+ ->executeQuery()
->fetch();
- $this->assertEquals((int) $row['notification_date'], 123700);
+ $this->assertEquals((int)$row['notification_date'], 123700);
}
@@ -284,7 +261,7 @@ class BackendTest extends TestCase {
'principaluri' => $query->createNamedParameter('principals/users/user001'),
'displayname' => $query->createNamedParameter('Displayname 123'),
])
- ->execute();
+ ->executeStatement();
$query = self::$realDatabase->getQueryBuilder();
$query->insert('calendars')
@@ -293,7 +270,7 @@ class BackendTest extends TestCase {
'principaluri' => $query->createNamedParameter('principals/users/user002'),
'displayname' => $query->createNamedParameter('Displayname 99'),
])
- ->execute();
+ ->executeStatement();
$query = self::$realDatabase->getQueryBuilder();
$query->insert('calendarobjects')
@@ -303,7 +280,7 @@ class BackendTest extends TestCase {
'calendarid' => $query->createNamedParameter(1),
'size' => $query->createNamedParameter(42),
])
- ->execute();
+ ->executeStatement();
$query = self::$realDatabase->getQueryBuilder();
$query->insert('calendarobjects')
@@ -313,7 +290,7 @@ class BackendTest extends TestCase {
'calendarid' => $query->createNamedParameter(1),
'size' => $query->createNamedParameter(42),
])
- ->execute();
+ ->executeStatement();
$query = self::$realDatabase->getQueryBuilder();
$query->insert('calendarobjects')
@@ -323,7 +300,7 @@ class BackendTest extends TestCase {
'calendarid' => $query->createNamedParameter(99),
'size' => $query->createNamedParameter(42),
])
- ->execute();
+ ->executeStatement();
$query = self::$realDatabase->getQueryBuilder();
$query->insert('calendar_reminders')
@@ -341,7 +318,7 @@ class BackendTest extends TestCase {
'notification_date' => $query->createNamedParameter(123456),
'is_repeat_based' => $query->createNamedParameter(0),
])
- ->execute();
+ ->executeStatement();
$query = self::$realDatabase->getQueryBuilder();
$query->insert('calendar_reminders')
@@ -359,7 +336,7 @@ class BackendTest extends TestCase {
'notification_date' => $query->createNamedParameter(123456),
'is_repeat_based' => $query->createNamedParameter(0),
])
- ->execute();
+ ->executeStatement();
$query = self::$realDatabase->getQueryBuilder();
$query->insert('calendar_reminders')
@@ -377,7 +354,7 @@ class BackendTest extends TestCase {
'notification_date' => $query->createNamedParameter(123499),
'is_repeat_based' => $query->createNamedParameter(0),
])
- ->execute();
+ ->executeStatement();
$query = self::$realDatabase->getQueryBuilder();
$query->insert('calendar_reminders')
@@ -395,6 +372,6 @@ class BackendTest extends TestCase {
'notification_date' => $query->createNamedParameter(123600),
'is_repeat_based' => $query->createNamedParameter(0),
])
- ->execute();
+ ->executeStatement();
}
}
diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AbstractNotificationProviderTest.php b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AbstractNotificationProviderTest.php
deleted file mode 100644
index 8335428c6d5..00000000000
--- a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AbstractNotificationProviderTest.php
+++ /dev/null
@@ -1,94 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2019, Thomas Citharel
- *
- * @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/>.
- *
- */
-namespace OCA\DAV\Tests\unit\CalDAV\Reminder\NotificationProvider;
-
-use OCA\DAV\CalDAV\Reminder\NotificationProvider\AbstractProvider;
-use OCP\IConfig;
-use OCP\IL10N;
-use OCP\ILogger;
-use OCP\IURLGenerator;
-use OCP\IUser;
-use OCP\L10N\IFactory as L10NFactory;
-use Sabre\VObject\Component\VCalendar;
-use Test\TestCase;
-
-abstract class AbstractNotificationProviderTest extends TestCase {
-
- /** @var ILogger|\PHPUnit\Framework\MockObject\MockObject */
- protected $logger;
-
- /** @var L10NFactory|\PHPUnit\Framework\MockObject\MockObject */
- protected $l10nFactory;
-
- /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
- protected $l10n;
-
- /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
- protected $urlGenerator;
-
- /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
- protected $config;
-
- /** @var AbstractProvider|\PHPUnit\Framework\MockObject\MockObject */
- protected $provider;
-
- /**
- * @var VCalendar
- */
- protected $vcalendar;
-
- /**
- * @var string
- */
- protected $calendarDisplayName;
-
- /**
- * @var IUser|\PHPUnit\Framework\MockObject\MockObject
- */
- protected $user;
-
- protected function setUp(): void {
- parent::setUp();
-
- $this->logger = $this->createMock(ILogger::class);
- $this->l10nFactory = $this->createMock(L10NFactory::class);
- $this->l10n = $this->createMock(IL10N::class);
- $this->urlGenerator = $this->createMock(IURLGenerator::class);
- $this->config = $this->createMock(IConfig::class);
-
- $this->vcalendar = new VCalendar();
- $this->vcalendar->add('VEVENT', [
- 'SUMMARY' => 'Fellowship meeting',
- 'DTSTART' => new \DateTime('2017-01-01 00:00:00+00:00'), // 1483228800,
- 'UID' => 'uid1234',
- ]);
- $this->calendarDisplayName = 'Personal';
-
- $this->user = $this->createMock(IUser::class);
- }
-}
diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AbstractNotificationProviderTestCase.php b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AbstractNotificationProviderTestCase.php
new file mode 100644
index 00000000000..70b374298ea
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AbstractNotificationProviderTestCase.php
@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Reminder\NotificationProvider;
+
+use OCA\DAV\CalDAV\Reminder\NotificationProvider\AbstractProvider;
+use OCP\IConfig;
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\L10N\IFactory as L10NFactory;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\VObject\Component\VCalendar;
+use Test\TestCase;
+
+abstract class AbstractNotificationProviderTestCase extends TestCase {
+ protected LoggerInterface&MockObject $logger;
+ protected L10NFactory&MockObject $l10nFactory;
+ protected IL10N&MockObject $l10n;
+ protected IURLGenerator&MockObject $urlGenerator;
+ protected IConfig&MockObject $config;
+ protected AbstractProvider $provider;
+ protected VCalendar $vcalendar;
+ protected string $calendarDisplayName;
+ protected IUser&MockObject $user;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->l10nFactory = $this->createMock(L10NFactory::class);
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->config = $this->createMock(IConfig::class);
+
+ $this->vcalendar = new VCalendar();
+ $this->vcalendar->add('VEVENT', [
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2017-01-01 00:00:00+00:00'), // 1483228800,
+ 'UID' => 'uid1234',
+ ]);
+ $this->calendarDisplayName = 'Personal';
+
+ $this->user = $this->createMock(IUser::class);
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AudioProviderTest.php b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AudioProviderTest.php
index 50c780396b0..d03205eaeb9 100644
--- a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AudioProviderTest.php
+++ b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AudioProviderTest.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, 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: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Tests\unit\CalDAV\Reminder\NotificationProvider;
diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/EmailProviderTest.php b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/EmailProviderTest.php
index 0352827aa5c..f7fbac2c407 100644
--- a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/EmailProviderTest.php
+++ b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/EmailProviderTest.php
@@ -3,67 +3,24 @@
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\Tests\unit\CalDAV\Reminder\NotificationProvider;
use OCA\DAV\CalDAV\Reminder\NotificationProvider\EmailProvider;
-use OCP\IConfig;
use OCP\IL10N;
-use OCP\ILogger;
-use OCP\IURLGenerator;
use OCP\IUser;
-use OCP\L10N\IFactory as L10NFactory;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMailer;
use OCP\Mail\IMessage;
+use OCP\Util;
use PHPUnit\Framework\MockObject\MockObject;
use Sabre\VObject\Component\VCalendar;
-class EmailProviderTest extends AbstractNotificationProviderTest {
+class EmailProviderTest extends AbstractNotificationProviderTestCase {
public const USER_EMAIL = 'frodo@hobb.it';
-
- /** @var ILogger|MockObject */
- protected $logger;
-
- /** @var L10NFactory|MockObject */
- protected $l10nFactory;
-
- /** @var IL10N|MockObject */
- protected $l10n;
-
- /** @var IURLGenerator|MockObject */
- protected $urlGenerator;
-
- /** @var IConfig|MockObject */
- protected $config;
-
- /** @var IMailer|MockObject */
- private $mailer;
+ private IMailer&MockObject $mailer;
protected function setUp(): void {
parent::setUp();
@@ -81,6 +38,7 @@ class EmailProviderTest extends AbstractNotificationProviderTest {
public function testSendWithoutAttendees():void {
[$user1, $user2, $user3, , $user5] = $users = $this->getUsers();
+ $principalEmailAddresses = [$user1->getEmailAddress()];
$enL10N = $this->createMock(IL10N::class);
$enL10N->method('t')
@@ -127,70 +85,54 @@ class EmailProviderTest extends AbstractNotificationProviderTest {
$message21 = $this->getMessageMock('uid2@example.com', $template2);
$message22 = $this->getMessageMock('uid3@example.com', $template2);
- $this->mailer->expects($this->at(0))
- ->method('createEMailTemplate')
- ->with('dav.calendarReminder')
- ->willReturn($template1);
-
- $this->mailer->expects($this->at(1))
- ->method('validateMailAddress')
- ->with('uid1@example.com')
- ->willReturn(true);
-
- $this->mailer->expects($this->at(2))
- ->method('createMessage')
- ->with()
- ->willReturn($message11);
- $this->mailer->expects($this->at(3))
- ->method('send')
- ->with($message11)
- ->willReturn([]);
-
- $this->mailer->expects($this->at(4))
+ $this->mailer->expects($this->exactly(2))
->method('createEMailTemplate')
->with('dav.calendarReminder')
- ->willReturn($template2);
+ ->willReturnOnConsecutiveCalls(
+ $template1,
+ $template2
+ );
- $this->mailer->expects($this->at(5))
+ $this->mailer->expects($this->exactly(4))
->method('validateMailAddress')
- ->with('uid2@example.com')
- ->willReturn(true);
-
- $this->mailer->expects($this->at(6))
- ->method('createMessage')
- ->with()
- ->willReturn($message21);
- $this->mailer->expects($this->at(7))
- ->method('send')
- ->with($message21)
- ->willReturn([]);
- $this->mailer->expects($this->at(8))
- ->method('validateMailAddress')
- ->with('uid3@example.com')
- ->willReturn(true);
+ ->willReturnMap([
+ ['uid1@example.com', true],
+ ['uid2@example.com', true],
+ ['uid3@example.com', true],
+ ['invalid', false],
+ ]);
- $this->mailer->expects($this->at(9))
+ $this->mailer->expects($this->exactly(3))
->method('createMessage')
->with()
- ->willReturn($message22);
- $this->mailer->expects($this->at(10))
+ ->willReturnOnConsecutiveCalls(
+ $message11,
+ $message21,
+ $message22
+ );
+
+ $calls = [
+ [$message11],
+ [$message21],
+ [$message22],
+ ];
+ $this->mailer->expects($this->exactly(count($calls)))
->method('send')
- ->with($message22)
- ->willReturn([]);
-
- $this->mailer->expects($this->at(11))
- ->method('validateMailAddress')
- ->with('invalid')
- ->willReturn(false);
+ ->willReturnCallback(function () use (&$calls) {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ return [];
+ });
$this->setupURLGeneratorMock(2);
$vcalendar = $this->getNoAttendeeVCalendar();
- $this->provider->send($vcalendar->VEVENT, $this->calendarDisplayName, $users);
+ $this->provider->send($vcalendar->VEVENT, $this->calendarDisplayName, $principalEmailAddresses, $users);
}
- public function testSendWithAttendees(): void {
+ public function testSendWithAttendeesWhenOwnerIsOrganizer(): void {
[$user1, $user2, $user3, , $user5] = $users = $this->getUsers();
+ $principalEmailAddresses = [$user1->getEmailAddress()];
$enL10N = $this->createMock(IL10N::class);
$enL10N->method('t')
@@ -240,103 +182,135 @@ class EmailProviderTest extends AbstractNotificationProviderTest {
$message22 = $this->getMessageMock('foo4@example.org', $template2);
$message23 = $this->getMessageMock('uid1@example.com', $template2);
- $this->mailer->expects($this->at(0))
+ $this->mailer->expects(self::exactly(2))
->method('createEMailTemplate')
->with('dav.calendarReminder')
- ->willReturn($template1);
-
- $this->mailer->expects($this->at(1))
+ ->willReturnOnConsecutiveCalls(
+ $template1,
+ $template2,
+ );
+ $this->mailer->expects($this->atLeastOnce())
->method('validateMailAddress')
- ->with('foo1@example.org')
- ->willReturn(true);
-
- $this->mailer->expects($this->at(2))
- ->method('createMessage')
- ->with()
- ->willReturn($message11);
- $this->mailer->expects($this->at(3))
- ->method('send')
- ->with($message11)
- ->willReturn([]);
- $this->mailer->expects($this->at(4))
- ->method('validateMailAddress')
- ->with('uid2@example.com')
- ->willReturn(true);
- $this->mailer->expects($this->at(5))
+ ->willReturnMap([
+ ['foo1@example.org', true],
+ ['foo3@example.org', true],
+ ['foo4@example.org', true],
+ ['uid1@example.com', true],
+ ['uid2@example.com', true],
+ ['uid3@example.com', true],
+ ['invalid', false],
+ ]);
+ $this->mailer->expects($this->exactly(6))
->method('createMessage')
->with()
- ->willReturn($message12);
- $this->mailer->expects($this->at(6))
+ ->willReturnOnConsecutiveCalls(
+ $message11,
+ $message12,
+ $message13,
+ $message21,
+ $message22,
+ $message23,
+ );
+
+ $calls = [
+ [$message11],
+ [$message12],
+ [$message13],
+ [$message21],
+ [$message22],
+ [$message23],
+ ];
+ $this->mailer->expects($this->exactly(count($calls)))
->method('send')
- ->with($message12)
- ->willReturn([]);
+ ->willReturnCallback(function () use (&$calls) {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ return [];
+ });
+ $this->setupURLGeneratorMock(2);
- $this->mailer->expects($this->at(7))
- ->method('validateMailAddress')
- ->with('uid3@example.com')
- ->willReturn(true);
+ $vcalendar = $this->getAttendeeVCalendar();
+ $this->provider->send($vcalendar->VEVENT, $this->calendarDisplayName, $principalEmailAddresses, $users);
+ }
- $this->mailer->expects($this->at(8))
- ->method('createMessage')
- ->with()
- ->willReturn($message13);
- $this->mailer->expects($this->at(9))
- ->method('send')
- ->with($message13)
- ->willReturn([]);
+ public function testSendWithAttendeesWhenOwnerIsAttendee(): void {
+ [$user1, $user2, $user3] = $this->getUsers();
+ $users = [$user2, $user3];
+ $principalEmailAddresses = [$user2->getEmailAddress()];
- $this->mailer->expects($this->at(10))
- ->method('validateMailAddress')
- ->with('invalid')
- ->willReturn(false);
+ $deL10N = $this->createMock(IL10N::class);
+ $deL10N->method('t')
+ ->willReturnArgument(0);
+ $deL10N->method('l')
+ ->willReturnArgument(0);
- $this->mailer->expects($this->at(11))
- ->method('createEMailTemplate')
- ->with('dav.calendarReminder')
- ->willReturn($template2);
+ $this->l10nFactory
+ ->method('getUserLanguage')
+ ->willReturnMap([
+ [$user2, 'de'],
+ [$user3, 'de'],
+ ]);
- $this->mailer->expects($this->at(12))
- ->method('validateMailAddress')
- ->with('foo3@example.org')
- ->willReturn(true);
+ $this->l10nFactory
+ ->method('findGenericLanguage')
+ ->willReturn('en');
- $this->mailer->expects($this->at(13))
- ->method('createMessage')
- ->with()
- ->willReturn($message21);
- $this->mailer->expects($this->at(14))
- ->method('send')
- ->with($message21)
- ->willReturn([]);
- $this->mailer->expects($this->at(15))
- ->method('validateMailAddress')
- ->with('foo4@example.org')
- ->willReturn(true);
- $this->mailer->expects($this->at(16))
- ->method('createMessage')
- ->with()
- ->willReturn($message22);
- $this->mailer->expects($this->at(17))
- ->method('send')
- ->with($message22)
- ->willReturn([]);
- $this->mailer->expects($this->at(18))
+ $this->l10nFactory
+ ->method('languageExists')
+ ->willReturnMap([
+ ['dav', 'de', true],
+ ]);
+
+ $this->l10nFactory
+ ->method('get')
+ ->willReturnMap([
+ ['dav', 'de', null, $deL10N],
+ ]);
+
+ $template1 = $this->getTemplateMock();
+ $message12 = $this->getMessageMock('uid2@example.com', $template1);
+ $message13 = $this->getMessageMock('uid3@example.com', $template1);
+
+ $this->mailer->expects(self::once())
+ ->method('createEMailTemplate')
+ ->with('dav.calendarReminder')
+ ->willReturnOnConsecutiveCalls(
+ $template1,
+ );
+ $this->mailer->expects($this->atLeastOnce())
->method('validateMailAddress')
- ->with('uid1@example.com')
- ->willReturn(true);
- $this->mailer->expects($this->at(19))
+ ->willReturnMap([
+ ['foo1@example.org', true],
+ ['foo3@example.org', true],
+ ['foo4@example.org', true],
+ ['uid1@example.com', true],
+ ['uid2@example.com', true],
+ ['uid3@example.com', true],
+ ['invalid', false],
+ ]);
+ $this->mailer->expects($this->exactly(2))
->method('createMessage')
->with()
- ->willReturn($message23);
- $this->mailer->expects($this->at(20))
+ ->willReturnOnConsecutiveCalls(
+ $message12,
+ $message13,
+ );
+
+ $calls = [
+ [$message12],
+ [$message13],
+ ];
+ $this->mailer->expects($this->exactly(count($calls)))
->method('send')
- ->with($message23)
- ->willReturn([]);
-
- $this->setupURLGeneratorMock(2);
+ ->willReturnCallback(function () use (&$calls) {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ return [];
+ });
+ $this->setupURLGeneratorMock(1);
$vcalendar = $this->getAttendeeVCalendar();
- $this->provider->send($vcalendar->VEVENT, $this->calendarDisplayName, $users);
+ $this->provider->send($vcalendar->VEVENT, $this->calendarDisplayName, $principalEmailAddresses, $users);
}
/**
@@ -345,42 +319,27 @@ class EmailProviderTest extends AbstractNotificationProviderTest {
private function getTemplateMock():IEMailTemplate {
$template = $this->createMock(IEMailTemplate::class);
- $template->expects($this->at(0))
+ $template->expects($this->once())
->method('addHeader')
->with()
->willReturn($template);
- $template->expects($this->at(1))
+ $template->expects($this->once())
->method('setSubject')
->with()
->willReturn($template);
- $template->expects($this->at(2))
+ $template->expects($this->once())
->method('addHeading')
->with()
->willReturn($template);
- $template->expects($this->at(3))
+ $template->expects($this->exactly(4))
->method('addBodyListItem')
->with()
->willReturn($template);
- $template->expects($this->at(4))
- ->method('addBodyListItem')
- ->with()
- ->willReturn($template);
-
- $template->expects($this->at(5))
- ->method('addBodyListItem')
- ->with()
- ->willReturn($template);
-
- $template->expects($this->at(6))
- ->method('addBodyListItem')
- ->with()
- ->willReturn($template);
-
- $template->expects($this->at(7))
+ $template->expects($this->once())
->method('addFooter')
->with()
->willReturn($template);
@@ -394,28 +353,31 @@ class EmailProviderTest extends AbstractNotificationProviderTest {
* @param array|null $replyTo
* @return IMessage
*/
- private function getMessageMock(string $toMail, IEMailTemplate $templateMock, array $replyTo = null):IMessage {
+ private function getMessageMock(string $toMail, IEMailTemplate $templateMock, ?array $replyTo = null):IMessage {
$message = $this->createMock(IMessage::class);
$i = 0;
- $message->expects($this->at($i++))
+ $message->expects($this->once())
->method('setFrom')
- ->with([\OCP\Util::getDefaultEmailAddress('reminders-noreply')])
+ ->with([Util::getDefaultEmailAddress('reminders-noreply')])
->willReturn($message);
if ($replyTo) {
- $message->expects($this->at($i++))
+ $message->expects($this->once())
->method('setReplyTo')
->with($replyTo)
->willReturn($message);
+ } else {
+ $message->expects($this->never())
+ ->method('setReplyTo');
}
- $message->expects($this->at($i++))
+ $message->expects($this->once())
->method('setTo')
->with([$toMail])
->willReturn($message);
- $message->expects($this->at($i++))
+ $message->expects($this->once())
->method('useTemplate')
->with($templateMock)
->willReturn($message);
@@ -447,6 +409,14 @@ class EmailProviderTest extends AbstractNotificationProviderTest {
]);
$vcalendar->VEVENT->add(
+ 'ORGANIZER',
+ 'mailto:uid1@example.com',
+ [
+ 'LANG' => 'en'
+ ]
+ );
+
+ $vcalendar->VEVENT->add(
'ATTENDEE',
'mailto:foo1@example.org',
[
@@ -486,56 +456,25 @@ class EmailProviderTest extends AbstractNotificationProviderTest {
return $vcalendar;
}
- private function setupURLGeneratorMock(int $times = 1):void {
- for ($i = 0; $i < $times; $i++) {
- $this->urlGenerator
- ->expects($this->at(8 * $i))
- ->method('imagePath')
- ->with('core', 'actions/info.png')
- ->willReturn('imagePath1');
-
- $this->urlGenerator
- ->expects($this->at(8 * $i + 1))
- ->method('getAbsoluteURL')
- ->with('imagePath1')
- ->willReturn('AbsURL1');
-
- $this->urlGenerator
- ->expects($this->at(8 * $i + 2))
- ->method('imagePath')
- ->with('core', 'places/calendar.png')
- ->willReturn('imagePath2');
-
- $this->urlGenerator
- ->expects($this->at(8 * $i + 3))
- ->method('getAbsoluteURL')
- ->with('imagePath2')
- ->willReturn('AbsURL2');
-
- $this->urlGenerator
- ->expects($this->at(8 * $i + 4))
- ->method('imagePath')
- ->with('core', 'actions/address.png')
- ->willReturn('imagePath3');
-
- $this->urlGenerator
- ->expects($this->at(8 * $i + 5))
- ->method('getAbsoluteURL')
- ->with('imagePath3')
- ->willReturn('AbsURL3');
-
- $this->urlGenerator
- ->expects($this->at(8 * $i + 6))
- ->method('imagePath')
- ->with('core', 'actions/more.png')
- ->willReturn('imagePath4');
-
- $this->urlGenerator
- ->expects($this->at(8 * $i + 7))
- ->method('getAbsoluteURL')
- ->with('imagePath4')
- ->willReturn('AbsURL4');
- }
+ private function setupURLGeneratorMock(int $times = 1): void {
+ $this->urlGenerator
+ ->expects($this->exactly($times * 4))
+ ->method('imagePath')
+ ->willReturnMap([
+ ['core', 'actions/info.png', 'imagePath1'],
+ ['core', 'places/calendar.png', 'imagePath2'],
+ ['core', 'actions/address.png', 'imagePath3'],
+ ['core', 'actions/more.png', 'imagePath4'],
+ ]);
+ $this->urlGenerator
+ ->expects($this->exactly($times * 4))
+ ->method('getAbsoluteURL')
+ ->willReturnMap([
+ ['imagePath1', 'AbsURL1'],
+ ['imagePath2', 'AbsURL2'],
+ ['imagePath3', 'AbsURL3'],
+ ['imagePath4', 'AbsURL4'],
+ ]);
}
private function getUsers(): array {
diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/PushProviderTest.php b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/PushProviderTest.php
index 9e9759f5eb8..5034af49cae 100644
--- a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/PushProviderTest.php
+++ b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/PushProviderTest.php
@@ -3,70 +3,25 @@
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\Tests\unit\CalDAV\Reminder\NotificationProvider;
use OCA\DAV\CalDAV\Reminder\NotificationProvider\PushProvider;
use OCP\AppFramework\Utility\ITimeFactory;
-use OCP\IConfig;
-use OCP\IL10N;
-use OCP\ILogger;
-use OCP\IURLGenerator;
use OCP\IUser;
-use OCP\L10N\IFactory as L10NFactory;
use OCP\Notification\IManager;
use OCP\Notification\INotification;
+use PHPUnit\Framework\MockObject\MockObject;
-class PushProviderTest extends AbstractNotificationProviderTest {
-
- /** @var ILogger|\PHPUnit\Framework\MockObject\MockObject */
- protected $logger;
-
- /** @var L10NFactory|\PHPUnit\Framework\MockObject\MockObject */
- protected $l10nFactory;
-
- /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
- protected $l10n;
-
- /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
- protected $urlGenerator;
-
- /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
- protected $config;
-
- /** @var IManager|\PHPUnit\Framework\MockObject\MockObject */
- private $manager;
-
- /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */
- private $timeFactory;
+class PushProviderTest extends AbstractNotificationProviderTestCase {
+ private IManager&MockObject $manager;
+ private ITimeFactory&MockObject $timeFactory;
protected function setUp(): void {
parent::setUp();
- $this->config = $this->createMock(IConfig::class);
$this->manager = $this->createMock(IManager::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
@@ -87,7 +42,7 @@ class PushProviderTest extends AbstractNotificationProviderTest {
public function testNotSend(): void {
$this->config->expects($this->once())
->method('getAppValue')
- ->with('dav', 'sendEventRemindersPush', 'no')
+ ->with('dav', 'sendEventRemindersPush', 'yes')
->willReturn('no');
$this->manager->expects($this->never())
@@ -107,13 +62,13 @@ class PushProviderTest extends AbstractNotificationProviderTest {
$users = [$user1, $user2, $user3];
- $this->provider->send($this->vcalendar->VEVENT, $this->calendarDisplayName, $users);
+ $this->provider->send($this->vcalendar->VEVENT, $this->calendarDisplayName, [], $users);
}
public function testSend(): void {
$this->config->expects($this->once())
->method('getAppValue')
- ->with('dav', 'sendEventRemindersPush', 'no')
+ ->with('dav', 'sendEventRemindersPush', 'yes')
->willReturn('yes');
$user1 = $this->createMock(IUser::class);
@@ -137,30 +92,27 @@ class PushProviderTest extends AbstractNotificationProviderTest {
$notification2 = $this->createNotificationMock('uid2', $dateTime);
$notification3 = $this->createNotificationMock('uid3', $dateTime);
- $this->manager->expects($this->at(0))
+ $this->manager->expects($this->exactly(3))
->method('createNotification')
- ->with()
- ->willReturn($notification1);
- $this->manager->expects($this->at(2))
- ->method('createNotification')
- ->with()
- ->willReturn($notification2);
- $this->manager->expects($this->at(4))
- ->method('createNotification')
- ->with()
- ->willReturn($notification3);
-
- $this->manager->expects($this->at(1))
- ->method('notify')
- ->with($notification1);
- $this->manager->expects($this->at(3))
- ->method('notify')
- ->with($notification2);
- $this->manager->expects($this->at(5))
+ ->willReturnOnConsecutiveCalls(
+ $notification1,
+ $notification2,
+ $notification3
+ );
+
+ $calls = [
+ $notification1,
+ $notification2,
+ $notification3,
+ ];
+ $this->manager->expects($this->exactly(3))
->method('notify')
- ->with($notification3);
+ ->willReturnCallback(function ($notification) use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, $notification);
+ });
- $this->provider->send($this->vcalendar->VEVENT, $this->calendarDisplayName, $users);
+ $this->provider->send($this->vcalendar->VEVENT, $this->calendarDisplayName, [], $users);
}
/**
diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProviderManagerTest.php b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProviderManagerTest.php
index be19f5de8a0..6b813ed0228 100644
--- a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProviderManagerTest.php
+++ b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProviderManagerTest.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\Tests\unit\CalDAV\Reminder;
@@ -35,15 +14,17 @@ use OCA\DAV\CalDAV\Reminder\NotificationProvider\PushProvider;
use OCA\DAV\CalDAV\Reminder\NotificationProviderManager;
use OCA\DAV\CalDAV\Reminder\NotificationTypeDoesNotExistException;
use OCA\DAV\Capabilities;
+use OCP\AppFramework\QueryException;
use Test\TestCase;
+/**
+ * @group DB
+ */
class NotificationProviderManagerTest extends TestCase {
-
- /** @var NotificationProviderManager|\PHPUnit\Framework\MockObject\MockObject */
- private $providerManager;
+ private NotificationProviderManager $providerManager;
/**
- * @throws \OCP\AppFramework\QueryException
+ * @throws QueryException
*/
protected function setUp(): void {
parent::setUp();
@@ -57,7 +38,7 @@ class NotificationProviderManagerTest extends TestCase {
* @throws NotificationTypeDoesNotExistException
*/
public function testGetProviderForUnknownType(): void {
- $this->expectException(\OCA\DAV\CalDAV\Reminder\NotificationTypeDoesNotExistException::class);
+ $this->expectException(NotificationTypeDoesNotExistException::class);
$this->expectExceptionMessage('Type NOT EXISTENT is not an accepted type of notification');
$this->providerManager->getProvider('NOT EXISTENT');
@@ -68,7 +49,7 @@ class NotificationProviderManagerTest extends TestCase {
* @throws ProviderNotAvailableException
*/
public function testGetProviderForUnRegisteredType(): void {
- $this->expectException(\OCA\DAV\CalDAV\Reminder\NotificationProvider\ProviderNotAvailableException::class);
+ $this->expectException(ProviderNotAvailableException::class);
$this->expectExceptionMessage('No notification provider for type AUDIO available');
$this->providerManager->getProvider('AUDIO');
@@ -86,7 +67,7 @@ class NotificationProviderManagerTest extends TestCase {
}
/**
- * @throws \OCP\AppFramework\QueryException
+ * @throws QueryException
*/
public function testRegisterBadProvider(): void {
$this->expectException(\InvalidArgumentException::class);
diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotifierTest.php b/apps/dav/tests/unit/CalDAV/Reminder/NotifierTest.php
index 1c823ac6830..c091f590711 100644
--- a/apps/dav/tests/unit/CalDAV/Reminder/NotifierTest.php
+++ b/apps/dav/tests/unit/CalDAV/Reminder/NotifierTest.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\Tests\unit\CalDAV\Reminder;
@@ -37,24 +16,16 @@ use OCP\IURLGenerator;
use OCP\L10N\IFactory;
use OCP\Notification\AlreadyProcessedException;
use OCP\Notification\INotification;
+use OCP\Notification\UnknownNotificationException;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class NotifierTest extends TestCase {
- /** @var Notifier */
- protected $notifier;
-
- /** @var IFactory|MockObject */
- protected $factory;
-
- /** @var IURLGenerator|MockObject */
- protected $urlGenerator;
-
- /** @var IL10N|MockObject */
- protected $l10n;
-
- /** @var ITimeFactory|MockObject */
- protected $timeFactory;
+ protected IFactory&MockObject $factory;
+ protected IURLGenerator&MockObject $urlGenerator;
+ protected IL10N&MockObject $l10n;
+ protected ITimeFactory&MockObject $timeFactory;
+ protected Notifier $notifier;
protected function setUp(): void {
parent::setUp();
@@ -109,10 +80,10 @@ class NotifierTest extends TestCase {
public function testPrepareWrongApp(): void {
- $this->expectException(\InvalidArgumentException::class);
+ $this->expectException(UnknownNotificationException::class);
$this->expectExceptionMessage('Notification not from this app');
- /** @var INotification|MockObject $notification */
+ /** @var INotification&MockObject $notification */
$notification = $this->createMock(INotification::class);
$notification->expects($this->once())
@@ -125,11 +96,11 @@ class NotifierTest extends TestCase {
}
- public function testPrepareWrongSubject() {
- $this->expectException(\InvalidArgumentException::class);
+ public function testPrepareWrongSubject(): void {
+ $this->expectException(UnknownNotificationException::class);
$this->expectExceptionMessage('Unknown subject');
- /** @var INotification|MockObject $notification */
+ /** @var INotification&MockObject $notification */
$notification = $this->createMock(INotification::class);
$notification->expects($this->once())
@@ -142,7 +113,15 @@ class NotifierTest extends TestCase {
$this->notifier->prepare($notification, 'en');
}
- public function dataPrepare(): array {
+ private static function hasPhpDatetimeDiffBug(): bool {
+ $d1 = \DateTime::createFromFormat(\DateTimeInterface::ATOM, '2023-11-22T11:52:00+01:00');
+ $d2 = new \DateTime('2023-11-22T10:52:03', new \DateTimeZone('UTC'));
+
+ // The difference is 3 seconds, not -1year+11months+…
+ return $d1->diff($d2)->y < 0;
+ }
+
+ public static function dataPrepare(): array {
return [
[
'calendar_reminder',
@@ -150,7 +129,7 @@ class NotifierTest extends TestCase {
'title' => 'Title of this event',
'start_atom' => '2005-08-15T15:52:01+02:00'
],
- 'Title of this event (in 1 hour, 52 minutes)',
+ self::hasPhpDatetimeDiffBug() ? 'Title of this event' : 'Title of this event (in 1 hour, 52 minutes)',
[
'title' => 'Title of this event',
'description' => null,
@@ -172,7 +151,7 @@ class NotifierTest extends TestCase {
'title' => 'Title of this event',
'start_atom' => '2005-08-15T13:00:00+02:00',
],
- 'Title of this event (1 hour ago)',
+ self::hasPhpDatetimeDiffBug() ? 'Title of this event' : 'Title of this event (1 hour ago)',
[
'title' => 'Title of this event',
'description' => null,
@@ -191,18 +170,9 @@ class NotifierTest extends TestCase {
];
}
- /**
- * @dataProvider dataPrepare
- *
- * @param string $subjectType
- * @param array $subjectParams
- * @param string $subject
- * @param array $messageParams
- * @param string $message
- * @throws \Exception
- */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataPrepare')]
public function testPrepare(string $subjectType, array $subjectParams, string $subject, array $messageParams, string $message): void {
- /** @var INotification|MockObject $notification */
+ /** @var INotification&MockObject $notification */
$notification = $this->createMock(INotification::class);
$notification->expects($this->once())
@@ -247,7 +217,7 @@ class NotifierTest extends TestCase {
}
public function testPassedEvent(): void {
- /** @var INotification|MockObject $notification */
+ /** @var INotification&MockObject $notification */
$notification = $this->createMock(INotification::class);
$notification->expects($this->once())
@@ -281,7 +251,7 @@ class NotifierTest extends TestCase {
$notification->expects($this->once())
->method('setParsedSubject')
- ->with('Title of this event (6 hours ago)')
+ ->with(self::hasPhpDatetimeDiffBug() ? 'Title of this event' : 'Title of this event (6 hours ago)')
->willReturnSelf();
$this->expectException(AlreadyProcessedException::class);
diff --git a/apps/dav/tests/unit/CalDAV/Reminder/ReminderServiceTest.php b/apps/dav/tests/unit/CalDAV/Reminder/ReminderServiceTest.php
index 39fbf1c79ff..c18901c5f58 100644
--- a/apps/dav/tests/unit/CalDAV/Reminder/ReminderServiceTest.php
+++ b/apps/dav/tests/unit/CalDAV/Reminder/ReminderServiceTest.php
@@ -3,72 +3,39 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Thomas Citharel
- *
- * @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\Tests\unit\CalDAV\Reminder;
+use DateTime;
+use DateTimeZone;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\Reminder\Backend;
use OCA\DAV\CalDAV\Reminder\INotificationProvider;
use OCA\DAV\CalDAV\Reminder\NotificationProviderManager;
use OCA\DAV\CalDAV\Reminder\ReminderService;
+use OCA\DAV\Connector\Sabre\Principal;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserManager;
-use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
use Test\TestCase;
class ReminderServiceTest extends TestCase {
-
- /** @var Backend|\PHPUnit\Framework\MockObject\MockObject */
- private $backend;
-
- /** @var NotificationProviderManager|\PHPUnit\Framework\MockObject\MockObject */
- private $notificationProviderManager;
-
- /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */
- private $userManager;
-
- /** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject*/
- private $groupManager;
-
- /** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */
- private $userSession;
-
- /** @var CalDavBackend|\PHPUnit\Framework\MockObject\MockObject */
- private $caldavBackend;
-
- /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */
- private $timeFactory;
-
- /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
- private $config;
-
- /** @var ReminderService */
- private $reminderService;
+ private Backend&MockObject $backend;
+ private NotificationProviderManager&MockObject $notificationProviderManager;
+ private IUserManager&MockObject $userManager;
+ private IGroupManager&MockObject $groupManager;
+ private CalDavBackend&MockObject $caldavBackend;
+ private ITimeFactory&MockObject $timeFactory;
+ private IConfig&MockObject $config;
+ private LoggerInterface&MockObject $logger;
+ private Principal&MockObject $principalConnector;
+ private ReminderService $reminderService;
public const CALENDAR_DATA = <<<EOD
BEGIN:VCALENDAR
@@ -189,6 +156,85 @@ END:VEVENT
END:VCALENDAR
EOD;
+ private const CALENDAR_DATA_ONE_TIME = <<<EOD
+BEGIN:VCALENDAR
+PRODID:-//IDN nextcloud.com//Calendar app 4.3.0-alpha.0//EN
+CALSCALE:GREGORIAN
+VERSION:2.0
+BEGIN:VEVENT
+CREATED:20230203T154600Z
+DTSTAMP:20230203T154602Z
+LAST-MODIFIED:20230203T154602Z
+SEQUENCE:2
+UID:f6a565b6-f9a8-4d1e-9d01-c8dcbe716b7e
+DTSTART;TZID=Europe/Vienna:20230204T090000
+DTEND;TZID=Europe/Vienna:20230204T120000
+STATUS:CONFIRMED
+SUMMARY:TEST
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER;RELATED=START:-PT1H
+END:VALARM
+END:VEVENT
+BEGIN:VTIMEZONE
+TZID:Europe/Vienna
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+END:VCALENDAR
+EOD;
+
+ private const CALENDAR_DATA_ALL_DAY = <<<EOD
+BEGIN:VCALENDAR
+PRODID:-//IDN nextcloud.com//Calendar app 4.3.0-alpha.0//EN
+CALSCALE:GREGORIAN
+VERSION:2.0
+BEGIN:VEVENT
+CREATED:20230203T113430Z
+DTSTAMP:20230203T113432Z
+LAST-MODIFIED:20230203T113432Z
+SEQUENCE:2
+UID:a163a056-ba26-44a2-8080-955f19611a8f
+DTSTART;VALUE=DATE:20230204
+DTEND;VALUE=DATE:20230205
+STATUS:CONFIRMED
+SUMMARY:TEST
+BEGIN:VALARM
+ACTION:EMAIL
+TRIGGER;RELATED=START:-PT1H
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ private const PAGO_PAGO_VTIMEZONE_ICS = <<<ICS
+BEGIN:VCALENDAR
+BEGIN:VTIMEZONE
+TZID:Pacific/Pago_Pago
+BEGIN:STANDARD
+TZOFFSETFROM:-1100
+TZOFFSETTO:-1100
+TZNAME:SST
+DTSTART:19700101T000000
+END:STANDARD
+END:VTIMEZONE
+END:VCALENDAR
+ICS;
+
+ private ?string $oldTimezone;
+
protected function setUp(): void {
parent::setUp();
@@ -199,16 +245,22 @@ EOD;
$this->caldavBackend = $this->createMock(CalDavBackend::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->config = $this->createMock(IConfig::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->principalConnector = $this->createMock(Principal::class);
$this->caldavBackend->method('getShares')->willReturn([]);
- $this->reminderService = new ReminderService($this->backend,
+ $this->reminderService = new ReminderService(
+ $this->backend,
$this->notificationProviderManager,
$this->userManager,
$this->groupManager,
$this->caldavBackend,
$this->timeFactory,
- $this->config);
+ $this->config,
+ $this->logger,
+ $this->principalConnector,
+ );
}
public function testOnCalendarObjectDelete():void {
@@ -232,18 +284,59 @@ EOD;
'component' => 'vevent',
];
- $this->backend->expects($this->exactly(2))
+ $calls = [
+ [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'de919af7429d3b5c11e8b9d289b411a6', 'EMAIL', true, 1465429500, false],
+ [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', '35b3eae8e792aa2209f0b4e1a302f105', 'DISPLAY', false, 1465344000, false]
+ ];
+ $this->backend->expects($this->exactly(count($calls)))
->method('insertReminder')
- ->withConsecutive(
- [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'de919af7429d3b5c11e8b9d289b411a6', 'EMAIL', true, 1465429500, false],
- [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', '35b3eae8e792aa2209f0b4e1a302f105', 'DISPLAY', false, 1465344000, false]
- )
- ->willReturn(1);
+ ->willReturnCallback(function () use (&$calls) {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ return 1;
+ });
$this->timeFactory->expects($this->once())
->method('getDateTime')
->with()
- ->willReturn(\DateTime::createFromFormat(\DateTime::ATOM, '2016-06-08T00:00:00+00:00'));
+ ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2016-06-08T00:00:00+00:00'));
+
+ $this->reminderService->onCalendarObjectCreate($objectData);
+ }
+
+ /**
+ * RFC5545 says DTSTART is REQUIRED, but we have seen event without the prop
+ */
+ public function testOnCalendarObjectCreateNoDtstart(): void {
+ $calendarData = <<<EOD
+BEGIN:VCALENDAR
+PRODID:-//Nextcloud calendar v1.6.4
+BEGIN:VEVENT
+CREATED:20160602T133732
+DTSTAMP:20160602T133732
+LAST-MODIFIED:20160602T133732
+UID:wej2z68l9h
+SUMMARY:Test Event
+BEGIN:VALARM
+ACTION:EMAIL
+TRIGGER:-PT15M
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER;VALUE=DATE-TIME:20160608T000000Z
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $objectData = [
+ 'calendardata' => $calendarData,
+ 'id' => '42',
+ 'calendarid' => '1337',
+ 'component' => 'vevent',
+ ];
+
+ $this->backend->expects($this->never())
+ ->method('insertReminder');
$this->reminderService->onCalendarObjectCreate($objectData);
}
@@ -256,21 +349,25 @@ EOD;
'component' => 'vevent',
];
- $this->backend->expects($this->exactly(5))
+ $calls = [
+ [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429500, false],
+ [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429620, true],
+ [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429740, true],
+ [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429860, true],
+ [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429980, true]
+ ];
+ $this->backend->expects($this->exactly(count($calls)))
->method('insertReminder')
- ->withConsecutive(
- [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429500, false],
- [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429620, true],
- [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429740, true],
- [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429860, true],
- [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429980, true]
- )
- ->willReturn(1);
+ ->willReturnCallback(function () use (&$calls) {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ return 1;
+ });
$this->timeFactory->expects($this->once())
->method('getDateTime')
->with()
- ->willReturn(\DateTime::createFromFormat(\DateTime::ATOM, '2016-06-08T00:00:00+00:00'));
+ ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2016-06-08T00:00:00+00:00'));
$this->reminderService->onCalendarObjectCreate($objectData);
}
@@ -283,18 +380,21 @@ EOD;
'component' => 'vevent',
];
- $this->backend->expects($this->exactly(2))
+ $calls = [
+ [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'de919af7429d3b5c11e8b9d289b411a6', 'EMAIL', true, 1467243900, false],
+ [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', '8996992118817f9f311ac5cc56d1cc97', 'EMAIL', true, 1467158400, false]
+ ];
+ $this->backend->expects($this->exactly(count($calls)))
->method('insertReminder')
- ->withConsecutive(
- [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'de919af7429d3b5c11e8b9d289b411a6', 'EMAIL', true, 1467243900, false],
- [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', '8996992118817f9f311ac5cc56d1cc97', 'EMAIL', true, 1467158400, false]
- )
- ->willReturn(1);
+ ->willReturnCallback(function () use (&$calls) {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ return 1;
+ });
$this->timeFactory->expects($this->once())
->method('getDateTime')
- ->with()
- ->willReturn(\DateTime::createFromFormat(\DateTime::ATOM, '2016-06-29T00:00:00+00:00'));
+ ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2016-06-29T00:00:00+00:00'));
$this->reminderService->onCalendarObjectCreate($objectData);
}
@@ -313,6 +413,87 @@ EOD;
$this->reminderService->onCalendarObjectCreate($objectData);
}
+ public function testOnCalendarObjectCreateAllDayWithNullTimezone(): void {
+ $objectData = [
+ 'calendardata' => self::CALENDAR_DATA_ALL_DAY,
+ 'id' => '42',
+ 'calendarid' => '1337',
+ 'component' => 'vevent',
+ ];
+ $this->timeFactory->expects($this->once())
+ ->method('getDateTime')
+ ->with()
+ ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2023-02-03T13:28:00+00:00'));
+ $this->caldavBackend->expects(self::once())
+ ->method('getCalendarById')
+ ->with(1337)
+ ->willReturn([
+ '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => null,
+ ]);
+
+ // One hour before midnight relative to the server's time
+ $expectedReminderTimstamp = (new DateTime('2023-02-03T23:00:00'))->getTimestamp();
+ $this->backend->expects(self::once())
+ ->method('insertReminder')
+ ->with(1337, 42, self::anything(), false, 1675468800, false, self::anything(), self::anything(), 'EMAIL', true, $expectedReminderTimstamp, false);
+
+ $this->reminderService->onCalendarObjectCreate($objectData);
+ }
+
+ public function testOnCalendarObjectCreateAllDayWithBlankTimezone(): void {
+ $objectData = [
+ 'calendardata' => self::CALENDAR_DATA_ALL_DAY,
+ 'id' => '42',
+ 'calendarid' => '1337',
+ 'component' => 'vevent',
+ ];
+ $this->timeFactory->expects($this->once())
+ ->method('getDateTime')
+ ->with()
+ ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2023-02-03T13:28:00+00:00'));
+ $this->caldavBackend->expects(self::once())
+ ->method('getCalendarById')
+ ->with(1337)
+ ->willReturn([
+ '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => '',
+ ]);
+
+ // One hour before midnight relative to the server's time
+ $expectedReminderTimstamp = (new DateTime('2023-02-03T23:00:00'))->getTimestamp();
+ $this->backend->expects(self::once())
+ ->method('insertReminder')
+ ->with(1337, 42, self::anything(), false, 1675468800, false, self::anything(), self::anything(), 'EMAIL', true, $expectedReminderTimstamp, false);
+
+ $this->reminderService->onCalendarObjectCreate($objectData);
+ }
+
+ public function testOnCalendarObjectCreateAllDayWithTimezone(): void {
+ $objectData = [
+ 'calendardata' => self::CALENDAR_DATA_ALL_DAY,
+ 'id' => '42',
+ 'calendarid' => '1337',
+ 'component' => 'vevent',
+ ];
+ $this->timeFactory->expects($this->once())
+ ->method('getDateTime')
+ ->with()
+ ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2023-02-03T13:28:00+00:00'));
+ $this->caldavBackend->expects(self::once())
+ ->method('getCalendarById')
+ ->with(1337)
+ ->willReturn([
+ '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => self::PAGO_PAGO_VTIMEZONE_ICS,
+ ]);
+
+ // One hour before midnight relative to the timezone
+ $expectedReminderTimstamp = (new DateTime('2023-02-03T23:00:00', new DateTimeZone('Pacific/Pago_Pago')))->getTimestamp();
+ $this->backend->expects(self::once())
+ ->method('insertReminder')
+ ->with(1337, 42, 'a163a056-ba26-44a2-8080-955f19611a8f', false, self::anything(), false, self::anything(), self::anything(), 'EMAIL', true, $expectedReminderTimstamp, false);
+
+ $this->reminderService->onCalendarObjectCreate($objectData);
+ }
+
public function testOnCalendarObjectCreateRecurringEntryWithRepeat():void {
$objectData = [
'calendardata' => self::CALENDAR_DATA_RECURRING_REPEAT,
@@ -320,29 +501,72 @@ EOD;
'calendarid' => '1337',
'component' => 'vevent',
];
+ $this->caldavBackend->expects(self::once())
+ ->method('getCalendarById')
+ ->with(1337)
+ ->willReturn([
+ '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => null,
+ ]);
- $this->backend->expects($this->exactly(6))
+ $calls = [
+ [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467243900, false],
+ [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467244020, true],
+ [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467244140, true],
+ [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467244260, true],
+ [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467244380, true],
+ [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', '8996992118817f9f311ac5cc56d1cc97', 'EMAIL', true, 1467158400, false]
+ ];
+ $this->backend->expects($this->exactly(count($calls)))
->method('insertReminder')
- ->withConsecutive(
- [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467243900, false],
- [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467244020, true],
- [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467244140, true],
- [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467244260, true],
- [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467244380, true],
- [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', '8996992118817f9f311ac5cc56d1cc97', 'EMAIL', true, 1467158400, false]
- )
- ->willReturn(1);
+ ->willReturnCallback(function () use (&$calls) {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ return 1;
+ });
+
+ $this->timeFactory->expects($this->once())
+ ->method('getDateTime')
+ ->with()
+ ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2016-06-29T00:00:00+00:00'));
+ $this->reminderService->onCalendarObjectCreate($objectData);
+ }
+
+ public function testOnCalendarObjectCreateWithEventTimezoneAndCalendarTimezone():void {
+ $objectData = [
+ 'calendardata' => self::CALENDAR_DATA_ONE_TIME,
+ 'id' => '42',
+ 'calendarid' => '1337',
+ 'component' => 'vevent',
+ ];
+ $this->caldavBackend->expects(self::once())
+ ->method('getCalendarById')
+ ->with(1337)
+ ->willReturn([
+ '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => self::PAGO_PAGO_VTIMEZONE_ICS,
+ ]);
+ $expectedReminderTimstamp = (new DateTime('2023-02-04T08:00:00', new DateTimeZone('Europe/Vienna')))->getTimestamp();
+ $this->backend->expects(self::once())
+ ->method('insertReminder')
+ ->with(1337, 42, self::anything(), false, self::anything(), false, self::anything(), self::anything(), self::anything(), true, $expectedReminderTimstamp, false)
+ ->willReturn(1);
+ $this->caldavBackend->expects(self::once())
+ ->method('getCalendarById')
+ ->with(1337)
+ ->willReturn([
+ '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => null,
+ ]);
$this->timeFactory->expects($this->once())
->method('getDateTime')
->with()
- ->willReturn(\DateTime::createFromFormat(\DateTime::ATOM, '2016-06-29T00:00:00+00:00'));
+ ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2023-02-03T13:28:00+00:00'));
+ ;
$this->reminderService->onCalendarObjectCreate($objectData);
}
public function testProcessReminders():void {
- $this->backend->expects($this->at(0))
+ $this->backend->expects($this->once())
->method('getRemindersToProcess')
->with()
->willReturn([
@@ -438,60 +662,34 @@ EOD;
]
]);
- $this->notificationProviderManager->expects($this->at(0))
+ $this->notificationProviderManager->expects($this->exactly(5))
->method('hasProvider')
- ->with('EMAIL')
- ->willReturn(true);
+ ->willReturnMap([
+ ['EMAIL', true],
+ ['DISPLAY', true],
+ ]);
$provider1 = $this->createMock(INotificationProvider::class);
- $this->notificationProviderManager->expects($this->at(1))
- ->method('getProvider')
- ->with('EMAIL')
- ->willReturn($provider1);
-
- $this->notificationProviderManager->expects($this->at(2))
- ->method('hasProvider')
- ->with('EMAIL')
- ->willReturn(true);
-
$provider2 = $this->createMock(INotificationProvider::class);
- $this->notificationProviderManager->expects($this->at(3))
- ->method('getProvider')
- ->with('EMAIL')
- ->willReturn($provider2);
-
- $this->notificationProviderManager->expects($this->at(4))
- ->method('hasProvider')
- ->with('DISPLAY')
- ->willReturn(true);
-
$provider3 = $this->createMock(INotificationProvider::class);
- $this->notificationProviderManager->expects($this->at(5))
- ->method('getProvider')
- ->with('DISPLAY')
- ->willReturn($provider3);
-
- $this->notificationProviderManager->expects($this->at(6))
- ->method('hasProvider')
- ->with('EMAIL')
- ->willReturn(true);
-
$provider4 = $this->createMock(INotificationProvider::class);
- $this->notificationProviderManager->expects($this->at(7))
- ->method('getProvider')
- ->with('EMAIL')
- ->willReturn($provider4);
-
- $this->notificationProviderManager->expects($this->at(8))
- ->method('hasProvider')
- ->with('EMAIL')
- ->willReturn(true);
-
$provider5 = $this->createMock(INotificationProvider::class);
- $this->notificationProviderManager->expects($this->at(9))
+
+ $getProviderCalls = [
+ ['EMAIL', $provider1],
+ ['EMAIL', $provider2],
+ ['DISPLAY', $provider3],
+ ['EMAIL', $provider4],
+ ['EMAIL', $provider5],
+ ];
+ $this->notificationProviderManager->expects($this->exactly(count($getProviderCalls)))
->method('getProvider')
- ->with('EMAIL')
- ->willReturn($provider5);
+ ->willReturnCallback(function () use (&$getProviderCalls) {
+ $expected = array_shift($getProviderCalls);
+ $return = array_pop($expected);
+ $this->assertEquals($expected, func_get_args());
+ return $return;
+ });
$user = $this->createMock(IUser::class);
$this->userManager->expects($this->exactly(5))
@@ -502,7 +700,7 @@ EOD;
$provider1->expects($this->once())
->method('send')
->with($this->callback(function ($vevent) {
- if ($vevent->DTSTART->getDateTime()->format(\DateTime::ATOM) !== '2016-06-09T00:00:00+00:00') {
+ if ($vevent->DTSTART->getDateTime()->format(DateTime::ATOM) !== '2016-06-09T00:00:00+00:00') {
return false;
}
return true;
@@ -510,7 +708,7 @@ EOD;
$provider2->expects($this->once())
->method('send')
->with($this->callback(function ($vevent) {
- if ($vevent->DTSTART->getDateTime()->format(\DateTime::ATOM) !== '2016-06-09T00:00:00+00:00') {
+ if ($vevent->DTSTART->getDateTime()->format(DateTime::ATOM) !== '2016-06-09T00:00:00+00:00') {
return false;
}
return true;
@@ -518,7 +716,7 @@ EOD;
$provider3->expects($this->once())
->method('send')
->with($this->callback(function ($vevent) {
- if ($vevent->DTSTART->getDateTime()->format(\DateTime::ATOM) !== '2016-06-09T00:00:00+00:00') {
+ if ($vevent->DTSTART->getDateTime()->format(DateTime::ATOM) !== '2016-06-09T00:00:00+00:00') {
return false;
}
return true;
@@ -526,7 +724,7 @@ EOD;
$provider4->expects($this->once())
->method('send')
->with($this->callback(function ($vevent) {
- if ($vevent->DTSTART->getDateTime()->format(\DateTime::ATOM) !== '2016-06-30T00:00:00+00:00') {
+ if ($vevent->DTSTART->getDateTime()->format(DateTime::ATOM) !== '2016-06-30T00:00:00+00:00') {
return false;
}
return true;
@@ -534,55 +732,45 @@ EOD;
$provider5->expects($this->once())
->method('send')
->with($this->callback(function ($vevent) {
- if ($vevent->DTSTART->getDateTime()->format(\DateTime::ATOM) !== '2016-07-07T00:00:00+00:00') {
+ if ($vevent->DTSTART->getDateTime()->format(DateTime::ATOM) !== '2016-07-07T00:00:00+00:00') {
return false;
}
return true;
}, 'Displayname 123', $user));
- $this->backend->expects($this->at(1))
- ->method('removeReminder')
- ->with(1);
- $this->backend->expects($this->at(2))
- ->method('removeReminder')
- ->with(2);
- $this->backend->expects($this->at(3))
- ->method('removeReminder')
- ->with(3);
- $this->backend->expects($this->at(4))
- ->method('removeReminder')
- ->with(4);
- $this->backend->expects($this->at(5))
- ->method('insertReminder')
- ->with(1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467848700, false)
- ->willReturn(99);
-
- $this->backend->expects($this->at(6))
- ->method('insertReminder')
- ->with(1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467848820, true)
- ->willReturn(99);
- $this->backend->expects($this->at(7))
- ->method('insertReminder')
- ->with(1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467848940, true)
- ->willReturn(99);
- $this->backend->expects($this->at(8))
- ->method('insertReminder')
- ->with(1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467849060, true)
- ->willReturn(99);
- $this->backend->expects($this->at(9))
- ->method('insertReminder')
- ->with(1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467849180, true)
- ->willReturn(99);
- $this->backend->expects($this->at(10))
+ $removeReminderCalls = [
+ [1],
+ [2],
+ [3],
+ [4],
+ [5],
+ ];
+ $this->backend->expects($this->exactly(5))
->method('removeReminder')
- ->with(5);
- $this->backend->expects($this->at(11))
+ ->willReturnCallback(function () use (&$removeReminderCalls): void {
+ $expected = array_shift($removeReminderCalls);
+ $this->assertEquals($expected, func_get_args());
+ });
+
+
+ $insertReminderCalls = [
+ [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467848700, false],
+ [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467848820, true],
+ [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467848940, true],
+ [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467849060, true],
+ [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467849180, true],
+ [1337, 42, 'wej2z68l9h', true, 1468454400, false, 'fbdb2726bc0f7dfacac1d881c1453e20', '8996992118817f9f311ac5cc56d1cc97', 'EMAIL', true, 1467763200, false],
+ ];
+ $this->backend->expects($this->exactly(count($insertReminderCalls)))
->method('insertReminder')
- ->with(1337, 42, 'wej2z68l9h', true, 1468454400, false, 'fbdb2726bc0f7dfacac1d881c1453e20', '8996992118817f9f311ac5cc56d1cc97', 'EMAIL', true, 1467763200, false)
- ->willReturn(99);
+ ->willReturnCallback(function () use (&$insertReminderCalls) {
+ $expected = array_shift($insertReminderCalls);
+ $this->assertEquals($expected, func_get_args());
+ return 99;
+ });
$this->timeFactory->method('getDateTime')
- ->willReturn(\DateTime::createFromFormat(\DateTime::ATOM, '2016-06-08T00:00:00+00:00'));
+ ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2016-06-08T00:00:00+00:00'));
$this->reminderService->processReminders();
}
diff --git a/apps/dav/tests/unit/CalDAV/ResourceBooking/AbstractPrincipalBackendTest.php b/apps/dav/tests/unit/CalDAV/ResourceBooking/AbstractPrincipalBackendTestCase.php
index 5f490286347..364bc74de49 100644
--- a/apps/dav/tests/unit/CalDAV/ResourceBooking/AbstractPrincipalBackendTest.php
+++ b/apps/dav/tests/unit/CalDAV/ResourceBooking/AbstractPrincipalBackendTestCase.php
@@ -1,90 +1,55 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018, Georg Ehrke
- *
- * @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: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Tests\unit\CalDAV\ResourceBooking;
use OCA\DAV\CalDAV\Proxy\Proxy;
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
+use OCA\DAV\CalDAV\ResourceBooking\ResourcePrincipalBackend;
+use OCA\DAV\CalDAV\ResourceBooking\RoomPrincipalBackend;
use OCP\IGroupManager;
-use OCP\ILogger;
use OCP\IUser;
use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
use Sabre\DAV\PropPatch;
use Test\TestCase;
-abstract class AbstractPrincipalBackendTest extends TestCase {
-
- /** @var \OCA\DAV\CalDAV\ResourceBooking\ResourcePrincipalBackend|\OCA\DAV\CalDAV\ResourceBooking\RoomPrincipalBackend */
- protected $principalBackend;
-
- /** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */
- protected $userSession;
-
- /** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject */
- protected $groupManager;
-
- /** @var ILogger|\PHPUnit\Framework\MockObject\MockObject */
- protected $logger;
-
- /** @var ProxyMapper|\PHPUnit\Framework\MockObject\MockObject */
- protected $proxyMapper;
-
- /** @var string */
- protected $mainDbTable;
-
- /** @var string */
- protected $metadataDbTable;
-
- /** @var string */
- protected $foreignKey;
-
- /** @var string */
- protected $principalPrefix;
-
- /** @var string */
- protected $expectedCUType;
+abstract class AbstractPrincipalBackendTestCase extends TestCase {
+ protected ResourcePrincipalBackend|RoomPrincipalBackend $principalBackend;
+ protected IUserSession&MockObject $userSession;
+ protected IGroupManager&MockObject $groupManager;
+ protected LoggerInterface&MockObject $logger;
+ protected ProxyMapper&MockObject $proxyMapper;
+ protected string $mainDbTable;
+ protected string $metadataDbTable;
+ protected string $foreignKey;
+ protected string $principalPrefix;
+ protected string $expectedCUType;
protected function setUp(): void {
parent::setUp();
$this->userSession = $this->createMock(IUserSession::class);
$this->groupManager = $this->createMock(IGroupManager::class);
- $this->logger = $this->createMock(ILogger::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
$this->proxyMapper = $this->createMock(ProxyMapper::class);
}
protected function tearDown(): void {
$query = self::$realDatabase->getQueryBuilder();
- $query->delete('calendar_resources')->execute();
- $query->delete('calendar_resources_md')->execute();
- $query->delete('calendar_rooms')->execute();
- $query->delete('calendar_rooms_md')->execute();
+ $query->delete('calendar_resources')->executeStatement();
+ $query->delete('calendar_resources_md')->executeStatement();
+ $query->delete('calendar_rooms')->executeStatement();
+ $query->delete('calendar_rooms_md')->executeStatement();
}
- public function testGetPrincipalsByPrefix() {
+ public function testGetPrincipalsByPrefix(): void {
$actual = $this->principalBackend->getPrincipalsByPrefix($this->principalPrefix);
$this->assertEquals([
@@ -132,12 +97,12 @@ abstract class AbstractPrincipalBackendTest extends TestCase {
], $actual);
}
- public function testGetNoPrincipalsByPrefixForWrongPrincipalPrefix() {
+ public function testGetNoPrincipalsByPrefixForWrongPrincipalPrefix(): void {
$actual = $this->principalBackend->getPrincipalsByPrefix('principals/users');
$this->assertEquals([], $actual);
}
- public function testGetPrincipalByPath() {
+ public function testGetPrincipalByPath(): void {
$actual = $this->principalBackend->getPrincipalByPath($this->principalPrefix . '/backend2-res3');
$this->assertEquals([
'uri' => $this->principalPrefix . '/backend2-res3',
@@ -149,22 +114,22 @@ abstract class AbstractPrincipalBackendTest extends TestCase {
], $actual);
}
- public function testGetPrincipalByPathNotFound() {
+ public function testGetPrincipalByPathNotFound(): void {
$actual = $this->principalBackend->getPrincipalByPath($this->principalPrefix . '/db-123');
$this->assertEquals(null, $actual);
}
- public function testGetPrincipalByPathWrongPrefix() {
+ public function testGetPrincipalByPathWrongPrefix(): void {
$actual = $this->principalBackend->getPrincipalByPath('principals/users/foo-bar');
$this->assertEquals(null, $actual);
}
- public function testGetGroupMemberSet() {
+ public function testGetGroupMemberSet(): void {
$actual = $this->principalBackend->getGroupMemberSet($this->principalPrefix . '/backend1-res1');
$this->assertEquals([], $actual);
}
- public function testGetGroupMemberSetProxyRead() {
+ public function testGetGroupMemberSetProxyRead(): void {
$proxy1 = new Proxy();
$proxy1->setProxyId('proxyId1');
$proxy1->setPermissions(1);
@@ -186,7 +151,7 @@ abstract class AbstractPrincipalBackendTest extends TestCase {
$this->assertEquals(['proxyId1'], $actual);
}
- public function testGetGroupMemberSetProxyWrite() {
+ public function testGetGroupMemberSetProxyWrite(): void {
$proxy1 = new Proxy();
$proxy1->setProxyId('proxyId1');
$proxy1->setPermissions(1);
@@ -208,7 +173,7 @@ abstract class AbstractPrincipalBackendTest extends TestCase {
$this->assertEquals(['proxyId2', 'proxyId3'], $actual);
}
- public function testGetGroupMembership() {
+ public function testGetGroupMembership(): void {
$proxy1 = new Proxy();
$proxy1->setOwnerId('proxyId1');
$proxy1->setPermissions(1);
@@ -227,15 +192,14 @@ abstract class AbstractPrincipalBackendTest extends TestCase {
$this->assertEquals(['proxyId1/calendar-proxy-read', 'proxyId2/calendar-proxy-write'], $actual);
}
- public function testSetGroupMemberSet() {
- $this->proxyMapper->expects($this->at(0))
+ public function testSetGroupMemberSet(): void {
+ $this->proxyMapper->expects($this->once())
->method('getProxiesOf')
->with($this->principalPrefix . '/backend1-res1')
->willReturn([]);
- $this->proxyMapper->expects($this->at(1))
- ->method('insert')
- ->with($this->callback(function ($proxy) {
+ $calls = [
+ function ($proxy) {
/** @var Proxy $proxy */
if ($proxy->getOwnerId() !== $this->principalPrefix . '/backend1-res1') {
return false;
@@ -248,10 +212,8 @@ abstract class AbstractPrincipalBackendTest extends TestCase {
}
return true;
- }));
- $this->proxyMapper->expects($this->at(2))
- ->method('insert')
- ->with($this->callback(function ($proxy) {
+ },
+ function ($proxy) {
/** @var Proxy $proxy */
if ($proxy->getOwnerId() !== $this->principalPrefix . '/backend1-res1') {
return false;
@@ -264,22 +226,28 @@ abstract class AbstractPrincipalBackendTest extends TestCase {
}
return true;
- }));
+ }
+ ];
+ $this->proxyMapper->expects($this->exactly(2))
+ ->method('insert')
+ ->willReturnCallback(function ($proxy) use (&$calls) {
+ $expected = array_shift($calls);
+ $this->assertTrue($expected($proxy));
+ return $proxy;
+ });
$this->principalBackend->setGroupMemberSet($this->principalPrefix . '/backend1-res1/calendar-proxy-write', [$this->principalPrefix . '/backend1-res2', $this->principalPrefix . '/backend2-res3']);
}
- public function testUpdatePrincipal() {
+ public function testUpdatePrincipal(): void {
$propPatch = $this->createMock(PropPatch::class);
$actual = $this->principalBackend->updatePrincipal($this->principalPrefix . '/foo-bar', $propPatch);
$this->assertEquals(0, $actual);
}
- /**
- * @dataProvider dataSearchPrincipals
- */
- public function testSearchPrincipals($expected, $test) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSearchPrincipals')]
+ public function testSearchPrincipals($expected, $test): void {
$user = $this->createMock(IUser::class);
$this->userSession->expects($this->once())
->method('getUser')
@@ -300,7 +268,7 @@ abstract class AbstractPrincipalBackendTest extends TestCase {
$actual);
}
- public function dataSearchPrincipals() {
+ public static function dataSearchPrincipals(): array {
// data providers are called before we subclass
// this class, $this->principalPrefix is null
// at that point, so we need this hack
@@ -319,7 +287,7 @@ abstract class AbstractPrincipalBackendTest extends TestCase {
];
}
- public function testSearchPrincipalsByMetadataKey() {
+ public function testSearchPrincipalsByMetadataKey(): void {
$user = $this->createMock(IUser::class);
$this->userSession->expects($this->once())
->method('getUser')
@@ -339,7 +307,7 @@ abstract class AbstractPrincipalBackendTest extends TestCase {
], $actual);
}
- public function testSearchPrincipalsByCalendarUserAddressSet() {
+ public function testSearchPrincipalsByCalendarUserAddressSet(): void {
$user = $this->createMock(IUser::class);
$this->userSession->method('getUser')
->with()
@@ -359,7 +327,7 @@ abstract class AbstractPrincipalBackendTest extends TestCase {
$actual);
}
- public function testSearchPrincipalsEmptySearchProperties() {
+ public function testSearchPrincipalsEmptySearchProperties(): void {
$this->userSession->expects($this->never())
->method('getUser');
$this->groupManager->expects($this->never())
@@ -368,7 +336,7 @@ abstract class AbstractPrincipalBackendTest extends TestCase {
$this->principalBackend->searchPrincipals($this->principalPrefix, []);
}
- public function testSearchPrincipalsWrongPrincipalPrefix() {
+ public function testSearchPrincipalsWrongPrincipalPrefix(): void {
$this->userSession->expects($this->never())
->method('getUser');
$this->groupManager->expects($this->never())
@@ -379,7 +347,7 @@ abstract class AbstractPrincipalBackendTest extends TestCase {
]);
}
- public function testFindByUriByEmail() {
+ public function testFindByUriByEmail(): void {
$user = $this->createMock(IUser::class);
$this->userSession->expects($this->once())
->method('getUser')
@@ -394,7 +362,7 @@ abstract class AbstractPrincipalBackendTest extends TestCase {
$this->assertEquals($this->principalPrefix . '/backend1-res1', $actual);
}
- public function testFindByUriByEmailForbiddenResource() {
+ public function testFindByUriByEmailForbiddenResource(): void {
$user = $this->createMock(IUser::class);
$this->userSession->expects($this->once())
->method('getUser')
@@ -409,7 +377,7 @@ abstract class AbstractPrincipalBackendTest extends TestCase {
$this->assertEquals(null, $actual);
}
- public function testFindByUriByEmailNotFound() {
+ public function testFindByUriByEmailNotFound(): void {
$user = $this->createMock(IUser::class);
$this->userSession->expects($this->once())
->method('getUser')
@@ -424,7 +392,7 @@ abstract class AbstractPrincipalBackendTest extends TestCase {
$this->assertEquals(null, $actual);
}
- public function testFindByUriByPrincipal() {
+ public function testFindByUriByPrincipal(): void {
$user = $this->createMock(IUser::class);
$this->userSession->expects($this->once())
->method('getUser')
@@ -439,7 +407,7 @@ abstract class AbstractPrincipalBackendTest extends TestCase {
$this->assertEquals($this->principalPrefix . '/backend3-res6', $actual);
}
- public function testFindByUriByPrincipalForbiddenResource() {
+ public function testFindByUriByPrincipalForbiddenResource(): void {
$user = $this->createMock(IUser::class);
$this->userSession->expects($this->once())
->method('getUser')
@@ -454,7 +422,7 @@ abstract class AbstractPrincipalBackendTest extends TestCase {
$this->assertEquals(null, $actual);
}
- public function testFindByUriByPrincipalNotFound() {
+ public function testFindByUriByPrincipalNotFound(): void {
$user = $this->createMock(IUser::class);
$this->userSession->expects($this->once())
->method('getUser')
@@ -469,7 +437,7 @@ abstract class AbstractPrincipalBackendTest extends TestCase {
$this->assertEquals(null, $actual);
}
- public function testFindByUriByUnknownUri() {
+ public function testFindByUriByUnknownUri(): void {
$user = $this->createMock(IUser::class);
$this->userSession->expects($this->once())
->method('getUser')
diff --git a/apps/dav/tests/unit/CalDAV/ResourceBooking/ResourcePrincipalBackendTest.php b/apps/dav/tests/unit/CalDAV/ResourceBooking/ResourcePrincipalBackendTest.php
index 8b40088b0dc..168e21c3a91 100644
--- a/apps/dav/tests/unit/CalDAV/ResourceBooking/ResourcePrincipalBackendTest.php
+++ b/apps/dav/tests/unit/CalDAV/ResourceBooking/ResourcePrincipalBackendTest.php
@@ -1,32 +1,15 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018, Georg Ehrke
- *
- * @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\Tests\unit\CalDAV\ResourceBooking;
use OCA\DAV\CalDAV\ResourceBooking\ResourcePrincipalBackend;
-class ResourcePrincipalBackendTest extends AbstractPrincipalBackendTest {
+class ResourcePrincipalBackendTest extends AbstractPrincipalBackendTestCase {
protected function setUp(): void {
parent::setUp();
diff --git a/apps/dav/tests/unit/CalDAV/ResourceBooking/RoomPrincipalBackendTest.php b/apps/dav/tests/unit/CalDAV/ResourceBooking/RoomPrincipalBackendTest.php
index 3d4e118b2e8..8a53b0ee25e 100644
--- a/apps/dav/tests/unit/CalDAV/ResourceBooking/RoomPrincipalBackendTest.php
+++ b/apps/dav/tests/unit/CalDAV/ResourceBooking/RoomPrincipalBackendTest.php
@@ -1,32 +1,15 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018, Georg Ehrke
- *
- * @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\Tests\unit\CalDAV\ResourceBooking;
use OCA\DAV\CalDAV\ResourceBooking\RoomPrincipalBackend;
-class RoomPrincipalBackendTest extends AbstractPrincipalBackendTest {
+class RoomPrincipalBackendTest extends AbstractPrincipalBackendTestCase {
protected function setUp(): void {
parent::setUp();
diff --git a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginCharsetTest.php b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginCharsetTest.php
new file mode 100644
index 00000000000..fa52d5319c9
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginCharsetTest.php
@@ -0,0 +1,193 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CalDAV\Schedule;
+
+use OC\L10N\L10N;
+use OC\URLGenerator;
+use OCA\DAV\CalDAV\EventComparisonService;
+use OCA\DAV\CalDAV\Schedule\IMipPlugin;
+use OCA\DAV\CalDAV\Schedule\IMipService;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Defaults;
+use OCP\IAppConfig;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\L10N\IFactory;
+use OCP\Mail\IMailer;
+use OCP\Mail\IMessage;
+use OCP\Mail\Provider\IManager;
+use OCP\Mail\Provider\IMessageSend;
+use OCP\Mail\Provider\IService;
+use OCP\Mail\Provider\Message as MailProviderMessage;
+use OCP\Security\ISecureRandom;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VEvent;
+use Sabre\VObject\ITip\Message;
+use Sabre\VObject\Property\ICalendar\CalAddress;
+use Symfony\Component\Mime\Email;
+use Test\TestCase;
+
+class IMipPluginCharsetTest extends TestCase {
+ // Dependencies
+ private Defaults&MockObject $defaults;
+ private IAppConfig&MockObject $appConfig;
+ private IConfig&MockObject $config;
+ private IDBConnection&MockObject $db;
+ private IFactory $l10nFactory;
+ private IManager&MockObject $mailManager;
+ private IMailer&MockObject $mailer;
+ private ISecureRandom&MockObject $random;
+ private ITimeFactory&MockObject $timeFactory;
+ private IUrlGenerator&MockObject $urlGenerator;
+ private IUserSession&MockObject $userSession;
+ private LoggerInterface $logger;
+
+ // Services
+ private EventComparisonService $eventComparisonService;
+ private IMipPlugin $imipPlugin;
+ private IMipService $imipService;
+
+ // ITip Message
+ private Message $itipMessage;
+
+ protected function setUp(): void {
+ // Used by IMipService and IMipPlugin
+ $today = new \DateTime('2025-06-15 14:30');
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->timeFactory->method('getTime')
+ ->willReturn($today->getTimestamp());
+ $this->timeFactory->method('getDateTime')
+ ->willReturn($today);
+
+ // IMipService
+ $this->urlGenerator = $this->createMock(URLGenerator::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->db = $this->createMock(IDBConnection::class);
+ $this->random = $this->createMock(ISecureRandom::class);
+ $l10n = $this->createMock(L10N::class);
+ $this->l10nFactory = $this->createMock(IFactory::class);
+ $this->l10nFactory->method('findGenericLanguage')
+ ->willReturn('en');
+ $this->l10nFactory->method('findLocale')
+ ->willReturn('en_US');
+ $this->l10nFactory->method('get')
+ ->willReturn($l10n);
+ $this->imipService = new IMipService(
+ $this->urlGenerator,
+ $this->config,
+ $this->db,
+ $this->random,
+ $this->l10nFactory,
+ $this->timeFactory,
+ );
+
+ // EventComparisonService
+ $this->eventComparisonService = new EventComparisonService();
+
+ // IMipPlugin
+ $this->appConfig = $this->createMock(IAppConfig::class);
+ $message = new \OC\Mail\Message(new Email(), false);
+ $this->mailer = $this->createMock(IMailer::class);
+ $this->mailer->method('createMessage')
+ ->willReturn($message);
+ $this->mailer->method('validateMailAddress')
+ ->willReturn(true);
+ $this->logger = new NullLogger();
+ $this->defaults = $this->createMock(Defaults::class);
+ $this->defaults->method('getName')
+ ->willReturn('Instance Name 123');
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('luigi');
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->userSession->method('getUser')
+ ->willReturn($user);
+ $this->mailManager = $this->createMock(IManager::class);
+ $this->imipPlugin = new IMipPlugin(
+ $this->appConfig,
+ $this->mailer,
+ $this->logger,
+ $this->timeFactory,
+ $this->defaults,
+ $this->userSession,
+ $this->imipService,
+ $this->eventComparisonService,
+ $this->mailManager,
+ );
+
+ // ITipMessage
+ $calendar = new VCalendar();
+ $event = new VEvent($calendar, 'VEVENT');
+ $event->UID = 'uid-1234';
+ $event->SEQUENCE = 1;
+ $event->SUMMARY = 'Lunch';
+ $event->DTSTART = new \DateTime('2025-06-20 12:30:00');
+ $organizer = new CalAddress($calendar, 'ORGANIZER', 'mailto:luigi@example.org');
+ $event->add($organizer);
+ $attendee = new CalAddress($calendar, 'ATTENDEE', 'mailto:jose@example.org', ['RSVP' => 'TRUE', 'CN' => 'José']);
+ $event->add($attendee);
+ $calendar->add($event);
+ $this->itipMessage = new Message();
+ $this->itipMessage->method = 'REQUEST';
+ $this->itipMessage->message = $calendar;
+ $this->itipMessage->sender = 'mailto:luigi@example.org';
+ $this->itipMessage->senderName = 'Luigi';
+ $this->itipMessage->recipient = 'mailto:' . 'jose@example.org';
+ }
+
+ public function testCharsetMailer(): void {
+ // Arrange
+ $symfonyEmail = null;
+ $this->mailer->expects(self::once())
+ ->method('send')
+ ->willReturnCallback(function (IMessage $message) use (&$symfonyEmail): array {
+ if ($message instanceof \OC\Mail\Message) {
+ $symfonyEmail = $message->getSymfonyEmail();
+ }
+ return [];
+ });
+
+ // Act
+ $this->imipPlugin->schedule($this->itipMessage);
+
+ // Assert
+ $this->assertNotNull($symfonyEmail);
+ $body = $symfonyEmail->getBody()->toString();
+ $this->assertStringContainsString('Content-Type: text/calendar; method=REQUEST; charset="utf-8"; name=event.ics', $body);
+ }
+
+ public function testCharsetMailProvider(): void {
+ // Arrange
+ $this->appConfig->method('getValueBool')
+ ->with('core', 'mail_providers_enabled', true)
+ ->willReturn(true);
+ $mailMessage = new MailProviderMessage();
+ $mailService = $this->createStubForIntersectionOfInterfaces([IService::class, IMessageSend::class]);
+ $mailService->method('initiateMessage')
+ ->willReturn($mailMessage);
+ $mailService->expects(self::once())
+ ->method('sendMessage');
+ $this->mailManager->method('findServiceByAddress')
+ ->willReturn($mailService);
+
+ // Act
+ $this->imipPlugin->schedule($this->itipMessage);
+
+ // Assert
+ $attachments = $mailMessage->getAttachments();
+ $this->assertCount(1, $attachments);
+ $this->assertStringContainsString('text/calendar; method=REQUEST; charset="utf-8"; name=event.ics', $attachments[0]->getType());
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php
index a81aac52fbb..8e71bfa6edf 100644
--- a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php
+++ b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php
@@ -1,84 +1,59 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright Copyright (c) 2017, Georg Ehrke
- *
- * @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 Morris Jobke <hey@morrisjobke.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: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Tests\unit\CalDAV\Schedule;
+use OCA\DAV\CalDAV\EventComparisonService;
use OCA\DAV\CalDAV\Schedule\IMipPlugin;
+use OCA\DAV\CalDAV\Schedule\IMipService;
use OCP\AppFramework\Utility\ITimeFactory;
-use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Defaults;
-use OCP\IConfig;
-use OCP\IDBConnection;
-use OCP\IL10N;
-use OCP\ILogger;
-use OCP\IURLGenerator;
+use OCP\IAppConfig;
use OCP\IUser;
-use OCP\IUserManager;
-use OCP\L10N\IFactory;
+use OCP\IUserSession;
use OCP\Mail\IAttachment;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMailer;
use OCP\Mail\IMessage;
-use OCP\Security\ISecureRandom;
+use OCP\Mail\Provider\IManager as IMailManager;
+use OCP\Mail\Provider\IMessage as IMailMessageNew;
+use OCP\Mail\Provider\IMessageSend as IMailMessageSend;
+use OCP\Mail\Provider\IService as IMailService;
use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VEvent;
use Sabre\VObject\ITip\Message;
use Test\TestCase;
+use function array_merge;
-class IMipPluginTest extends TestCase {
-
- /** @var IMessage|MockObject */
- private $mailMessage;
-
- /** @var IMailer|MockObject */
- private $mailer;
-
- /** @var IEMailTemplate|MockObject */
- private $emailTemplate;
-
- /** @var IAttachment|MockObject */
- private $emailAttachment;
-
- /** @var ITimeFactory|MockObject */
- private $timeFactory;
-
- /** @var IConfig|MockObject */
- private $config;
-
- /** @var IUserManager|MockObject */
- private $userManager;
-
- /** @var IQueryBuilder|MockObject */
- private $queryBuilder;
+interface IMailServiceMock extends IMailService, IMailMessageSend {
+ // workaround for creating mock class with multiple interfaces
+ // TODO: remove after phpUnit 10 is supported.
+}
- /** @var IMipPlugin */
- private $plugin;
+class IMipPluginTest extends TestCase {
+ private IMessage&MockObject $mailMessage;
+ private IMailer&MockObject $mailer;
+ private IEMailTemplate&MockObject $emailTemplate;
+ private IAttachment&MockObject $emailAttachment;
+ private ITimeFactory&MockObject $timeFactory;
+ private IAppConfig&MockObject $config;
+ private IUserSession&MockObject $userSession;
+ private IUser&MockObject $user;
+ private IMipPlugin $plugin;
+ private IMipService&MockObject $service;
+ private Defaults&MockObject $defaults;
+ private LoggerInterface&MockObject $logger;
+ private EventComparisonService&MockObject $eventComparisonService;
+ private IMailManager&MockObject $mailManager;
+ private IMailServiceMock&MockObject $mailService;
+ private IMailMessageNew&MockObject $mailMessageNew;
protected function setUp(): void {
$this->mailMessage = $this->createMock(IMessage::class);
@@ -86,7 +61,7 @@ class IMipPluginTest extends TestCase {
$this->mailMessage->method('setReplyTo')->willReturn($this->mailMessage);
$this->mailMessage->method('setTo')->willReturn($this->mailMessage);
- $this->mailer = $this->getMockBuilder(IMailer::class)->disableOriginalConstructor()->getMock();
+ $this->mailer = $this->createMock(IMailer::class);
$this->mailer->method('createMessage')->willReturn($this->mailMessage);
$this->emailTemplate = $this->createMock(IEMailTemplate::class);
@@ -95,249 +70,1011 @@ class IMipPluginTest extends TestCase {
$this->emailAttachment = $this->createMock(IAttachment::class);
$this->mailer->method('createAttachment')->willReturn($this->emailAttachment);
- /** @var ILogger|MockObject $logger */
- $logger = $this->getMockBuilder(ILogger::class)->disableOriginalConstructor()->getMock();
+ $this->logger = $this->createMock(LoggerInterface::class);
- $this->timeFactory = $this->getMockBuilder(ITimeFactory::class)->disableOriginalConstructor()->getMock();
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
$this->timeFactory->method('getTime')->willReturn(1496912528); // 2017-01-01
- $this->config = $this->createMock(IConfig::class);
+ $this->config = $this->createMock(IAppConfig::class);
- $this->userManager = $this->createMock(IUserManager::class);
+ $this->user = $this->createMock(IUser::class);
- $l10n = $this->createMock(IL10N::class);
- $l10n->method('t')
- ->willReturnCallback(function ($text, $parameters = []) {
- return vsprintf($text, $parameters);
- });
- $l10nFactory = $this->createMock(IFactory::class);
- $l10nFactory->method('get')->willReturn($l10n);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->userSession->method('getUser')
+ ->willReturn($this->user);
- $urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->defaults = $this->createMock(Defaults::class);
+ $this->defaults->method('getName')
+ ->willReturn('Instance Name 123');
- $this->queryBuilder = $this->createMock(IQueryBuilder::class);
- $db = $this->createMock(IDBConnection::class);
- $db->method('getQueryBuilder')
- ->with()
- ->willReturn($this->queryBuilder);
+ $this->service = $this->createMock(IMipService::class);
- $random = $this->createMock(ISecureRandom::class);
- $random->method('generate')
- ->with(60, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')
- ->willReturn('random_token');
+ $this->eventComparisonService = $this->createMock(EventComparisonService::class);
- $defaults = $this->createMock(Defaults::class);
- $defaults->method('getName')
- ->willReturn('Instance Name 123');
+ $this->mailManager = $this->createMock(IMailManager::class);
- $this->plugin = new IMipPlugin($this->config, $this->mailer, $logger, $this->timeFactory, $l10nFactory, $urlGenerator, $defaults, $random, $db, $this->userManager, 'user123');
- }
+ $this->mailService = $this->createMock(IMailServiceMock::class);
- public function testDelivery() {
- $this->config
- ->expects($this->at(1))
- ->method('getAppValue')
- ->with('dav', 'invitation_link_recipients', 'yes')
- ->willReturn('yes');
- $this->mailer->method('validateMailAddress')->willReturn(true);
+ $this->mailMessageNew = $this->createMock(IMailMessageNew::class);
+
+ $this->plugin = new IMipPlugin(
+ $this->config,
+ $this->mailer,
+ $this->logger,
+ $this->timeFactory,
+ $this->defaults,
+ $this->userSession,
+ $this->service,
+ $this->eventComparisonService,
+ $this->mailManager,
+ );
+ }
- $message = $this->_testMessage();
- $this->_expectSend();
+ public function testDeliveryNoSignificantChange(): void {
+ $message = new Message();
+ $message->method = 'REQUEST';
+ $message->message = new VCalendar();
+ $message->message->add('VEVENT', array_merge([
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 0,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ], []));
+ $message->message->VEVENT->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $message->message->VEVENT->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE']);
+ $message->sender = 'mailto:gandalf@wiz.ard';
+ $message->senderName = 'Mr. Wizard';
+ $message->recipient = 'mailto:' . 'frodo@hobb.it';
+ $message->significantChange = false;
$this->plugin->schedule($message);
- $this->assertEquals('1.1', $message->getScheduleStatus());
+ $this->assertEquals('1.0', $message->getScheduleStatus());
}
- public function testFailedDelivery() {
- $this->config
- ->expects($this->at(1))
- ->method('getAppValue')
+ public function testParsingSingle(): void {
+ $message = new Message();
+ $message->method = 'REQUEST';
+ $newVCalendar = new VCalendar();
+ $newVevent = new VEvent($newVCalendar, 'one', array_merge([
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 1,
+ 'SUMMARY' => 'Fellowship meeting without (!) Boromir',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ], []));
+ $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $message->message = $newVCalendar;
+ $message->sender = 'mailto:gandalf@wiz.ard';
+ $message->senderName = 'Mr. Wizard';
+ $message->recipient = 'mailto:' . 'frodo@hobb.it';
+ // save the old copy in the plugin
+ $oldVCalendar = new VCalendar();
+ $oldVEvent = new VEvent($oldVCalendar, 'one', [
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 0,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ]);
+ $oldVEvent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $oldVEvent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $oldVEvent->add('ATTENDEE', 'mailto:' . 'boromir@tra.it.or', ['RSVP' => 'TRUE']);
+ $oldVCalendar->add($oldVEvent);
+ $data = ['invitee_name' => 'Mr. Wizard',
+ 'meeting_title' => 'Fellowship meeting without (!) Boromir',
+ 'attendee_name' => 'frodo@hobb.it'
+ ];
+ $attendees = $newVevent->select('ATTENDEE');
+ $atnd = '';
+ foreach ($attendees as $attendee) {
+ if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
+ $atnd = $attendee;
+ }
+ }
+ $this->plugin->setVCalendar($oldVCalendar);
+ $this->service->expects(self::once())
+ ->method('getLastOccurrence')
+ ->willReturn(1496912700);
+ $this->mailer->expects(self::once())
+ ->method('validateMailAddress')
+ ->with('frodo@hobb.it')
+ ->willReturn(true);
+ $this->eventComparisonService->expects(self::once())
+ ->method('findModified')
+ ->willReturn(['new' => [$newVevent], 'old' => [$oldVEvent]]);
+ $this->service->expects(self::once())
+ ->method('getCurrentAttendee')
+ ->with($message)
+ ->willReturn($atnd);
+ $this->service->expects(self::once())
+ ->method('isRoomOrResource')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('isCircle')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('buildBodyData')
+ ->with($newVevent, $oldVEvent)
+ ->willReturn($data);
+ $this->user->expects(self::any())
+ ->method('getUID')
+ ->willReturn('user1');
+ $this->user->expects(self::any())
+ ->method('getDisplayName')
+ ->willReturn('Mr. Wizard');
+ $this->userSession->expects(self::any())
+ ->method('getUser')
+ ->willReturn($this->user);
+ $this->service->expects(self::once())
+ ->method('getFrom');
+ $this->service->expects(self::once())
+ ->method('addSubjectAndHeading')
+ ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting without (!) Boromir', true);
+ $this->service->expects(self::once())
+ ->method('addBulletList')
+ ->with($this->emailTemplate, $newVevent, $data);
+ $this->service->expects(self::once())
+ ->method('getAttendeeRsvpOrReqForParticipant')
+ ->willReturn(true);
+ $this->config->expects(self::once())
+ ->method('getValueString')
->with('dav', 'invitation_link_recipients', 'yes')
->willReturn('yes');
- $this->mailer->method('validateMailAddress')->willReturn(true);
-
- $message = $this->_testMessage();
- $this->mailer
+ $this->service->expects(self::once())
+ ->method('createInvitationToken')
+ ->with($message, $newVevent, 1496912700)
+ ->willReturn('token');
+ $this->service->expects(self::once())
+ ->method('addResponseButtons')
+ ->with($this->emailTemplate, 'token');
+ $this->service->expects(self::once())
+ ->method('addMoreOptionsButton')
+ ->with($this->emailTemplate, 'token');
+ $this->mailer->expects(self::once())
->method('send')
- ->willThrowException(new \Exception());
- $this->_expectSend();
+ ->willReturn([]);
$this->plugin->schedule($message);
- $this->assertEquals('5.0', $message->getScheduleStatus());
+ $this->assertEquals('1.1', $message->getScheduleStatus());
}
- public function testInvalidEmailDelivery() {
- $this->mailer->method('validateMailAddress')->willReturn(false);
+ public function testAttendeeIsResource(): void {
+ $message = new Message();
+ $message->method = 'REQUEST';
+ $newVCalendar = new VCalendar();
+ $newVevent = new VEvent($newVCalendar, 'one', array_merge([
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 1,
+ 'SUMMARY' => 'Fellowship meeting without (!) Boromir',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ], []));
+ $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $newVevent->add('ATTENDEE', 'mailto:' . 'the-shire@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'The Shire', 'CUTYPE' => 'ROOM']);
+ $message->message = $newVCalendar;
+ $message->sender = 'mailto:gandalf@wiz.ard';
+ $message->senderName = 'Mr. Wizard';
+ $message->recipient = 'mailto:' . 'the-shire@hobb.it';
+ // save the old copy in the plugin
+ $oldVCalendar = new VCalendar();
+ $oldVEvent = new VEvent($oldVCalendar, 'one', [
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 0,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ]);
+ $oldVEvent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $oldVEvent->add('ATTENDEE', 'mailto:' . 'the-shire@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'The Shire', 'CUTYPE' => 'ROOM']);
+ $oldVEvent->add('ATTENDEE', 'mailto:' . 'boromir@tra.it.or', ['RSVP' => 'TRUE']);
+ $oldVCalendar->add($oldVEvent);
+ $data = ['invitee_name' => 'Mr. Wizard',
+ 'meeting_title' => 'Fellowship meeting without (!) Boromir',
+ 'attendee_name' => 'frodo@hobb.it'
+ ];
+ $attendees = $newVevent->select('ATTENDEE');
+ $room = '';
+ foreach ($attendees as $attendee) {
+ if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
+ $room = $attendee;
+ }
+ }
+ $this->plugin->setVCalendar($oldVCalendar);
+ $this->service->expects(self::once())
+ ->method('getLastOccurrence')
+ ->willReturn(1496912700);
+ $this->mailer->expects(self::once())
+ ->method('validateMailAddress')
+ ->with('the-shire@hobb.it')
+ ->willReturn(true);
+ $this->eventComparisonService->expects(self::once())
+ ->method('findModified')
+ ->willReturn(['new' => [$newVevent], 'old' => [$oldVEvent]]);
+ $this->service->expects(self::once())
+ ->method('getCurrentAttendee')
+ ->with($message)
+ ->willReturn($room);
+ $this->service->expects(self::once())
+ ->method('isRoomOrResource')
+ ->with($room)
+ ->willReturn(true);
+ $this->service->expects(self::never())
+ ->method('isCircle');
+ $this->service->expects(self::never())
+ ->method('buildBodyData');
+ $this->user->expects(self::any())
+ ->method('getUID')
+ ->willReturn('user1');
+ $this->user->expects(self::any())
+ ->method('getDisplayName')
+ ->willReturn('Mr. Wizard');
+ $this->userSession->expects(self::any())
+ ->method('getUser')
+ ->willReturn($this->user);
+ $this->service->expects(self::never())
+ ->method('getFrom');
+ $this->service->expects(self::never())
+ ->method('addSubjectAndHeading');
+ $this->service->expects(self::never())
+ ->method('addBulletList');
+ $this->service->expects(self::never())
+ ->method('getAttendeeRsvpOrReqForParticipant');
+ $this->config->expects(self::never())
+ ->method('getValueString');
+ $this->service->expects(self::never())
+ ->method('createInvitationToken');
+ $this->service->expects(self::never())
+ ->method('addResponseButtons');
+ $this->service->expects(self::never())
+ ->method('addMoreOptionsButton');
+ $this->mailer->expects(self::never())
+ ->method('send');
+ $this->plugin->schedule($message);
+ $this->assertEquals('1.0', $message->getScheduleStatus());
+ }
- $message = $this->_testMessage();
+ public function testAttendeeIsCircle(): void {
+ $message = new Message();
+ $message->method = 'REQUEST';
+ $newVCalendar = new VCalendar();
+ $newVevent = new VEvent($newVCalendar, 'one', array_merge([
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 1,
+ 'SUMMARY' => 'Fellowship meeting without (!) Boromir',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ], []));
+ $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $newVevent->add('ATTENDEE', 'mailto:' . 'circle+82utEV1Fle8wvxndZLK5TVAPtxj8IIe@middle.earth', ['RSVP' => 'TRUE', 'CN' => 'The Fellowship', 'CUTYPE' => 'GROUP']);
+ $newVevent->add('ATTENDEE', 'mailto:' . 'boromir@tra.it.or', ['RSVP' => 'TRUE', 'MEMBER' => 'circle+82utEV1Fle8wvxndZLK5TVAPtxj8IIe@middle.earth']);
+ $message->message = $newVCalendar;
+ $message->sender = 'mailto:gandalf@wiz.ard';
+ $message->senderName = 'Mr. Wizard';
+ $message->recipient = 'mailto:' . 'circle+82utEV1Fle8wvxndZLK5TVAPtxj8IIe@middle.earth';
+ $attendees = $newVevent->select('ATTENDEE');
+ $circle = '';
+ foreach ($attendees as $attendee) {
+ if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
+ $circle = $attendee;
+ }
+ }
+ $this->assertNotEmpty($circle, 'Failed to find attendee belonging to the circle');
+ $this->service->expects(self::once())
+ ->method('getLastOccurrence')
+ ->willReturn(1496912700);
+ $this->mailer->expects(self::once())
+ ->method('validateMailAddress')
+ ->with('circle+82utEV1Fle8wvxndZLK5TVAPtxj8IIe@middle.earth')
+ ->willReturn(true);
+ $this->eventComparisonService->expects(self::once())
+ ->method('findModified')
+ ->willReturn(['new' => [$newVevent], 'old' => null]);
+ $this->service->expects(self::once())
+ ->method('getCurrentAttendee')
+ ->with($message)
+ ->willReturn($circle);
+ $this->service->expects(self::once())
+ ->method('isRoomOrResource')
+ ->with($circle)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('isCircle')
+ ->with($circle)
+ ->willReturn(true);
+ $this->service->expects(self::never())
+ ->method('buildBodyData');
+ $this->user->expects(self::any())
+ ->method('getUID')
+ ->willReturn('user1');
+ $this->user->expects(self::any())
+ ->method('getDisplayName')
+ ->willReturn('Mr. Wizard');
+ $this->userSession->expects(self::any())
+ ->method('getUser')
+ ->willReturn($this->user);
+ $this->service->expects(self::never())
+ ->method('getFrom');
+ $this->service->expects(self::never())
+ ->method('addSubjectAndHeading');
+ $this->service->expects(self::never())
+ ->method('addBulletList');
+ $this->service->expects(self::never())
+ ->method('getAttendeeRsvpOrReqForParticipant');
+ $this->config->expects(self::never())
+ ->method('getValueString');
+ $this->service->expects(self::never())
+ ->method('createInvitationToken');
+ $this->service->expects(self::never())
+ ->method('addResponseButtons');
+ $this->service->expects(self::never())
+ ->method('addMoreOptionsButton');
+ $this->mailer->expects(self::never())
+ ->method('send');
$this->plugin->schedule($message);
- $this->assertEquals('5.0', $message->getScheduleStatus());
+ $this->assertEquals('1.0', $message->getScheduleStatus());
}
- public function testDeliveryWithNoCommonName() {
- $this->config
- ->expects($this->at(1))
- ->method('getAppValue')
+ public function testParsingRecurrence(): void {
+ $message = new Message();
+ $message->method = 'REQUEST';
+ $newVCalendar = new VCalendar();
+ $newVevent = new VEvent($newVCalendar, 'one', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z'
+ ]);
+ $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $newvEvent2 = new VEvent($newVCalendar, 'two', [
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 1,
+ 'SUMMARY' => 'Elevenses',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'RECURRENCE-ID' => new \DateTime('2016-01-01 00:00:00')
+ ]);
+ $newvEvent2->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $newvEvent2->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $message->message = $newVCalendar;
+ $message->sender = 'mailto:gandalf@wiz.ard';
+ $message->recipient = 'mailto:' . 'frodo@hobb.it';
+ // save the old copy in the plugin
+ $oldVCalendar = new VCalendar();
+ $oldVEvent = new VEvent($oldVCalendar, 'one', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z'
+ ]);
+ $oldVEvent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $oldVEvent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $data = ['invitee_name' => 'Mr. Wizard',
+ 'meeting_title' => 'Elevenses',
+ 'attendee_name' => 'frodo@hobb.it'
+ ];
+ $attendees = $newVevent->select('ATTENDEE');
+ $atnd = '';
+ foreach ($attendees as $attendee) {
+ if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
+ $atnd = $attendee;
+ }
+ }
+ $this->plugin->setVCalendar($oldVCalendar);
+ $this->service->expects(self::once())
+ ->method('getLastOccurrence')
+ ->willReturn(1496912700);
+ $this->mailer->expects(self::once())
+ ->method('validateMailAddress')
+ ->with('frodo@hobb.it')
+ ->willReturn(true);
+ $this->eventComparisonService->expects(self::once())
+ ->method('findModified')
+ ->willReturn(['old' => [] ,'new' => [$newVevent]]);
+ $this->service->expects(self::once())
+ ->method('getCurrentAttendee')
+ ->with($message)
+ ->willReturn($atnd);
+ $this->service->expects(self::once())
+ ->method('isRoomOrResource')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('isCircle')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('buildBodyData')
+ ->with($newVevent, null)
+ ->willReturn($data);
+ $this->user->expects(self::any())
+ ->method('getUID')
+ ->willReturn('user1');
+ $this->user->expects(self::any())
+ ->method('getDisplayName')
+ ->willReturn('Mr. Wizard');
+ $this->userSession->expects(self::any())
+ ->method('getUser')
+ ->willReturn($this->user);
+ $this->service->expects(self::once())
+ ->method('getFrom');
+ $this->service->expects(self::once())
+ ->method('addSubjectAndHeading')
+ ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Elevenses', false);
+ $this->service->expects(self::once())
+ ->method('addBulletList')
+ ->with($this->emailTemplate, $newVevent, $data);
+ $this->service->expects(self::once())
+ ->method('getAttendeeRsvpOrReqForParticipant')
+ ->willReturn(true);
+ $this->config->expects(self::once())
+ ->method('getValueString')
->with('dav', 'invitation_link_recipients', 'yes')
->willReturn('yes');
- $this->mailer->method('validateMailAddress')->willReturn(true);
-
- $message = $this->_testMessage();
- $message->senderName = null;
-
- $user = $this->createMock(IUser::class);
- $user->method('getDisplayName')->willReturn('Mr. Wizard');
-
- $this->userManager->expects($this->once())
- ->method('get')
- ->with('user123')
- ->willReturn($user);
-
- $this->_expectSend();
+ $this->service->expects(self::once())
+ ->method('createInvitationToken')
+ ->with($message, $newVevent, 1496912700)
+ ->willReturn('token');
+ $this->service->expects(self::once())
+ ->method('addResponseButtons')
+ ->with($this->emailTemplate, 'token');
+ $this->service->expects(self::once())
+ ->method('addMoreOptionsButton')
+ ->with($this->emailTemplate, 'token');
+ $this->mailer->expects(self::once())
+ ->method('send')
+ ->willReturn([]);
$this->plugin->schedule($message);
$this->assertEquals('1.1', $message->getScheduleStatus());
}
- /**
- * @dataProvider dataNoMessageSendForPastEvents
- */
- public function testNoMessageSendForPastEvents(array $veventParams, bool $expectsMail) {
- $this->config
- ->method('getAppValue')
- ->willReturn('yes');
- $this->mailer->method('validateMailAddress')->willReturn(true);
-
- $message = $this->_testMessage($veventParams);
+ public function testEmailValidationFailed(): void {
+ $message = new Message();
+ $message->method = 'REQUEST';
+ $message->message = new VCalendar();
+ $message->message->add('VEVENT', array_merge([
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 0,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ], []));
+ $message->message->VEVENT->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $message->message->VEVENT->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE']);
+ $message->sender = 'mailto:gandalf@wiz.ard';
+ $message->senderName = 'Mr. Wizard';
+ $message->recipient = 'mailto:' . 'frodo@hobb.it';
- $this->_expectSend('frodo@hobb.it', $expectsMail, $expectsMail);
+ $this->service->expects(self::once())
+ ->method('getLastOccurrence')
+ ->willReturn(1496912700);
+ $this->mailer->expects(self::once())
+ ->method('validateMailAddress')
+ ->with('frodo@hobb.it')
+ ->willReturn(false);
$this->plugin->schedule($message);
-
- if ($expectsMail) {
- $this->assertEquals('1.1', $message->getScheduleStatus());
- } else {
- $this->assertEquals(false, $message->getScheduleStatus());
- }
+ $this->assertEquals('5.0', $message->getScheduleStatus());
}
- public function dataNoMessageSendForPastEvents() {
- return [
- [['DTSTART' => new \DateTime('2017-01-01 00:00:00')], false],
- [['DTSTART' => new \DateTime('2017-01-01 00:00:00'), 'DTEND' => new \DateTime('2017-01-01 00:00:00')], false],
- [['DTSTART' => new \DateTime('2017-01-01 00:00:00'), 'DTEND' => new \DateTime('2017-12-31 00:00:00')], true],
- [['DTSTART' => new \DateTime('2017-01-01 00:00:00'), 'DURATION' => 'P1D'], false],
- [['DTSTART' => new \DateTime('2017-01-01 00:00:00'), 'DURATION' => 'P52W'], true],
- [['DTSTART' => new \DateTime('2017-01-01 00:00:00'), 'DTEND' => new \DateTime('2017-01-01 00:00:00'), 'RRULE' => 'FREQ=WEEKLY'], true],
- [['DTSTART' => new \DateTime('2017-01-01 00:00:00'), 'DTEND' => new \DateTime('2017-01-01 00:00:00'), 'RRULE' => 'FREQ=WEEKLY;COUNT=3'], false],
- [['DTSTART' => new \DateTime('2017-01-01 00:00:00'), 'DTEND' => new \DateTime('2017-01-01 00:00:00'), 'RRULE' => 'FREQ=WEEKLY;UNTIL=20170301T000000Z'], false],
- [['DTSTART' => new \DateTime('2017-01-01 00:00:00'), 'DTEND' => new \DateTime('2017-01-01 00:00:00'), 'RRULE' => 'FREQ=WEEKLY;COUNT=33'], true],
- [['DTSTART' => new \DateTime('2017-01-01 00:00:00'), 'DTEND' => new \DateTime('2017-01-01 00:00:00'), 'RRULE' => 'FREQ=WEEKLY;UNTIL=20171001T000000Z'], true],
+ public function testFailedDelivery(): void {
+ $message = new Message();
+ $message->method = 'REQUEST';
+ $newVcalendar = new VCalendar();
+ $newVevent = new VEvent($newVcalendar, 'one', array_merge([
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 1,
+ 'SUMMARY' => 'Fellowship meeting without (!) Boromir',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ], []));
+ $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $message->message = $newVcalendar;
+ $message->sender = 'mailto:gandalf@wiz.ard';
+ $message->senderName = 'Mr. Wizard';
+ $message->recipient = 'mailto:' . 'frodo@hobb.it';
+ // save the old copy in the plugin
+ $oldVcalendar = new VCalendar();
+ $oldVevent = new VEvent($oldVcalendar, 'one', [
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 0,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ]);
+ $oldVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $oldVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $oldVevent->add('ATTENDEE', 'mailto:' . 'boromir@tra.it.or', ['RSVP' => 'TRUE']);
+ $oldVcalendar->add($oldVevent);
+ $data = ['invitee_name' => 'Mr. Wizard',
+ 'meeting_title' => 'Fellowship meeting without (!) Boromir',
+ 'attendee_name' => 'frodo@hobb.it'
];
+ $attendees = $newVevent->select('ATTENDEE');
+ $atnd = '';
+ foreach ($attendees as $attendee) {
+ if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
+ $atnd = $attendee;
+ }
+ }
+ $this->plugin->setVCalendar($oldVcalendar);
+ $this->service->expects(self::once())
+ ->method('getLastOccurrence')
+ ->willReturn(1496912700);
+ $this->mailer->expects(self::once())
+ ->method('validateMailAddress')
+ ->with('frodo@hobb.it')
+ ->willReturn(true);
+ $this->eventComparisonService->expects(self::once())
+ ->method('findModified')
+ ->willReturn(['old' => [] ,'new' => [$newVevent]]);
+ $this->service->expects(self::once())
+ ->method('getCurrentAttendee')
+ ->with($message)
+ ->willReturn($atnd);
+ $this->service->expects(self::once())
+ ->method('isRoomOrResource')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('isCircle')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('buildBodyData')
+ ->with($newVevent, null)
+ ->willReturn($data);
+ $this->user->expects(self::any())
+ ->method('getUID')
+ ->willReturn('user1');
+ $this->user->expects(self::any())
+ ->method('getDisplayName')
+ ->willReturn('Mr. Wizard');
+ $this->userSession->expects(self::any())
+ ->method('getUser')
+ ->willReturn($this->user);
+ $this->service->expects(self::once())
+ ->method('getFrom');
+ $this->service->expects(self::once())
+ ->method('addSubjectAndHeading')
+ ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting without (!) Boromir', false);
+ $this->service->expects(self::once())
+ ->method('addBulletList')
+ ->with($this->emailTemplate, $newVevent, $data);
+ $this->service->expects(self::once())
+ ->method('getAttendeeRsvpOrReqForParticipant')
+ ->willReturn(true);
+ $this->config->expects(self::once())
+ ->method('getValueString')
+ ->with('dav', 'invitation_link_recipients', 'yes')
+ ->willReturn('yes');
+ $this->service->expects(self::once())
+ ->method('createInvitationToken')
+ ->with($message, $newVevent, 1496912700)
+ ->willReturn('token');
+ $this->service->expects(self::once())
+ ->method('addResponseButtons')
+ ->with($this->emailTemplate, 'token');
+ $this->service->expects(self::once())
+ ->method('addMoreOptionsButton')
+ ->with($this->emailTemplate, 'token');
+ $this->mailer->expects(self::once())
+ ->method('send')
+ ->willReturn([]);
+ $this->mailer
+ ->method('send')
+ ->willThrowException(new \Exception());
+ $this->logger->expects(self::once())
+ ->method('error');
+ $this->plugin->schedule($message);
+ $this->assertEquals('5.0', $message->getScheduleStatus());
}
- /**
- * @dataProvider dataIncludeResponseButtons
- */
- public function testIncludeResponseButtons(string $config_setting, string $recipient, bool $has_buttons) {
- $message = $this->_testMessage([],$recipient);
- $this->mailer->method('validateMailAddress')->willReturn(true);
-
- $this->_expectSend($recipient, true, $has_buttons);
- $this->config
- ->expects($this->at(1))
- ->method('getAppValue')
+ public function testMailProviderSend(): void {
+ // construct iTip message with event and attendees
+ $message = new Message();
+ $message->method = 'REQUEST';
+ $calendar = new VCalendar();
+ $event = new VEvent($calendar, 'one', array_merge([
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 1,
+ 'SUMMARY' => 'Fellowship meeting without (!) Boromir',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ], []));
+ $event->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $event->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $message->message = $calendar;
+ $message->sender = 'mailto:gandalf@wiz.ard';
+ $message->senderName = 'Mr. Wizard';
+ $message->recipient = 'mailto:' . 'frodo@hobb.it';
+ // construct
+ foreach ($event->select('ATTENDEE') as $entry) {
+ if (strcasecmp($entry->getValue(), $message->recipient) === 0) {
+ $attendee = $entry;
+ }
+ }
+ // construct body data return
+ $data = ['invitee_name' => 'Mr. Wizard',
+ 'meeting_title' => 'Fellowship meeting without (!) Boromir',
+ 'attendee_name' => 'frodo@hobb.it'
+ ];
+ // construct system config mock returns
+ $this->config->expects(self::once())
+ ->method('getValueString')
->with('dav', 'invitation_link_recipients', 'yes')
- ->willReturn($config_setting);
+ ->willReturn('yes');
+ // construct user mock returns
+ $this->user->expects(self::any())
+ ->method('getUID')
+ ->willReturn('user1');
+ $this->user->expects(self::any())
+ ->method('getDisplayName')
+ ->willReturn('Mr. Wizard');
+ // construct user session mock returns
+ $this->userSession->expects(self::any())
+ ->method('getUser')
+ ->willReturn($this->user);
+ // construct service mock returns
+ $this->service->expects(self::once())
+ ->method('getLastOccurrence')
+ ->willReturn(1496912700);
+ $this->service->expects(self::once())
+ ->method('getCurrentAttendee')
+ ->with($message)
+ ->willReturn($attendee);
+ $this->service->expects(self::once())
+ ->method('isRoomOrResource')
+ ->with($attendee)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('isCircle')
+ ->with($attendee)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('buildBodyData')
+ ->with($event, null)
+ ->willReturn($data);
+ $this->service->expects(self::once())
+ ->method('getFrom');
+ $this->service->expects(self::once())
+ ->method('addSubjectAndHeading')
+ ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting without (!) Boromir', false);
+ $this->service->expects(self::once())
+ ->method('addBulletList')
+ ->with($this->emailTemplate, $event, $data);
+ $this->service->expects(self::once())
+ ->method('getAttendeeRsvpOrReqForParticipant')
+ ->willReturn(true);
+ $this->service->expects(self::once())
+ ->method('createInvitationToken')
+ ->with($message, $event, 1496912700)
+ ->willReturn('token');
+ $this->service->expects(self::once())
+ ->method('addResponseButtons')
+ ->with($this->emailTemplate, 'token');
+ $this->service->expects(self::once())
+ ->method('addMoreOptionsButton')
+ ->with($this->emailTemplate, 'token');
+ $this->eventComparisonService->expects(self::once())
+ ->method('findModified')
+ ->willReturn(['old' => [] ,'new' => [$event]]);
+ // construct mail mock returns
+ $this->mailer->expects(self::once())
+ ->method('validateMailAddress')
+ ->with('frodo@hobb.it')
+ ->willReturn(true);
+ // construct mail provider mock returns
+ $this->mailService
+ ->method('initiateMessage')
+ ->willReturn($this->mailMessageNew);
+ $this->mailService
+ ->method('sendMessage')
+ ->with($this->mailMessageNew);
+ $this->mailManager
+ ->method('findServiceByAddress')
+ ->with('user1', 'gandalf@wiz.ard')
+ ->willReturn($this->mailService);
$this->plugin->schedule($message);
$this->assertEquals('1.1', $message->getScheduleStatus());
}
- public function dataIncludeResponseButtons() {
- return [
- // dav.invitation_link_recipients, recipient, $has_buttons
- [ 'yes', 'joe@internal.com', true],
- [ 'joe@internal.com', 'joe@internal.com', true],
- [ 'internal.com', 'joe@internal.com', true],
- [ 'pete@otherinternal.com,internal.com', 'joe@internal.com', true],
- [ 'no', 'joe@internal.com', false],
- [ 'internal.com', 'joe@external.com', false],
- [ 'jane@otherinternal.com,internal.com', 'joe@otherinternal.com', false],
+ public function testMailProviderDisabled(): void {
+ $message = new Message();
+ $message->method = 'REQUEST';
+ $newVCalendar = new VCalendar();
+ $newVevent = new VEvent($newVCalendar, 'one', array_merge([
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 1,
+ 'SUMMARY' => 'Fellowship meeting without (!) Boromir',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ], []));
+ $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $message->message = $newVCalendar;
+ $message->sender = 'mailto:gandalf@wiz.ard';
+ $message->senderName = 'Mr. Wizard';
+ $message->recipient = 'mailto:' . 'frodo@hobb.it';
+ // save the old copy in the plugin
+ $oldVCalendar = new VCalendar();
+ $oldVEvent = new VEvent($oldVCalendar, 'one', [
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 0,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ]);
+ $oldVEvent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $oldVEvent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $oldVEvent->add('ATTENDEE', 'mailto:' . 'boromir@tra.it.or', ['RSVP' => 'TRUE']);
+ $oldVCalendar->add($oldVEvent);
+ $data = ['invitee_name' => 'Mr. Wizard',
+ 'meeting_title' => 'Fellowship meeting without (!) Boromir',
+ 'attendee_name' => 'frodo@hobb.it'
];
- }
-
- public function testMessageSendWhenEventWithoutName() {
- $this->config
- ->method('getAppValue')
+ $attendees = $newVevent->select('ATTENDEE');
+ $atnd = '';
+ foreach ($attendees as $attendee) {
+ if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
+ $atnd = $attendee;
+ }
+ }
+ $this->plugin->setVCalendar($oldVCalendar);
+ $this->service->expects(self::once())
+ ->method('getLastOccurrence')
+ ->willReturn(1496912700);
+ $this->mailer->expects(self::once())
+ ->method('validateMailAddress')
+ ->with('frodo@hobb.it')
+ ->willReturn(true);
+ $this->eventComparisonService->expects(self::once())
+ ->method('findModified')
+ ->willReturn(['new' => [$newVevent], 'old' => [$oldVEvent]]);
+ $this->service->expects(self::once())
+ ->method('getCurrentAttendee')
+ ->with($message)
+ ->willReturn($atnd);
+ $this->service->expects(self::once())
+ ->method('isRoomOrResource')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('isCircle')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('buildBodyData')
+ ->with($newVevent, $oldVEvent)
+ ->willReturn($data);
+ $this->user->expects(self::any())
+ ->method('getUID')
+ ->willReturn('user1');
+ $this->user->expects(self::any())
+ ->method('getDisplayName')
+ ->willReturn('Mr. Wizard');
+ $this->userSession->expects(self::any())
+ ->method('getUser')
+ ->willReturn($this->user);
+ $this->service->expects(self::once())
+ ->method('getFrom');
+ $this->service->expects(self::once())
+ ->method('addSubjectAndHeading')
+ ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting without (!) Boromir', true);
+ $this->service->expects(self::once())
+ ->method('addBulletList')
+ ->with($this->emailTemplate, $newVevent, $data);
+ $this->service->expects(self::once())
+ ->method('getAttendeeRsvpOrReqForParticipant')
+ ->willReturn(true);
+ $this->config->expects(self::once())
+ ->method('getValueString')
+ ->with('dav', 'invitation_link_recipients', 'yes')
->willReturn('yes');
- $this->mailer->method('validateMailAddress')->willReturn(true);
-
- $message = $this->_testMessage(['SUMMARY' => '']);
- $this->_expectSend('frodo@hobb.it', true, true,'Invitation: Untitled event');
- $this->emailTemplate->expects($this->once())
- ->method('addHeading')
- ->with('Invitation');
+ $this->config->expects(self::once())
+ ->method('getValueBool')
+ ->with('core', 'mail_providers_enabled', true)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('createInvitationToken')
+ ->with($message, $newVevent, 1496912700)
+ ->willReturn('token');
+ $this->service->expects(self::once())
+ ->method('addResponseButtons')
+ ->with($this->emailTemplate, 'token');
+ $this->service->expects(self::once())
+ ->method('addMoreOptionsButton')
+ ->with($this->emailTemplate, 'token');
+ $this->mailer->expects(self::once())
+ ->method('send')
+ ->willReturn([]);
$this->plugin->schedule($message);
$this->assertEquals('1.1', $message->getScheduleStatus());
}
- private function _testMessage(array $attrs = [], string $recipient = 'frodo@hobb.it') {
+ public function testNoOldEvent(): void {
$message = new Message();
$message->method = 'REQUEST';
- $message->message = new VCalendar();
- $message->message->add('VEVENT', array_merge([
+ $newVCalendar = new VCalendar();
+ $newVevent = new VEvent($newVCalendar, 'VEVENT', array_merge([
'UID' => 'uid-1234',
- 'SEQUENCE' => 0,
+ 'SEQUENCE' => 1,
'SUMMARY' => 'Fellowship meeting',
- 'DTSTART' => new \DateTime('2018-01-01 00:00:00')
- ], $attrs));
- $message->message->VEVENT->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
- $message->message->VEVENT->add('ATTENDEE', 'mailto:'.$recipient, [ 'RSVP' => 'TRUE' ]);
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ], []));
+ $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $message->message = $newVCalendar;
$message->sender = 'mailto:gandalf@wiz.ard';
$message->senderName = 'Mr. Wizard';
- $message->recipient = 'mailto:'.$recipient;
- return $message;
- }
-
-
- private function _expectSend(string $recipient = 'frodo@hobb.it', bool $expectSend = true, bool $expectButtons = true, string $subject = 'Invitation: Fellowship meeting') {
-
- // if the event is in the past, we skip out
- if (!$expectSend) {
- $this->mailer
- ->expects($this->never())
- ->method('send');
- return;
+ $message->recipient = 'mailto:' . 'frodo@hobb.it';
+ $data = ['invitee_name' => 'Mr. Wizard',
+ 'meeting_title' => 'Fellowship meeting',
+ 'attendee_name' => 'frodo@hobb.it'
+ ];
+ $attendees = $newVevent->select('ATTENDEE');
+ $atnd = '';
+ foreach ($attendees as $attendee) {
+ if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
+ $atnd = $attendee;
+ }
}
-
- $this->emailTemplate->expects($this->once())
- ->method('setSubject')
- ->with($subject);
- $this->mailMessage->expects($this->once())
- ->method('setTo')
- ->with([$recipient => null]);
- $this->mailMessage->expects($this->once())
- ->method('setReplyTo')
- ->with(['gandalf@wiz.ard' => 'Mr. Wizard']);
- $this->mailMessage->expects($this->once())
- ->method('setFrom')
- ->with(['invitations-noreply@localhost' => 'Mr. Wizard via Instance Name 123']);
+ $this->service->expects(self::once())
+ ->method('getLastOccurrence')
+ ->willReturn(1496912700);
+ $this->mailer->expects(self::once())
+ ->method('validateMailAddress')
+ ->with('frodo@hobb.it')
+ ->willReturn(true);
+ $this->eventComparisonService->expects(self::once())
+ ->method('findModified')
+ ->with($newVCalendar, null)
+ ->willReturn(['old' => [] ,'new' => [$newVevent]]);
+ $this->service->expects(self::once())
+ ->method('getCurrentAttendee')
+ ->with($message)
+ ->willReturn($atnd);
+ $this->service->expects(self::once())
+ ->method('isRoomOrResource')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('isCircle')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('buildBodyData')
+ ->with($newVevent, null)
+ ->willReturn($data);
+ $this->user->expects(self::any())
+ ->method('getUID')
+ ->willReturn('user1');
+ $this->user->expects(self::any())
+ ->method('getDisplayName')
+ ->willReturn('Mr. Wizard');
+ $this->userSession->expects(self::any())
+ ->method('getUser')
+ ->willReturn($this->user);
+ $this->service->expects(self::once())
+ ->method('getFrom');
+ $this->service->expects(self::once())
+ ->method('addSubjectAndHeading')
+ ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting', false);
+ $this->service->expects(self::once())
+ ->method('addBulletList')
+ ->with($this->emailTemplate, $newVevent, $data);
+ $this->service->expects(self::once())
+ ->method('getAttendeeRsvpOrReqForParticipant')
+ ->willReturn(true);
+ $this->config->expects(self::once())
+ ->method('getValueString')
+ ->with('dav', 'invitation_link_recipients', 'yes')
+ ->willReturn('yes');
+ $this->service->expects(self::once())
+ ->method('createInvitationToken')
+ ->with($message, $newVevent, 1496912700)
+ ->willReturn('token');
+ $this->service->expects(self::once())
+ ->method('addResponseButtons')
+ ->with($this->emailTemplate, 'token');
+ $this->service->expects(self::once())
+ ->method('addMoreOptionsButton')
+ ->with($this->emailTemplate, 'token');
+ $this->mailer->expects(self::once())
+ ->method('send')
+ ->willReturn([]);
$this->mailer
- ->expects($this->once())
- ->method('send');
+ ->method('send')
+ ->willReturn([]);
+ $this->plugin->schedule($message);
+ $this->assertEquals('1.1', $message->getScheduleStatus());
+ }
- if ($expectButtons) {
- $this->queryBuilder->expects($this->at(0))
- ->method('insert')
- ->with('calendar_invitations')
- ->willReturn($this->queryBuilder);
- $this->queryBuilder->expects($this->at(8))
- ->method('values')
- ->willReturn($this->queryBuilder);
- $this->queryBuilder->expects($this->at(9))
- ->method('execute');
- } else {
- $this->queryBuilder->expects($this->never())
- ->method('insert')
- ->with('calendar_invitations');
+ public function testNoButtons(): void {
+ $message = new Message();
+ $message->method = 'REQUEST';
+ $newVCalendar = new VCalendar();
+ $newVevent = new VEvent($newVCalendar, 'VEVENT', array_merge([
+ 'UID' => 'uid-1234',
+ 'SEQUENCE' => 1,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ], []));
+ $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $message->message = $newVCalendar;
+ $message->sender = 'mailto:gandalf@wiz.ard';
+ $message->recipient = 'mailto:' . 'frodo@hobb.it';
+ $data = ['invitee_name' => 'Mr. Wizard',
+ 'meeting_title' => 'Fellowship meeting',
+ 'attendee_name' => 'frodo@hobb.it'
+ ];
+ $attendees = $newVevent->select('ATTENDEE');
+ $atnd = '';
+ foreach ($attendees as $attendee) {
+ if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
+ $atnd = $attendee;
+ }
}
+ $this->service->expects(self::once())
+ ->method('getLastOccurrence')
+ ->willReturn(1496912700);
+ $this->mailer->expects(self::once())
+ ->method('validateMailAddress')
+ ->with('frodo@hobb.it')
+ ->willReturn(true);
+ $this->eventComparisonService->expects(self::once())
+ ->method('findModified')
+ ->with($newVCalendar, null)
+ ->willReturn(['old' => [] ,'new' => [$newVevent]]);
+ $this->service->expects(self::once())
+ ->method('getCurrentAttendee')
+ ->with($message)
+ ->willReturn($atnd);
+ $this->service->expects(self::once())
+ ->method('isRoomOrResource')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('isCircle')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('buildBodyData')
+ ->with($newVevent, null)
+ ->willReturn($data);
+ $this->user->expects(self::any())
+ ->method('getUID')
+ ->willReturn('user1');
+ $this->user->expects(self::any())
+ ->method('getDisplayName')
+ ->willReturn('Mr. Wizard');
+ $this->userSession->expects(self::any())
+ ->method('getUser')
+ ->willReturn($this->user);
+ $this->service->expects(self::once())
+ ->method('getFrom');
+ $this->service->expects(self::once())
+ ->method('addSubjectAndHeading')
+ ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting', false);
+ $this->service->expects(self::once())
+ ->method('addBulletList')
+ ->with($this->emailTemplate, $newVevent, $data);
+ $this->service->expects(self::once())
+ ->method('getAttendeeRsvpOrReqForParticipant')
+ ->willReturn(true);
+ $this->config->expects(self::once())
+ ->method('getValueString')
+ ->with('dav', 'invitation_link_recipients', 'yes')
+ ->willReturn('no');
+ $this->service->expects(self::never())
+ ->method('createInvitationToken');
+ $this->service->expects(self::never())
+ ->method('addResponseButtons');
+ $this->service->expects(self::never())
+ ->method('addMoreOptionsButton');
+ $this->mailer->expects(self::once())
+ ->method('send')
+ ->willReturn([]);
+ $this->mailer
+ ->method('send')
+ ->willReturn([]);
+ $this->plugin->schedule($message);
+ $this->assertEquals('1.1', $message->getScheduleStatus());
}
}
diff --git a/apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php b/apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php
new file mode 100644
index 00000000000..2be6a1cf8b1
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php
@@ -0,0 +1,2200 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace OCA\DAV\Tests\unit\CalDAV\Schedule;
+
+use OC\URLGenerator;
+use OCA\DAV\CalDAV\EventReader;
+use OCA\DAV\CalDAV\Schedule\IMipService;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IL10N;
+use OCP\L10N\IFactory;
+use OCP\Security\ISecureRandom;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Property\ICalendar\DateTime;
+use Test\TestCase;
+
+class IMipServiceTest extends TestCase {
+ private URLGenerator&MockObject $urlGenerator;
+ private IConfig&MockObject $config;
+ private IDBConnection&MockObject $db;
+ private ISecureRandom&MockObject $random;
+ private IFactory&MockObject $l10nFactory;
+ private IL10N&MockObject $l10n;
+ private ITimeFactory&MockObject $timeFactory;
+ private IMipService $service;
+
+
+ private VCalendar $vCalendar1a;
+ private VCalendar $vCalendar1b;
+ private VCalendar $vCalendar2;
+ private VCalendar $vCalendar3;
+ /** @var DateTime DateTime object that will be returned by DateTime() or DateTime('now') */
+ public static $datetimeNow;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->urlGenerator = $this->createMock(URLGenerator::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->db = $this->createMock(IDBConnection::class);
+ $this->random = $this->createMock(ISecureRandom::class);
+ $this->l10nFactory = $this->createMock(IFactory::class);
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->l10nFactory->expects(self::once())
+ ->method('findGenericLanguage')
+ ->willReturn('en');
+ $this->l10nFactory->expects(self::once())
+ ->method('get')
+ ->with('dav', 'en')
+ ->willReturn($this->l10n);
+ $this->service = new IMipService(
+ $this->urlGenerator,
+ $this->config,
+ $this->db,
+ $this->random,
+ $this->l10nFactory,
+ $this->timeFactory
+ );
+
+ // construct calendar with a 1 hour event and same start/end time zones
+ $this->vCalendar1a = new VCalendar();
+ $vEvent = $this->vCalendar1a->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Testing Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a 1 hour event and different start/end time zones
+ $this->vCalendar1b = new VCalendar();
+ $vEvent = $this->vCalendar1b->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Vancouver']);
+ $vEvent->add('SUMMARY', 'Testing Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a full day event
+ $this->vCalendar2 = new VCalendar();
+ // time zone component
+ $vTimeZone = $this->vCalendar2->add('VTIMEZONE');
+ $vTimeZone->add('TZID', 'America/Toronto');
+ // event component
+ $vEvent = $this->vCalendar2->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701');
+ $vEvent->add('DTEND', '20240702');
+ $vEvent->add('SUMMARY', 'Testing Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+
+ // construct calendar with a multi day event
+ $this->vCalendar3 = new VCalendar();
+ // time zone component
+ $vTimeZone = $this->vCalendar3->add('VTIMEZONE');
+ $vTimeZone->add('TZID', 'America/Toronto');
+ // event component
+ $vEvent = $this->vCalendar3->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701');
+ $vEvent->add('DTEND', '20240706');
+ $vEvent->add('SUMMARY', 'Testing Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ }
+
+ public function testGetFrom(): void {
+ $senderName = 'Detective McQueen';
+ $default = 'Twin Lakes Police Department - Darkside Division';
+ $expected = 'Detective McQueen via Twin Lakes Police Department - Darkside Division';
+
+ $this->l10n->expects(self::once())
+ ->method('t')
+ ->willReturn($expected);
+
+ $actual = $this->service->getFrom($senderName, $default);
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testBuildBodyDataCreated(): void {
+
+ // construct l10n return(s)
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'full'] => 'July 1, 2024'
+ };
+ }
+ );
+ $this->l10n->method('n')->willReturnMap([
+ [
+ 'In a day on %1$s between %2$s - %3$s',
+ 'In %n days on %1$s between %2$s - %3$s',
+ 1,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In a day on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ]
+ ]);
+ // construct time factory return(s)
+ $this->timeFactory->method('getDateTime')->willReturnCallback(
+ function ($v1, $v2) {
+ return match (true) {
+ $v1 == 'now' && $v2 == null => (new \DateTime('20240630T000000'))
+ };
+ }
+ );
+ /** test singleton partial day event*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // define expected output
+ $expected = [
+ 'meeting_when' => $this->service->generateWhenString($eventReader),
+ 'meeting_description' => '',
+ 'meeting_title' => 'Testing Event',
+ 'meeting_location' => '',
+ 'meeting_url' => '',
+ 'meeting_url_html' => '',
+ ];
+ // generate actual output
+ $actual = $this->service->buildBodyData($vCalendar->VEVENT[0], null);
+ // test output
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testBuildBodyDataUpdate(): void {
+
+ // construct l10n return(s)
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'full'] => 'July 1, 2024'
+ };
+ }
+ );
+ $this->l10n->method('n')->willReturnMap([
+ [
+ 'In a day on %1$s between %2$s - %3$s',
+ 'In %n days on %1$s between %2$s - %3$s',
+ 1,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In a day on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ]
+ ]);
+ // construct time factory return(s)
+ $this->timeFactory->method('getDateTime')->willReturnCallback(
+ function ($v1, $v2) {
+ return match (true) {
+ $v1 == 'now' && $v2 == null => (new \DateTime('20240630T000000'))
+ };
+ }
+ );
+ /** test singleton partial day event*/
+ $vCalendarNew = clone $this->vCalendar1a;
+ $vCalendarOld = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReaderNew = new EventReader($vCalendarNew, $vCalendarNew->VEVENT[0]->UID->getValue());
+ // alter old event label/title
+ $vCalendarOld->VEVENT[0]->SUMMARY->setValue('Testing Singleton Event');
+ // define expected output
+ $expected = [
+ 'meeting_when' => $this->service->generateWhenString($eventReaderNew),
+ 'meeting_description' => '',
+ 'meeting_title' => 'Testing Event',
+ 'meeting_location' => '',
+ 'meeting_url' => '',
+ 'meeting_url_html' => '',
+ 'meeting_when_html' => $this->service->generateWhenString($eventReaderNew),
+ 'meeting_title_html' => sprintf("<span style='text-decoration: line-through'>%s</span><br />%s", 'Testing Singleton Event', 'Testing Event'),
+ 'meeting_description_html' => '',
+ 'meeting_location_html' => ''
+ ];
+ // generate actual output
+ $actual = $this->service->buildBodyData($vCalendarNew->VEVENT[0], $vCalendarOld->VEVENT[0]);
+ // test output
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testGetLastOccurrenceRRULE(): void {
+ $vCalendar = new VCalendar();
+ $vCalendar->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z',
+ ]);
+
+ $occurrence = $this->service->getLastOccurrence($vCalendar);
+ $this->assertEquals(1454284800, $occurrence);
+ }
+
+ public function testGetLastOccurrenceEndDate(): void {
+ $vCalendar = new VCalendar();
+ $vCalendar->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'DTEND' => new \DateTime('2017-01-01 00:00:00'),
+ ]);
+
+ $occurrence = $this->service->getLastOccurrence($vCalendar);
+ $this->assertEquals(1483228800, $occurrence);
+ }
+
+ public function testGetLastOccurrenceDuration(): void {
+ $vCalendar = new VCalendar();
+ $vCalendar->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ 'DURATION' => 'P12W',
+ ]);
+
+ $occurrence = $this->service->getLastOccurrence($vCalendar);
+ $this->assertEquals(1458864000, $occurrence);
+ }
+
+ public function testGetLastOccurrenceAllDay(): void {
+ $vCalendar = new VCalendar();
+ $vEvent = $vCalendar->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ ]);
+
+ // rewrite from DateTime to Date
+ $vEvent->DTSTART['VALUE'] = 'DATE';
+
+ $occurrence = $this->service->getLastOccurrence($vCalendar);
+ $this->assertEquals(1451692800, $occurrence);
+ }
+
+ public function testGetLastOccurrenceFallback(): void {
+ $vCalendar = new VCalendar();
+ $vCalendar->add('VEVENT', [
+ 'UID' => 'uid-1234',
+ 'LAST-MODIFIED' => 123456,
+ 'SEQUENCE' => 2,
+ 'SUMMARY' => 'Fellowship meeting',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
+ ]);
+
+ $occurrence = $this->service->getLastOccurrence($vCalendar);
+ $this->assertEquals(1451606400, $occurrence);
+ }
+
+ public function testGenerateWhenStringSingular(): void {
+
+ // construct l10n return(s)
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'full'] => 'July 1, 2024',
+ $v1 === 'date' && $v2 == (new \DateTime('20240701T000000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'full'] => 'July 1, 2024'
+ };
+ }
+ );
+ $this->l10n->method('t')->willReturnMap([
+ [
+ 'In the past on %1$s for the entire day',
+ ['July 1, 2024'],
+ 'In the past on July 1, 2024 for the entire day'
+ ],
+ [
+ 'In the past on %1$s between %2$s - %3$s',
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In the past on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ ]);
+ $this->l10n->method('n')->willReturnMap([
+ // singular entire day
+ [
+ 'In a minute on %1$s for the entire day',
+ 'In %n minutes on %1$s for the entire day',
+ 1,
+ ['July 1, 2024'],
+ 'In a minute on July 1, 2024 for the entire day'
+ ],
+ [
+ 'In a hour on %1$s for the entire day',
+ 'In %n hours on %1$s for the entire day',
+ 1,
+ ['July 1, 2024'],
+ 'In a hour on July 1, 2024 for the entire day'
+ ],
+ [
+ 'In a day on %1$s for the entire day',
+ 'In %n days on %1$s for the entire day',
+ 1,
+ ['July 1, 2024'],
+ 'In a day on July 1, 2024 for the entire day'
+ ],
+ [
+ 'In a week on %1$s for the entire day',
+ 'In %n weeks on %1$s for the entire day',
+ 1,
+ ['July 1, 2024'],
+ 'In a week on July 1, 2024 for the entire day'
+ ],
+ [
+ 'In a month on %1$s for the entire day',
+ 'In %n months on %1$s for the entire day',
+ 1,
+ ['July 1, 2024'],
+ 'In a month on July 1, 2024 for the entire day'
+ ],
+ [
+ 'In a year on %1$s for the entire day',
+ 'In %n years on %1$s for the entire day',
+ 1,
+ ['July 1, 2024'],
+ 'In a year on July 1, 2024 for the entire day'
+ ],
+ // plural entire day
+ [
+ 'In a minute on %1$s for the entire day',
+ 'In %n minutes on %1$s for the entire day',
+ 2,
+ ['July 1, 2024'],
+ 'In 2 minutes on July 1, 2024 for the entire day'
+ ],
+ [
+ 'In a hour on %1$s for the entire day',
+ 'In %n hours on %1$s for the entire day',
+ 2,
+ ['July 1, 2024'],
+ 'In 2 hours on July 1, 2024 for the entire day'
+ ],
+ [
+ 'In a day on %1$s for the entire day',
+ 'In %n days on %1$s for the entire day',
+ 2,
+ ['July 1, 2024'],
+ 'In 2 days on July 1, 2024 for the entire day'
+ ],
+ [
+ 'In a week on %1$s for the entire day',
+ 'In %n weeks on %1$s for the entire day',
+ 2,
+ ['July 1, 2024'],
+ 'In 2 weeks on July 1, 2024 for the entire day'
+ ],
+ [
+ 'In a month on %1$s for the entire day',
+ 'In %n months on %1$s for the entire day',
+ 2,
+ ['July 1, 2024'],
+ 'In 2 months on July 1, 2024 for the entire day'
+ ],
+ [
+ 'In a year on %1$s for the entire day',
+ 'In %n years on %1$s for the entire day',
+ 2,
+ ['July 1, 2024'],
+ 'In 2 years on July 1, 2024 for the entire day'
+ ],
+ // singular partial day
+ [
+ 'In a minute on %1$s between %2$s - %3$s',
+ 'In %n minutes on %1$s between %2$s - %3$s',
+ 1,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In a minute on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ [
+ 'In a hour on %1$s between %2$s - %3$s',
+ 'In %n hours on %1$s between %2$s - %3$s',
+ 1,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In a hour on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ [
+ 'In a day on %1$s between %2$s - %3$s',
+ 'In %n days on %1$s between %2$s - %3$s',
+ 1,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In a day on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ [
+ 'In a week on %1$s between %2$s - %3$s',
+ 'In %n weeks on %1$s between %2$s - %3$s',
+ 1,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In a week on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ [
+ 'In a month on %1$s between %2$s - %3$s',
+ 'In %n months on %1$s between %2$s - %3$s',
+ 1,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In a month on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ [
+ 'In a year on %1$s between %2$s - %3$s',
+ 'In %n years on %1$s between %2$s - %3$s',
+ 1,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In a year on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ // plural partial day
+ [
+ 'In a minute on %1$s between %2$s - %3$s',
+ 'In %n minutes on %1$s between %2$s - %3$s',
+ 2,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In 2 minutes on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ [
+ 'In a hour on %1$s between %2$s - %3$s',
+ 'In %n hours on %1$s between %2$s - %3$s',
+ 2,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In 2 hours on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ [
+ 'In a day on %1$s between %2$s - %3$s',
+ 'In %n days on %1$s between %2$s - %3$s',
+ 2,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In 2 days on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ [
+ 'In a week on %1$s between %2$s - %3$s',
+ 'In %n weeks on %1$s between %2$s - %3$s',
+ 2,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In 2 weeks on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ [
+ 'In a month on %1$s between %2$s - %3$s',
+ 'In %n months on %1$s between %2$s - %3$s',
+ 2,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In 2 months on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ [
+ 'In a year on %1$s between %2$s - %3$s',
+ 'In %n years on %1$s between %2$s - %3$s',
+ 2,
+ ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'],
+ 'In 2 years on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)'
+ ],
+ ]);
+
+ // construct time factory return(s)
+ $this->timeFactory->method('getDateTime')->willReturnOnConsecutiveCalls(
+ // past interval test dates
+ (new \DateTime('20240702T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240703T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240702T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240703T170000', (new \DateTimeZone('America/Toronto')))),
+ // minute interval test dates
+ (new \DateTime('20240701T075900', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240630T235900', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240701T075800', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240630T235800', (new \DateTimeZone('America/Toronto')))),
+ // hour interval test dates
+ (new \DateTime('20240701T070000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240630T230000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240701T060000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240630T220000', (new \DateTimeZone('America/Toronto')))),
+ // day interval test dates
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ // week interval test dates
+ (new \DateTime('20240621T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240621T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240614T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240614T170000', (new \DateTimeZone('America/Toronto')))),
+ // month interval test dates
+ (new \DateTime('20240530T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240530T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240430T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240430T170000', (new \DateTimeZone('America/Toronto')))),
+ // year interval test dates
+ (new \DateTime('20230630T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20230630T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20220630T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20220630T170000', (new \DateTimeZone('America/Toronto'))))
+ );
+
+ /** test partial day event in 1 day in the past*/
+ $vCalendar = clone $this->vCalendar1a;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In the past on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 1 day in the past*/
+ $vCalendar = clone $this->vCalendar2;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In the past on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event in 2 days in the past*/
+ $vCalendar = clone $this->vCalendar1a;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In the past on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 2 days in the past*/
+ $vCalendar = clone $this->vCalendar2;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In the past on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event in 1 minute*/
+ $vCalendar = clone $this->vCalendar1a;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In a minute on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 1 minute*/
+ $vCalendar = clone $this->vCalendar2;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In a minute on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event in 2 minutes*/
+ $vCalendar = clone $this->vCalendar1a;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In 2 minutes on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 2 minutes*/
+ $vCalendar = clone $this->vCalendar2;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In 2 minutes on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event in 1 hour*/
+ $vCalendar = clone $this->vCalendar1a;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In a hour on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 1 hour*/
+ $vCalendar = clone $this->vCalendar2;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In a hour on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event in 2 hours*/
+ $vCalendar = clone $this->vCalendar1a;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In 2 hours on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 2 hours*/
+ $vCalendar = clone $this->vCalendar2;
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ $this->assertEquals(
+ 'In 2 hours on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test patrial day event in 1 day*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 1 day*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test patrial day event in 2 days*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 2 days*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test patrial day event in 1 week*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a week on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 1 week*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a week on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test patrial day event in 2 weeks*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 weeks on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 2 weeks*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 weeks on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test patrial day event in 1 month*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a month on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 1 month*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a month on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test patrial day event in 2 months*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 months on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 2 months*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 months on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test patrial day event in 1 year*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a year on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 1 year*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a year on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test patrial day event in 2 years*/
+ $vCalendar = clone $this->vCalendar1a;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 years on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event in 2 years*/
+ $vCalendar = clone $this->vCalendar2;
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 years on July 1, 2024 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ }
+
+ public function testGenerateWhenStringRecurringDaily(): void {
+
+ // construct l10n return maps
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20240713T080000', (new \DateTimeZone('UTC')))) && $v3 == ['width' => 'long'] => 'July 13, 2024'
+ };
+ }
+ );
+ $this->l10n->method('t')->willReturnMap([
+ ['Every Day for the entire day', [], 'Every Day for the entire day'],
+ ['Every Day for the entire day until %1$s', ['July 13, 2024'], 'Every Day for the entire day until July 13, 2024'],
+ ['Every Day between %1$s - %2$s', ['8:00 AM', '9:00 AM (America/Toronto)'], 'Every Day between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every Day between %1$s - %2$s until %3$s', ['8:00 AM', '9:00 AM (America/Toronto)', 'July 13, 2024'], 'Every Day between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024'],
+ ['Every %1$d Days for the entire day', [3], 'Every 3 Days for the entire day'],
+ ['Every %1$d Days for the entire day until %2$s', [3, 'July 13, 2024'], 'Every 3 Days for the entire day until July 13, 2024'],
+ ['Every %1$d Days between %2$s - %3$s', [3, '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 3 Days between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every %1$d Days between %2$s - %3$s until %4$s', [3, '8:00 AM', '9:00 AM (America/Toronto)', 'July 13, 2024'], 'Every 3 Days between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024'],
+ ['Could not generate event recurrence statement', [], 'Could not generate event recurrence statement'],
+ ]);
+
+ /** test partial day event with every day interval and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=1;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Day between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event with every day interval and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=1;UNTIL=20240713T080000Z');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Day between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event every 3rd day interval and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=3;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 3 Days between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event with every 3rd day interval and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=3;UNTIL=20240713T080000Z');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 3 Days between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every day interval and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=1;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Day for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every day interval and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=1;UNTIL=20240713T080000Z');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Day for the entire day until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every 3rd day interval and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=3;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 3 Days for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every 3rd day interval and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=3;UNTIL=20240713T080000Z');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 3 Days for the entire day until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ }
+
+ public function testGenerateWhenStringRecurringWeekly(): void {
+
+ // construct l10n return maps
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20240722T080000', (new \DateTimeZone('UTC')))) && $v3 == ['width' => 'long'] => 'July 13, 2024'
+ };
+ }
+ );
+ $this->l10n->method('t')->willReturnMap([
+ ['Every Week on %1$s for the entire day', ['Monday, Wednesday, Friday'], 'Every Week on Monday, Wednesday, Friday for the entire day'],
+ ['Every Week on %1$s for the entire day until %2$s', ['Monday, Wednesday, Friday', 'July 13, 2024'], 'Every Week on Monday, Wednesday, Friday for the entire day until July 13, 2024'],
+ ['Every Week on %1$s between %2$s - %3$s', ['Monday, Wednesday, Friday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every Week on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every Week on %1$s between %2$s - %3$s until %4$s', ['Monday, Wednesday, Friday', '8:00 AM', '9:00 AM (America/Toronto)', 'July 13, 2024'], 'Every Week on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024'],
+ ['Every %1$d Weeks on %2$s for the entire day', [2, 'Monday, Wednesday, Friday'], 'Every 2 Weeks on Monday, Wednesday, Friday for the entire day'],
+ ['Every %1$d Weeks on %2$s for the entire day until %3$s', [2, 'Monday, Wednesday, Friday', 'July 13, 2024'], 'Every 2 Weeks on Monday, Wednesday, Friday for the entire day until July 13, 2024'],
+ ['Every %1$d Weeks on %2$s between %3$s - %4$s', [2, 'Monday, Wednesday, Friday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 2 Weeks on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s', [2, 'Monday, Wednesday, Friday', '8:00 AM', '9:00 AM (America/Toronto)', 'July 13, 2024'], 'Every 2 Weeks on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024'],
+ ['Could not generate event recurrence statement', [], 'Could not generate event recurrence statement'],
+ ['Monday', [], 'Monday'],
+ ['Wednesday', [], 'Wednesday'],
+ ['Friday', [], 'Friday'],
+ ]);
+
+ /** test partial day event with every week interval on Mon, Wed, Fri and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Week on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event with every week interval on Mon, Wed, Fri and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20240722T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Week on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event with every 2nd week interval on Mon, Wed, Fri and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Weeks on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test partial day event with every 2nd week interval on Mon, Wed, Fri and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2;UNTIL=20240722T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Weeks on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every week interval on Mon, Wed, Fri and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Week on Monday, Wednesday, Friday for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every week interval on Mon, Wed, Fri and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20240722T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Week on Monday, Wednesday, Friday for the entire day until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every 2nd week interval on Mon, Wed, Fri and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Weeks on Monday, Wednesday, Friday for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every 2nd week interval on Mon, Wed, Fri and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2;UNTIL=20240722T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Weeks on Monday, Wednesday, Friday for the entire day until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ }
+
+ public function testGenerateWhenStringRecurringMonthly(): void {
+
+ // construct l10n return maps
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20241231T080000', (new \DateTimeZone('UTC')))) && $v3 == ['width' => 'long'] => 'December 31, 2024'
+ };
+ }
+ );
+ $this->l10n->method('t')->willReturnMap([
+ ['Every Month on the %1$s for the entire day', ['1, 8'], 'Every Month on the 1, 8 for the entire day'],
+ ['Every Month on the %1$s for the entire day until %2$s', ['1, 8', 'December 31, 2024'], 'Every Month on the 1, 8 for the entire day until December 31, 2024'],
+ ['Every Month on the %1$s between %2$s - %3$s', ['1, 8', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every Month on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every Month on the %1$s between %2$s - %3$s until %4$s', ['1, 8', '8:00 AM', '9:00 AM (America/Toronto)', 'December 31, 2024'], 'Every Month on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024'],
+ ['Every %1$d Months on the %2$s for the entire day', [2, '1, 8'], 'Every 2 Months on the 1, 8 for the entire day'],
+ ['Every %1$d Months on the %2$s for the entire day until %3$s', [2, '1, 8', 'December 31, 2024'], 'Every 2 Months on the 1, 8 for the entire day until December 31, 2024'],
+ ['Every %1$d Months on the %2$s between %3$s - %4$s', [2, '1, 8', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 2 Months on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every %1$d Months on the %2$s between %3$s - %4$s until %5$s', [2, '1, 8', '8:00 AM', '9:00 AM (America/Toronto)', 'December 31, 2024'], 'Every 2 Months on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024'],
+ ['Every Month on the %1$s for the entire day', ['First Sunday, Saturday'], 'Every Month on the First Sunday, Saturday for the entire day'],
+ ['Every Month on the %1$s for the entire day until %2$s', ['First Sunday, Saturday', 'December 31, 2024'], 'Every Month on the First Sunday, Saturday for the entire day until December 31, 2024'],
+ ['Every Month on the %1$s between %2$s - %3$s', ['First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every Month on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every Month on the %1$s between %2$s - %3$s until %4$s', ['First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)', 'December 31, 2024'], 'Every Month on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024'],
+ ['Every %1$d Months on the %2$s for the entire day', [2, 'First Sunday, Saturday'], 'Every 2 Months on the First Sunday, Saturday for the entire day'],
+ ['Every %1$d Months on the %2$s for the entire day until %3$s', [2, 'First Sunday, Saturday', 'December 31, 2024'], 'Every 2 Months on the First Sunday, Saturday for the entire day until December 31, 2024'],
+ ['Every %1$d Months on the %2$s between %3$s - %4$s', [2, 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 2 Months on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every %1$d Months on the %2$s between %3$s - %4$s until %5$s', [2, 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)', 'December 31, 2024'], 'Every 2 Months on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024'],
+ ['Could not generate event recurrence statement', [], 'Could not generate event recurrence statement'],
+ ['Saturday', [], 'Saturday'],
+ ['Sunday', [], 'Sunday'],
+ ['First', [], 'First'],
+ ]);
+
+ /** test absolute partial day event with every month interval on 1st, 8th and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute partial day event with every Month interval on 1st, 8th and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute partial day event with every 2nd Month interval on 1st, 8th and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute partial day event with every 2nd Month interval on 1st, 8th and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;INTERVAL=2;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every Month interval on 1st, 8th and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the 1, 8 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every Month interval on 1st, 8th and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the 1, 8 for the entire day until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every 2nd Month interval on 1st, 8th and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the 1, 8 for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every 2nd Month interval on 1st, 8th and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;INTERVAL=2;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the 1, 8 for the entire day until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every month interval on the 1st Saturday, Sunday and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every Month interval on the 1st Saturday, Sunday and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every 2nd Month interval on the 1st Saturday, Sunday and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every 2nd Month interval on the 1st Saturday, Sunday and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every Month interval on the 1st Saturday, Sunday and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the First Sunday, Saturday for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every Month interval on the 1st Saturday, Sunday and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Month on the First Sunday, Saturday for the entire day until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every 2nd Month interval on the 1st Saturday, Sunday and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the First Sunday, Saturday for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every 2nd Month interval on the 1st Saturday, Sunday and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;UNTIL=20241231T080000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Months on the First Sunday, Saturday for the entire day until December 31, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ }
+
+ public function testGenerateWhenStringRecurringYearly(): void {
+
+ // construct l10n return maps
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20260731T040000', (new \DateTimeZone('UTC')))) && $v3 == ['width' => 'long'] => 'July 31, 2026'
+ };
+ }
+ );
+ $this->l10n->method('t')->willReturnMap([
+ ['Every Year in %1$s on the %2$s for the entire day', ['July', '1st'], 'Every Year in July on the 1st for the entire day'],
+ ['Every Year in %1$s on the %2$s for the entire day until %3$s', ['July', '1st', 'July 31, 2026'], 'Every Year in July on the 1st for the entire day until July 31, 2026'],
+ ['Every Year in %1$s on the %2$s between %3$s - %4$s', ['July', '1st', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every Year in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s', ['July', '1st', '8:00 AM', '9:00 AM (America/Toronto)', 'July 31, 2026'], 'Every Year in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026'],
+ ['Every %1$d Years in %2$s on the %3$s for the entire day', [2, 'July', '1st'], 'Every 2 Years in July on the 1st for the entire day'],
+ ['Every %1$d Years in %2$s on the %3$s for the entire day until %4$s', [2, 'July', '1st', 'July 31, 2026'], 'Every 2 Years in July on the 1st for the entire day until July 31, 2026'],
+ ['Every %1$d Years in %2$s on the %3$s between %4$s - %5$s', [2, 'July', '1st', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 2 Years in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s', [2, 'July', '1st', '8:00 AM', '9:00 AM (America/Toronto)', 'July 31, 2026'], 'Every 2 Years in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026'],
+ ['Every Year in %1$s on the %2$s for the entire day', ['July', 'First Sunday, Saturday'], 'Every Year in July on the First Sunday, Saturday for the entire day'],
+ ['Every Year in %1$s on the %2$s for the entire day until %3$s', ['July', 'First Sunday, Saturday', 'July 31, 2026'], 'Every Year in July on the First Sunday, Saturday for the entire day until July 31, 2026'],
+ ['Every Year in %1$s on the %2$s between %3$s - %4$s', ['July', 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every Year in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s', ['July', 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)', 'July 31, 2026'], 'Every Year in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026'],
+ ['Every %1$d Years in %2$s on the %3$s for the entire day', [2, 'July', 'First Sunday, Saturday'], 'Every 2 Years in July on the First Sunday, Saturday for the entire day'],
+ ['Every %1$d Years in %2$s on the %3$s for the entire day until %4$s', [2, 'July', 'First Sunday, Saturday', 'July 31, 2026'], 'Every 2 Years in July on the First Sunday, Saturday for the entire day until July 31, 2026'],
+ ['Every %1$d Years in %2$s on the %3$s between %4$s - %5$s', [2, 'July', 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 2 Years in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)'],
+ ['Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s', [2, 'July', 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)', 'July 31, 2026'], 'Every 2 Years in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026'],
+ ['Could not generate event recurrence statement', [], 'Could not generate event recurrence statement'],
+ ['July', [], 'July'],
+ ['Saturday', [], 'Saturday'],
+ ['Sunday', [], 'Sunday'],
+ ['First', [], 'First'],
+ ]);
+
+ /** test absolute partial day event with every year interval on July 1 and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute partial day event with every year interval on July 1 and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;UNTIL=20260731T040000Z');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute partial day event with every 2nd year interval on July 1 and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute partial day event with every 2nd year interval on July 1 and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;INTERVAL=2;UNTIL=20260731T040000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every year interval on July 1 and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the 1st for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every year interval on July 1 and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;UNTIL=20260731T040000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the 1st for the entire day until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every 2nd year interval on July 1 and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the 1st for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test absolute entire day event with every 2nd year interval on July 1 and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;INTERVAL=2;UNTIL=20260731T040000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the 1st for the entire day until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every year interval on the 1st Saturday, Sunday in July and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every year interval on the 1st Saturday, Sunday in July and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;UNTIL=20260731T040000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every 2nd year interval on the 1st Saturday, Sunday in July and no conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative partial day event with every 2nd year interval on the 1st Saturday, Sunday in July and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;UNTIL=20260731T040000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every year interval on the 1st Saturday, Sunday in July and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the First Sunday, Saturday for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every year interval on the 1st Saturday, Sunday in July and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;UNTIL=20260731T040000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every Year in July on the First Sunday, Saturday for the entire day until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every 2nd year interval on the 1st Saturday, Sunday in July and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the First Sunday, Saturday for the entire day',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test relative entire day event with every 2nd year interval on the 1st Saturday, Sunday in July and conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;UNTIL=20260731T040000Z;');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'Every 2 Years in July on the First Sunday, Saturday for the entire day until July 31, 2026',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ }
+
+ public function testGenerateWhenStringRecurringFixed(): void {
+
+ // construct l10n return maps
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM',
+ $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM',
+ $v1 === 'date' && $v2 == (new \DateTime('20240713T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 13, 2024'
+ };
+ }
+ );
+ $this->l10n->method('t')->willReturnMap([
+ ['On specific dates for the entire day until %1$s', ['July 13, 2024'], 'On specific dates for the entire day until July 13, 2024'],
+ ['On specific dates between %1$s - %2$s until %3$s', ['8:00 AM', '9:00 AM (America/Toronto)', 'July 13, 2024'], 'On specific dates between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024'],
+ ]);
+
+ /** test partial day event with every day interval and conclusion*/
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703T080000,20240709T080000,20240713T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'On specific dates between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ /** test entire day event with every day interval and no conclusion*/
+ $vCalendar = clone $this->vCalendar2;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703T080000,20240709T080000,20240713T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'On specific dates for the entire day until July 13, 2024',
+ $this->service->generateWhenString($eventReader)
+ );
+
+ }
+
+ public function testGenerateOccurringStringWithRrule(): void {
+
+ // construct l10n return(s)
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 1, 2024',
+ $v1 === 'date' && $v2 == (new \DateTime('20240703T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 3, 2024',
+ $v1 === 'date' && $v2 == (new \DateTime('20240705T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 5, 2024'
+ };
+ }
+ );
+ $this->l10n->method('n')->willReturnMap([
+ // singular
+ [
+ 'In a day on %1$s',
+ 'In %n days on %1$s',
+ 1,
+ ['July 1, 2024'],
+ 'In a day on July 1, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s',
+ 'In %n days on %1$s then on %2$s',
+ 1,
+ ['July 1, 2024', 'July 3, 2024'],
+ 'In a day on July 1, 2024 then on July 3, 2024'
+ ],
+ [
+ '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',
+ 1,
+ ['July 1, 2024', 'July 3, 2024', 'July 5, 2024'],
+ 'In a day on July 1, 2024 then on July 3, 2024 and July 5, 2024'
+ ],
+ // plural
+ [
+ 'In a day on %1$s',
+ 'In %n days on %1$s',
+ 2,
+ ['July 1, 2024'],
+ 'In 2 days on July 1, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s',
+ 'In %n days on %1$s then on %2$s',
+ 2,
+ ['July 1, 2024', 'July 3, 2024'],
+ 'In 2 days on July 1, 2024 then on July 3, 2024'
+ ],
+ [
+ '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',
+ 2,
+ ['July 1, 2024', 'July 3, 2024', 'July 5, 2024'],
+ 'In 2 days on July 1, 2024 then on July 3, 2024 and July 5, 2024'
+ ],
+ ]);
+
+ // construct time factory return(s)
+ $this->timeFactory->method('getDateTime')->willReturnOnConsecutiveCalls(
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ );
+
+ /** test patrial day recurring event in 1 day with single occurrence remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024',
+ $this->service->generateOccurringString($eventReader)
+ );
+
+ /** test patrial day recurring event in 1 day with two occurrences remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024 then on July 3, 2024',
+ $this->service->generateOccurringString($eventReader)
+ );
+
+ /** test patrial day recurring event in 1 day with three occurrences remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024 then on July 3, 2024 and July 5, 2024',
+ $this->service->generateOccurringString($eventReader)
+ );
+
+ /** test patrial day recurring event in 2 days with single occurrence remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024',
+ $this->service->generateOccurringString($eventReader)
+ );
+
+ /** test patrial day recurring event in 2 days with two occurrences remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024 then on July 3, 2024',
+ $this->service->generateOccurringString($eventReader)
+ );
+
+ /** test patrial day recurring event in 2 days with three occurrences remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024 then on July 3, 2024 and July 5, 2024',
+ $this->service->generateOccurringString($eventReader)
+ );
+ }
+
+ public function testGenerateOccurringStringWithRdate(): void {
+
+ // construct l10n return(s)
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 1, 2024',
+ $v1 === 'date' && $v2 == (new \DateTime('20240703T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 3, 2024',
+ $v1 === 'date' && $v2 == (new \DateTime('20240705T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 5, 2024'
+ };
+ }
+ );
+ $this->l10n->method('n')->willReturnMap([
+ // singular
+ [
+ 'In a day on %1$s',
+ 'In %n days on %1$s',
+ 1,
+ ['July 1, 2024'],
+ 'In a day on July 1, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s',
+ 'In %n days on %1$s then on %2$s',
+ 1,
+ ['July 1, 2024', 'July 3, 2024'],
+ 'In a day on July 1, 2024 then on July 3, 2024'
+ ],
+ [
+ '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',
+ 1,
+ ['July 1, 2024', 'July 3, 2024', 'July 5, 2024'],
+ 'In a day on July 1, 2024 then on July 3, 2024 and July 5, 2024'
+ ],
+ // plural
+ [
+ 'In a day on %1$s',
+ 'In %n days on %1$s',
+ 2,
+ ['July 1, 2024'],
+ 'In 2 days on July 1, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s',
+ 'In %n days on %1$s then on %2$s',
+ 2,
+ ['July 1, 2024', 'July 3, 2024'],
+ 'In 2 days on July 1, 2024 then on July 3, 2024'
+ ],
+ [
+ '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',
+ 2,
+ ['July 1, 2024', 'July 3, 2024', 'July 5, 2024'],
+ 'In 2 days on July 1, 2024 then on July 3, 2024 and July 5, 2024'
+ ],
+ ]);
+
+ // construct time factory return(s)
+ $this->timeFactory->method('getDateTime')->willReturnOnConsecutiveCalls(
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ );
+
+ /** test patrial day recurring event in 1 day with single occurrence remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 1 day with single occurrence remaining'
+ );
+
+ /** test patrial day recurring event in 1 day with two occurrences remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000,20240703T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024 then on July 3, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 1 day with two occurrences remaining'
+ );
+
+ /** test patrial day recurring event in 1 day with three occurrences remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000,20240703T080000,20240705T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024 then on July 3, 2024 and July 5, 2024',
+ $this->service->generateOccurringString($eventReader),
+ ''
+ );
+
+ /** test patrial day recurring event in 2 days with single occurrences remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024',
+ $this->service->generateOccurringString($eventReader),
+ ''
+ );
+
+ /** test patrial day recurring event in 2 days with two occurrences remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024 then on July 3, 2024',
+ $this->service->generateOccurringString($eventReader),
+ ''
+ );
+
+ /** test patrial day recurring event in 2 days with three occurrences remaining */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240703T080000');
+ $vCalendar->VEVENT[0]->add('RDATE', '20240705T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024 then on July 3, 2024 and July 5, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 2 days with three occurrences remaining'
+ );
+ }
+
+ public function testGenerateOccurringStringWithOneExdate(): void {
+
+ // construct l10n return(s)
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 1, 2024',
+ $v1 === 'date' && $v2 == (new \DateTime('20240705T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 5, 2024',
+ $v1 === 'date' && $v2 == (new \DateTime('20240707T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 7, 2024'
+ };
+ }
+ );
+ $this->l10n->method('n')->willReturnMap([
+ // singular
+ [
+ 'In a day on %1$s',
+ 'In %n days on %1$s',
+ 1,
+ ['July 1, 2024'],
+ 'In a day on July 1, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s',
+ 'In %n days on %1$s then on %2$s',
+ 1,
+ ['July 1, 2024', 'July 5, 2024'],
+ 'In a day on July 1, 2024 then on July 5, 2024'
+ ],
+ [
+ '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',
+ 1,
+ ['July 1, 2024', 'July 5, 2024', 'July 7, 2024'],
+ 'In a day on July 1, 2024 then on July 5, 2024 and July 7, 2024'
+ ],
+ // plural
+ [
+ 'In a day on %1$s',
+ 'In %n days on %1$s',
+ 2,
+ ['July 1, 2024'],
+ 'In 2 days on July 1, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s',
+ 'In %n days on %1$s then on %2$s',
+ 2,
+ ['July 1, 2024', 'July 5, 2024'],
+ 'In 2 days on July 1, 2024 then on July 5, 2024'
+ ],
+ [
+ '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',
+ 2,
+ ['July 1, 2024', 'July 5, 2024', 'July 7, 2024'],
+ 'In 2 days on July 1, 2024 then on July 5, 2024 and July 7, 2024'
+ ],
+ ]);
+
+ // construct time factory return(s)
+ $this->timeFactory->method('getDateTime')->willReturnOnConsecutiveCalls(
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ );
+
+ /** test patrial day recurring event in 1 day with single occurrence remaining and one exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 1 day with single occurrence remaining and one exception'
+ );
+
+ /** test patrial day recurring event in 1 day with two occurrences remaining and one exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 1 day with two occurrences remaining and one exception'
+ );
+
+ /** test patrial day recurring event in 1 day with three occurrences remaining and one exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024 then on July 5, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 1 day with three occurrences remaining and one exception'
+ );
+
+ /** test patrial day recurring event in 1 day with four occurrences remaining and one exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=4');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024 then on July 5, 2024 and July 7, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 1 day with four occurrences remaining and one exception'
+ );
+
+ /** test patrial day recurring event in 2 days with single occurrences remaining and one exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 2 days with single occurrences remaining and one exception'
+ );
+
+ /** test patrial day recurring event in 2 days with two occurrences remaining and one exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 2 days with two occurrences remaining and one exception'
+ );
+
+ /** test patrial day recurring event in 2 days with three occurrences remaining and one exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024 then on July 5, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 2 days with three occurrences remaining and one exception'
+ );
+
+ /** test patrial day recurring event in 2 days with four occurrences remaining and one exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=4');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024 then on July 5, 2024 and July 7, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 2 days with four occurrences remaining and one exception'
+ );
+ }
+
+ public function testGenerateOccurringStringWithTwoExdate(): void {
+
+ // construct l10n return(s)
+ $this->l10n->method('l')->willReturnCallback(
+ function ($v1, $v2, $v3) {
+ return match (true) {
+ $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 1, 2024',
+ $v1 === 'date' && $v2 == (new \DateTime('20240705T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 5, 2024',
+ $v1 === 'date' && $v2 == (new \DateTime('20240709T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 9, 2024'
+ };
+ }
+ );
+ $this->l10n->method('n')->willReturnMap([
+ // singular
+ [
+ 'In a day on %1$s',
+ 'In %n days on %1$s',
+ 1,
+ ['July 1, 2024'],
+ 'In a day on July 1, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s',
+ 'In %n days on %1$s then on %2$s',
+ 1,
+ ['July 1, 2024', 'July 5, 2024'],
+ 'In a day on July 1, 2024 then on July 5, 2024'
+ ],
+ [
+ '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',
+ 1,
+ ['July 1, 2024', 'July 5, 2024', 'July 9, 2024'],
+ 'In a day on July 1, 2024 then on July 5, 2024 and July 9, 2024'
+ ],
+ // plural
+ [
+ 'In a day on %1$s',
+ 'In %n days on %1$s',
+ 2,
+ ['July 1, 2024'],
+ 'In 2 days on July 1, 2024'
+ ],
+ [
+ 'In a day on %1$s then on %2$s',
+ 'In %n days on %1$s then on %2$s',
+ 2,
+ ['July 1, 2024', 'July 5, 2024'],
+ 'In 2 days on July 1, 2024 then on July 5, 2024'
+ ],
+ [
+ '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',
+ 2,
+ ['July 1, 2024', 'July 5, 2024', 'July 9, 2024'],
+ 'In 2 days on July 1, 2024 then on July 5, 2024 and July 9, 2024'
+ ],
+ ]);
+
+ // construct time factory return(s)
+ $this->timeFactory->method('getDateTime')->willReturnOnConsecutiveCalls(
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))),
+ );
+
+ /** test patrial day recurring event in 1 day with single occurrence remaining and two exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 1 day with single occurrence remaining and two exception'
+ );
+
+ /** test patrial day recurring event in 1 day with two occurrences remaining and two exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 1 day with two occurrences remaining and two exception'
+ );
+
+ /** test patrial day recurring event in 1 day with three occurrences remaining and two exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024 then on July 5, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 1 day with three occurrences remaining and two exception'
+ );
+
+ /** test patrial day recurring event in 1 day with four occurrences remaining and two exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=5');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In a day on July 1, 2024 then on July 5, 2024 and July 9, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 1 day with four occurrences remaining and two exception'
+ );
+
+ /** test patrial day recurring event in 2 days with single occurrences remaining and two exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 2 days with single occurrences remaining and two exception'
+ );
+
+ /** test patrial day recurring event in 2 days with two occurrences remaining and two exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 2 days with two occurrences remaining and two exception'
+ );
+
+ /** test patrial day recurring event in 2 days with three occurrences remaining and two exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024 then on July 5, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 2 days with three occurrences remaining and two exception'
+ );
+
+ /** test patrial day recurring event in 2 days with five occurrences remaining and two exception */
+ $vCalendar = clone $this->vCalendar1a;
+ $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=5');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000');
+ $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000');
+ // construct event reader
+ $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
+ // test output
+ $this->assertEquals(
+ 'In 2 days on July 1, 2024 then on July 5, 2024 and July 9, 2024',
+ $this->service->generateOccurringString($eventReader),
+ 'test patrial day recurring event in 2 days with five occurrences remaining and two exception'
+ );
+ }
+
+}
diff --git a/apps/dav/tests/unit/CalDAV/Schedule/PluginTest.php b/apps/dav/tests/unit/CalDAV/Schedule/PluginTest.php
index 2518cc3d91a..524ac556e19 100644
--- a/apps/dav/tests/unit/CalDAV/Schedule/PluginTest.php
+++ b/apps/dav/tests/unit/CalDAV/Schedule/PluginTest.php
@@ -1,94 +1,88 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
- *
- * @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 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\Tests\unit\CalDAV\Schedule;
use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\Calendar;
use OCA\DAV\CalDAV\CalendarHome;
+use OCA\DAV\CalDAV\DefaultCalendarValidator;
use OCA\DAV\CalDAV\Plugin as CalDAVPlugin;
use OCA\DAV\CalDAV\Schedule\Plugin;
+use OCA\DAV\CalDAV\Trashbin\Plugin as TrashbinPlugin;
use OCP\IConfig;
+use OCP\IL10N;
use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\CalDAV\Backend\BackendInterface;
use Sabre\DAV\PropFind;
use Sabre\DAV\Server;
use Sabre\DAV\Tree;
use Sabre\DAV\Xml\Property\Href;
use Sabre\DAV\Xml\Property\LocalHref;
use Sabre\DAVACL\IPrincipal;
+use Sabre\HTTP\Request;
+use Sabre\HTTP\Response;
use Sabre\HTTP\ResponseInterface;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\ITip\Message;
use Sabre\VObject\Parameter;
use Sabre\VObject\Property\ICalendar\CalAddress;
use Sabre\Xml\Service;
use Test\TestCase;
class PluginTest extends TestCase {
- /** @var Plugin */
- private $plugin;
- /** @var Server|MockObject */
- private $server;
-
- /** @var IConfig|MockObject */
- private $config;
+ private Plugin $plugin;
+ private Server&MockObject $server;
+ private IConfig&MockObject $config;
+ private LoggerInterface&MockObject $logger;
+ private DefaultCalendarValidator $calendarValidator;
protected function setUp(): void {
parent::setUp();
- $this->server = $this->createMock(Server::class);
$this->config = $this->createMock(IConfig::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->calendarValidator = new DefaultCalendarValidator();
- $response = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->server->httpResponse = $response;
+ $this->server = $this->createMock(Server::class);
+ $this->server->httpResponse = $this->createMock(ResponseInterface::class);
$this->server->xml = new Service();
- $this->plugin = new Plugin($this->config);
+ $this->plugin = new Plugin($this->config, $this->logger, $this->calendarValidator);
$this->plugin->initialize($this->server);
}
- public function testInitialize() {
- $plugin = new Plugin($this->config);
-
- $this->server->expects($this->at(7))
- ->method('on')
- ->with('propFind', [$plugin, 'propFindDefaultCalendarUrl'], 90);
-
- $this->server->expects($this->at(8))
- ->method('on')
- ->with('afterWriteContent', [$plugin, 'dispatchSchedulingResponses']);
-
- $this->server->expects($this->at(9))
+ public function testInitialize(): void {
+ $calls = [
+ // Sabre\CalDAV\Schedule\Plugin events
+ ['method:POST', [$this->plugin, 'httpPost'], 100],
+ ['propFind', [$this->plugin, 'propFind'], 100],
+ ['propPatch', [$this->plugin, 'propPatch'], 100],
+ ['calendarObjectChange', [$this->plugin, 'calendarObjectChange'], 100],
+ ['beforeUnbind', [$this->plugin, 'beforeUnbind'], 100],
+ ['schedule', [$this->plugin, 'scheduleLocalDelivery'], 100],
+ ['getSupportedPrivilegeSet', [$this->plugin, 'getSupportedPrivilegeSet'], 100],
+ // OCA\DAV\CalDAV\Schedule\Plugin events
+ ['propFind', [$this->plugin, 'propFindDefaultCalendarUrl'], 90],
+ ['afterWriteContent', [$this->plugin, 'dispatchSchedulingResponses'], 100],
+ ['afterCreateFile', [$this->plugin, 'dispatchSchedulingResponses'], 100],
+ ];
+ $this->server->expects($this->exactly(count($calls)))
->method('on')
- ->with('afterCreateFile', [$plugin, 'dispatchSchedulingResponses']);
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
- $plugin->initialize($this->server);
+ $this->plugin->initialize($this->server);
}
- public function testGetAddressesForPrincipal() {
+ public function testGetAddressesForPrincipal(): void {
$href = $this->createMock(Href::class);
$href
->expects($this->once())
@@ -111,8 +105,7 @@ class PluginTest extends TestCase {
$this->assertSame(['lukas@nextcloud.com', 'rullzer@nextcloud.com'], $result);
}
-
- public function testGetAddressesForPrincipalEmpty() {
+ public function testGetAddressesForPrincipalEmpty(): void {
$this->server
->expects($this->once())
->method('getProperties')
@@ -128,12 +121,12 @@ class PluginTest extends TestCase {
$this->assertSame([], $result);
}
- public function testStripOffMailTo() {
+ public function testStripOffMailTo(): void {
$this->assertEquals('test@example.com', $this->invokePrivate($this->plugin, 'stripOffMailTo', ['test@example.com']));
$this->assertEquals('test@example.com', $this->invokePrivate($this->plugin, 'stripOffMailTo', ['mailto:test@example.com']));
}
- public function testGetAttendeeRSVP() {
+ public function testGetAttendeeRSVP(): void {
$property1 = $this->createMock(CalAddress::class);
$parameter1 = $this->createMock(Parameter::class);
$property1->expects($this->once())
@@ -167,7 +160,7 @@ class PluginTest extends TestCase {
$this->assertFalse($this->invokePrivate($this->plugin, 'getAttendeeRSVP', [$property3]));
}
- public function propFindDefaultCalendarUrlProvider(): array {
+ public static function propFindDefaultCalendarUrlProvider(): array {
return [
[
'principals/users/myuser',
@@ -183,6 +176,25 @@ class PluginTest extends TestCase {
false,
CalDavBackend::PERSONAL_CALENDAR_URI,
CalDavBackend::PERSONAL_CALENDAR_NAME,
+ true,
+ true
+ ],
+ [
+ 'principals/users/myuser',
+ 'calendars/myuser',
+ false,
+ CalDavBackend::PERSONAL_CALENDAR_URI,
+ CalDavBackend::PERSONAL_CALENDAR_NAME,
+ false,
+ false,
+ true
+ ],
+ [
+ 'principals/users/myuser',
+ 'calendars/myuser',
+ false,
+ CalDavBackend::PERSONAL_CALENDAR_URI,
+ CalDavBackend::PERSONAL_CALENDAR_NAME,
false
],
[
@@ -201,6 +213,8 @@ class PluginTest extends TestCase {
CalDavBackend::PERSONAL_CALENDAR_NAME,
true,
false,
+ false,
+ false,
],
[
'principals/users/myuser',
@@ -237,18 +251,8 @@ class PluginTest extends TestCase {
];
}
- /**
- * @dataProvider propFindDefaultCalendarUrlProvider
- * @param string $principalUri
- * @param string $calendarHome
- * @param bool $isResource
- * @param string $calendarUri
- * @param string $displayName
- * @param bool $exists
- * @param bool $propertiesForPath
- */
- public function testPropFindDefaultCalendarUrl(string $principalUri, ?string $calendarHome, bool $isResource, string $calendarUri, string $displayName, bool $exists, bool $propertiesForPath = true) {
- /** @var PropFind $propFind */
+ #[\PHPUnit\Framework\Attributes\DataProvider('propFindDefaultCalendarUrlProvider')]
+ public function testPropFindDefaultCalendarUrl(string $principalUri, ?string $calendarHome, bool $isResource, string $calendarUri, string $displayName, bool $exists, bool $deleted = false, bool $hasExistingCalendars = false, bool $propertiesForPath = true): void {
$propFind = new PropFind(
$principalUri,
[
@@ -256,7 +260,7 @@ class PluginTest extends TestCase {
],
0
);
- /** @var IPrincipal|MockObject $node */
+ /** @var IPrincipal&MockObject $node */
$node = $this->getMockBuilder(IPrincipal::class)
->disableOriginalConstructor()
->getMock();
@@ -290,6 +294,7 @@ class PluginTest extends TestCase {
$this->assertNull($propFind->get(Plugin::SCHEDULE_DEFAULT_CALENDAR_URL));
return;
}
+
if (!$isResource) {
$this->config->expects($this->once())
->method('getUserValue')
@@ -303,21 +308,56 @@ class PluginTest extends TestCase {
->with($calendarUri)
->willReturn($exists);
- if (!$exists) {
- $calendarBackend = $this->createMock(CalDavBackend::class);
- $calendarBackend->expects($this->once())
- ->method('createCalendar')
- ->with($principalUri, $calendarUri, [
- '{DAV:}displayname' => $displayName,
- ]);
-
- $calendarHomeObject->expects($this->once())
- ->method('getCalDAVBackend')
- ->with()
- ->willReturn($calendarBackend);
+ if ($exists) {
+ $calendar = $this->createMock(Calendar::class);
+ $calendar->expects($this->once())->method('isDeleted')->willReturn($deleted);
+ $calendarHomeObject->expects($deleted && !$hasExistingCalendars ? $this->exactly(2) : $this->once())->method('getChild')->with($calendarUri)->willReturn($calendar);
}
- /** @var Tree|MockObject $tree */
+ $calendarBackend = $this->createMock(CalDavBackend::class);
+ $calendarUri = $hasExistingCalendars ? 'custom' : $calendarUri;
+ $displayName = $hasExistingCalendars ? 'Custom Calendar' : $displayName;
+
+ $existingCalendars = $hasExistingCalendars ? [
+ new Calendar(
+ $calendarBackend,
+ ['uri' => 'deleted', '{DAV:}displayname' => 'A deleted calendar', TrashbinPlugin::PROPERTY_DELETED_AT => 42],
+ $this->createMock(IL10N::class),
+ $this->config,
+ $this->createMock(LoggerInterface::class)
+ ),
+ new Calendar(
+ $calendarBackend,
+ ['uri' => $calendarUri, '{DAV:}displayname' => $displayName],
+ $this->createMock(IL10N::class),
+ $this->config,
+ $this->createMock(LoggerInterface::class)
+ )
+ ] : [];
+
+ if (!$exists || $deleted) {
+ if (!$hasExistingCalendars) {
+ $calendarBackend->expects($this->once())
+ ->method('createCalendar')
+ ->with($principalUri, $calendarUri, [
+ '{DAV:}displayname' => $displayName,
+ ]);
+
+ $calendarHomeObject->expects($this->exactly($deleted ? 2 : 1))
+ ->method('getCalDAVBackend')
+ ->with()
+ ->willReturn($calendarBackend);
+ }
+
+ if (!$isResource) {
+ $calendarHomeObject->expects($this->once())
+ ->method('getChildren')
+ ->with()
+ ->willReturn($existingCalendars);
+ }
+ }
+
+ /** @var Tree&MockObject $tree */
$tree = $this->createMock(Tree::class);
$tree->expects($this->once())
->method('getNodeForPath')
@@ -331,7 +371,7 @@ class PluginTest extends TestCase {
$this->server->expects($this->once())
->method('getPropertiesForPath')
- ->with($calendarHome .'/' . $calendarUri, [], 1)
+ ->with($calendarHome . '/' . $calendarUri, [], 1)
->willReturn($properties);
$this->plugin->propFindDefaultCalendarUrl($propFind, $node);
@@ -343,6 +383,388 @@ class PluginTest extends TestCase {
/** @var LocalHref $result */
$result = $propFind->get(Plugin::SCHEDULE_DEFAULT_CALENDAR_URL);
- $this->assertEquals('/remote.php/dav/'. $calendarHome . '/' . $calendarUri, $result->getHref());
+ $this->assertEquals('/remote.php/dav/' . $calendarHome . '/' . $calendarUri, $result->getHref());
+ }
+
+ /**
+ * Test Calendar Event Creation for Personal Calendar
+ *
+ * Should generate 2 messages for attendees User 2 and User External
+ */
+ public function testCalendarObjectChangePersonalCalendarCreate(): void {
+
+ // define place holders
+ /** @var Message[] $iTipMessages */
+ $iTipMessages = [];
+ // construct calendar node
+ $calendarNode = new Calendar(
+ $this->createMock(BackendInterface::class),
+ [
+ 'uri' => 'personal',
+ 'principaluri' => 'principals/users/user1',
+ '{DAV:}displayname' => 'Calendar Shared By User1',
+ ],
+ $this->createMock(IL10N::class),
+ $this->config,
+ $this->logger
+ );
+ // construct server request object
+ $request = new Request(
+ 'PUT',
+ '/remote.php/dav/calendars/user1/personal/B0DC78AE-6DD7-47E3-80BE-89F23E6D5383.ics'
+ );
+ $request->setBaseUrl('/remote.php/dav/');
+ // construct server response object
+ $response = new Response();
+ // construct server tree object
+ $tree = $this->createMock(Tree::class);
+ $tree->expects($this->once())
+ ->method('getNodeForPath')
+ ->with('calendars/user1/personal')
+ ->willReturn($calendarNode);
+ // construct server properties and returns
+ $this->server->httpRequest = $request;
+ $this->server->tree = $tree;
+ $this->server->expects($this->exactly(1))->method('getProperties')
+ ->willReturnMap([
+ [
+ 'principals/users/user1',
+ ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'],
+ ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => new LocalHref(
+ ['mailto:user1@testing.local','/remote.php/dav/principals/users/user1/']
+ )]
+ ]
+ ]);
+ $this->server->expects($this->exactly(2))->method('emit')->willReturnCallback(
+ function (string $eventName, array $arguments = [], ?callable $continueCallBack = null) use (&$iTipMessages) {
+ $this->assertEquals('schedule', $eventName);
+ $this->assertCount(1, $arguments);
+ $iTipMessages[] = $arguments[0];
+ return true;
+ }
+ );
+ // construct calendar with a 1 hour event and same start/end time zones
+ $vCalendar = new VCalendar();
+ $vEvent = $vCalendar->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Test Recurring Event');
+ $vEvent->add('ORGANIZER', 'mailto:user1@testing.local', ['CN' => 'User One']);
+ $vEvent->add('ATTENDEE', 'mailto:user2@testing.local', [
+ 'CN' => 'User Two',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ $vEvent->add('ATTENDEE', 'mailto:user@external.local', [
+ 'CN' => 'User External',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ // define flags
+ $newFlag = true;
+ $modifiedFlag = false;
+ // execute method
+ $this->plugin->calendarObjectChange(
+ $request,
+ $response,
+ $vCalendar,
+ 'calendars/user1/personal',
+ $modifiedFlag,
+ $newFlag
+ );
+ // test for correct iTip message count
+ $this->assertCount(2, $iTipMessages);
+ // test for Sharer Attendee
+ $this->assertEquals('mailto:user1@testing.local', $iTipMessages[0]->sender);
+ $this->assertEquals('mailto:user2@testing.local', $iTipMessages[0]->recipient);
+ $this->assertTrue($iTipMessages[0]->significantChange);
+ // test for External Attendee
+ $this->assertEquals('mailto:user1@testing.local', $iTipMessages[1]->sender);
+ $this->assertEquals('mailto:user@external.local', $iTipMessages[1]->recipient);
+ $this->assertTrue($iTipMessages[1]->significantChange);
+
+ }
+
+ /**
+ * Test Calendar Event Creation for Shared Calendar as Sharer/Owner
+ *
+ * Should generate 3 messages for attendees User 2 (Sharee), User 3 (Non-Sharee) and User External
+ */
+ public function testCalendarObjectChangeSharedCalendarSharerCreate(): void {
+
+ // define place holders
+ /** @var Message[] $iTipMessages */
+ $iTipMessages = [];
+ // construct calendar node
+ $calendarNode = new Calendar(
+ $this->createMock(BackendInterface::class),
+ [
+ 'uri' => 'calendar_shared_by_user1',
+ 'principaluri' => 'principals/users/user1',
+ '{DAV:}displayname' => 'Calendar Shared By User1',
+ '{http://owncloud.org/ns}owner-principal' => 'principals/users/user1'
+ ],
+ $this->createMock(IL10N::class),
+ $this->config,
+ $this->logger
+ );
+ // construct server request object
+ $request = new Request(
+ 'PUT',
+ '/remote.php/dav/calendars/user1/calendar_shared_by_user1/B0DC78AE-6DD7-47E3-80BE-89F23E6D5383.ics'
+ );
+ $request->setBaseUrl('/remote.php/dav/');
+ // construct server response object
+ $response = new Response();
+ // construct server tree object
+ $tree = $this->createMock(Tree::class);
+ $tree->expects($this->once())
+ ->method('getNodeForPath')
+ ->with('calendars/user1/calendar_shared_by_user1')
+ ->willReturn($calendarNode);
+ // construct server properties and returns
+ $this->server->httpRequest = $request;
+ $this->server->tree = $tree;
+ $this->server->expects($this->exactly(1))->method('getProperties')
+ ->willReturnMap([
+ [
+ 'principals/users/user1',
+ ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'],
+ ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => new LocalHref(
+ ['mailto:user1@testing.local','/remote.php/dav/principals/users/user1/']
+ )]
+ ]
+ ]);
+ $this->server->expects($this->exactly(3))->method('emit')->willReturnCallback(
+ function (string $eventName, array $arguments = [], ?callable $continueCallBack = null) use (&$iTipMessages) {
+ $this->assertEquals('schedule', $eventName);
+ $this->assertCount(1, $arguments);
+ $iTipMessages[] = $arguments[0];
+ return true;
+ }
+ );
+ // construct calendar with a 1 hour event and same start/end time zones
+ $vCalendar = new VCalendar();
+ $vEvent = $vCalendar->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Test Recurring Event');
+ $vEvent->add('ORGANIZER', 'mailto:user1@testing.local', ['CN' => 'User One']);
+ $vEvent->add('ATTENDEE', 'mailto:user2@testing.local', [
+ 'CN' => 'User Two',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ $vEvent->add('ATTENDEE', 'mailto:user3@testing.local', [
+ 'CN' => 'User Three',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ $vEvent->add('ATTENDEE', 'mailto:user@external.local', [
+ 'CN' => 'User External',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ // define flags
+ $newFlag = true;
+ $modifiedFlag = false;
+ // execute method
+ $this->plugin->calendarObjectChange(
+ $request,
+ $response,
+ $vCalendar,
+ 'calendars/user1/calendar_shared_by_user1',
+ $modifiedFlag,
+ $newFlag
+ );
+ // test for correct iTip message count
+ $this->assertCount(3, $iTipMessages);
+ // test for Sharer Attendee
+ $this->assertEquals('mailto:user1@testing.local', $iTipMessages[0]->sender);
+ $this->assertEquals('mailto:user2@testing.local', $iTipMessages[0]->recipient);
+ $this->assertTrue($iTipMessages[0]->significantChange);
+ // test for Non Shee Attendee
+ $this->assertEquals('mailto:user1@testing.local', $iTipMessages[1]->sender);
+ $this->assertEquals('mailto:user3@testing.local', $iTipMessages[1]->recipient);
+ $this->assertTrue($iTipMessages[1]->significantChange);
+ // test for External Attendee
+ $this->assertEquals('mailto:user1@testing.local', $iTipMessages[2]->sender);
+ $this->assertEquals('mailto:user@external.local', $iTipMessages[2]->recipient);
+ $this->assertTrue($iTipMessages[2]->significantChange);
+
+ }
+
+ /**
+ * Test Calendar Event Creation for Shared Calendar as Shree
+ *
+ * Should generate 3 messages for attendees User 1 (Sharer/Owner), User 3 (Non-Sharee) and User External
+ */
+ public function testCalendarObjectChangeSharedCalendarShreeCreate(): void {
+
+ // define place holders
+ /** @var Message[] $iTipMessages */
+ $iTipMessages = [];
+ // construct calendar node
+ $calendarNode = new Calendar(
+ $this->createMock(BackendInterface::class),
+ [
+ 'uri' => 'calendar_shared_by_user1',
+ 'principaluri' => 'principals/users/user2',
+ '{DAV:}displayname' => 'Calendar Shared By User1',
+ '{http://owncloud.org/ns}owner-principal' => 'principals/users/user1'
+ ],
+ $this->createMock(IL10N::class),
+ $this->config,
+ $this->logger
+ );
+ // construct server request object
+ $request = new Request(
+ 'PUT',
+ '/remote.php/dav/calendars/user2/calendar_shared_by_user1/B0DC78AE-6DD7-47E3-80BE-89F23E6D5383.ics'
+ );
+ $request->setBaseUrl('/remote.php/dav/');
+ // construct server response object
+ $response = new Response();
+ // construct server tree object
+ $tree = $this->createMock(Tree::class);
+ $tree->expects($this->once())
+ ->method('getNodeForPath')
+ ->with('calendars/user2/calendar_shared_by_user1')
+ ->willReturn($calendarNode);
+ // construct server properties and returns
+ $this->server->httpRequest = $request;
+ $this->server->tree = $tree;
+ $this->server->expects($this->exactly(2))->method('getProperties')
+ ->willReturnMap([
+ [
+ 'principals/users/user1',
+ ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'],
+ ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => new LocalHref(
+ ['mailto:user1@testing.local','/remote.php/dav/principals/users/user1/']
+ )]
+ ],
+ [
+ 'principals/users/user2',
+ ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'],
+ ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => new LocalHref(
+ ['mailto:user2@testing.local','/remote.php/dav/principals/users/user2/']
+ )]
+ ]
+ ]);
+ $this->server->expects($this->exactly(3))->method('emit')->willReturnCallback(
+ function (string $eventName, array $arguments = [], ?callable $continueCallBack = null) use (&$iTipMessages) {
+ $this->assertEquals('schedule', $eventName);
+ $this->assertCount(1, $arguments);
+ $iTipMessages[] = $arguments[0];
+ return true;
+ }
+ );
+ // construct calendar with a 1 hour event and same start/end time zones
+ $vCalendar = new VCalendar();
+ $vEvent = $vCalendar->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Test Recurring Event');
+ $vEvent->add('ORGANIZER', 'mailto:user2@testing.local', ['CN' => 'User Two']);
+ $vEvent->add('ATTENDEE', 'mailto:user1@testing.local', [
+ 'CN' => 'User One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ $vEvent->add('ATTENDEE', 'mailto:user3@testing.local', [
+ 'CN' => 'User Three',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ $vEvent->add('ATTENDEE', 'mailto:user@external.local', [
+ 'CN' => 'User External',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ // define flags
+ $newFlag = true;
+ $modifiedFlag = false;
+ // execute method
+ $this->plugin->calendarObjectChange(
+ $request,
+ $response,
+ $vCalendar,
+ 'calendars/user2/calendar_shared_by_user1',
+ $modifiedFlag,
+ $newFlag
+ );
+ // test for correct iTip message count
+ $this->assertCount(3, $iTipMessages);
+ // test for Sharer Attendee
+ $this->assertEquals('mailto:user2@testing.local', $iTipMessages[0]->sender);
+ $this->assertEquals('mailto:user1@testing.local', $iTipMessages[0]->recipient);
+ $this->assertTrue($iTipMessages[0]->significantChange);
+ // test for Non Shee Attendee
+ $this->assertEquals('mailto:user2@testing.local', $iTipMessages[1]->sender);
+ $this->assertEquals('mailto:user3@testing.local', $iTipMessages[1]->recipient);
+ $this->assertTrue($iTipMessages[1]->significantChange);
+ // test for External Attendee
+ $this->assertEquals('mailto:user2@testing.local', $iTipMessages[2]->sender);
+ $this->assertEquals('mailto:user@external.local', $iTipMessages[2]->recipient);
+ $this->assertTrue($iTipMessages[2]->significantChange);
+
+ }
+
+ /**
+ * Test Calendar Event Creation with iTip and iMip disabled
+ *
+ * Should generate 2 messages for attendees User 2 and User External
+ */
+ public function testCalendarObjectChangeWithSchedulingDisabled(): void {
+ // construct server request
+ $request = new Request(
+ 'PUT',
+ '/remote.php/dav/calendars/user1/personal/B0DC78AE-6DD7-47E3-80BE-89F23E6D5383.ics',
+ ['x-nc-scheduling' => 'false']
+ );
+ $request->setBaseUrl('/remote.php/dav/');
+ // construct server response
+ $response = new Response();
+ // construct server tree
+ $tree = $this->createMock(Tree::class);
+ $tree->expects($this->never())
+ ->method('getNodeForPath');
+ // construct server properties and returns
+ $this->server->httpRequest = $request;
+ $this->server->tree = $tree;
+ // construct empty calendar event
+ $vCalendar = new VCalendar();
+ $vEvent = $vCalendar->add('VEVENT', []);
+ // define flags
+ $newFlag = true;
+ $modifiedFlag = false;
+ // execute method
+ $this->plugin->calendarObjectChange(
+ $request,
+ $response,
+ $vCalendar,
+ 'calendars/user1/personal',
+ $modifiedFlag,
+ $newFlag
+ );
}
}
diff --git a/apps/dav/tests/unit/CalDAV/Search/Request/CalendarSearchReportTest.php b/apps/dav/tests/unit/CalDAV/Search/Request/CalendarSearchReportTest.php
index 1d4c34ae84c..02ae504bce0 100644
--- a/apps/dav/tests/unit/CalDAV/Search/Request/CalendarSearchReportTest.php
+++ b/apps/dav/tests/unit/CalDAV/Search/Request/CalendarSearchReportTest.php
@@ -1,26 +1,9 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @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\Tests\unit\CalDAV\Search\Xml\Request;
@@ -29,12 +12,12 @@ use Sabre\Xml\Reader;
use Test\TestCase;
class CalendarSearchReportTest extends TestCase {
- private $elementMap = [
- '{http://nextcloud.com/ns}calendar-search' =>
- 'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport',
+ private array $elementMap = [
+ '{http://nextcloud.com/ns}calendar-search'
+ => 'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport',
];
- public function testFoo() {
+ public function testFoo(): void {
$xml = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:">
@@ -90,7 +73,7 @@ XML;
);
}
- public function testNoLimitOffset() {
+ public function testNoLimitOffset(): void {
$xml = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:">
@@ -131,8 +114,8 @@ XML;
);
}
-
- public function testRequiresCompFilter() {
+
+ public function testRequiresCompFilter(): void {
$this->expectException(\Sabre\DAV\Exception\BadRequest::class);
$this->expectExceptionMessage('{http://nextcloud.com/ns}prop-filter or {http://nextcloud.com/ns}param-filter given without any {http://nextcloud.com/ns}comp-filter');
@@ -158,8 +141,8 @@ XML;
$this->parse($xml);
}
-
- public function testRequiresFilter() {
+
+ public function testRequiresFilter(): void {
$this->expectException(\Sabre\DAV\Exception\BadRequest::class);
$this->expectExceptionMessage('The {http://nextcloud.com/ns}filter element is required for this request');
@@ -176,8 +159,8 @@ XML;
$this->parse($xml);
}
-
- public function testNoSearchTerm() {
+
+ public function testNoSearchTerm(): void {
$this->expectException(\Sabre\DAV\Exception\BadRequest::class);
$this->expectExceptionMessage('{http://nextcloud.com/ns}search-term is required for this request');
@@ -204,8 +187,8 @@ XML;
$this->parse($xml);
}
-
- public function testCompOnly() {
+
+ public function testCompOnly(): void {
$this->expectException(\Sabre\DAV\Exception\BadRequest::class);
$this->expectExceptionMessage('At least one{http://nextcloud.com/ns}prop-filter or {http://nextcloud.com/ns}param-filter is required for this request');
@@ -247,7 +230,7 @@ XML;
);
}
- public function testPropOnly() {
+ public function testPropOnly(): void {
$xml = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:">
@@ -288,7 +271,7 @@ XML;
);
}
- public function testParamOnly() {
+ public function testParamOnly(): void {
$xml = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:">
@@ -332,7 +315,7 @@ XML;
);
}
- private function parse($xml, array $elementMap = []) {
+ private function parse(string $xml, array $elementMap = []): array {
$reader = new Reader();
$reader->elementMap = array_merge($this->elementMap, $elementMap);
$reader->xml($xml);
diff --git a/apps/dav/tests/unit/CalDAV/Search/SearchPluginTest.php b/apps/dav/tests/unit/CalDAV/Search/SearchPluginTest.php
index 81155039015..e576fbae34c 100644
--- a/apps/dav/tests/unit/CalDAV/Search/SearchPluginTest.php
+++ b/apps/dav/tests/unit/CalDAV/Search/SearchPluginTest.php
@@ -1,26 +1,9 @@
<?php
+
+declare(strict_types=1);
/**
- * @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\Tests\unit\CalDAV\Search;
@@ -33,7 +16,7 @@ use Test\TestCase;
class SearchPluginTest extends TestCase {
protected $server;
- /** @var \OCA\DAV\CalDAV\Search\SearchPlugin $plugin */
+ /** @var SearchPlugin $plugin */
protected $plugin;
protected function setUp(): void {
@@ -48,20 +31,20 @@ class SearchPluginTest extends TestCase {
$this->plugin->initialize($this->server);
}
- public function testGetFeatures() {
+ public function testGetFeatures(): void {
$this->assertEquals(['nc-calendar-search'], $this->plugin->getFeatures());
}
- public function testGetName() {
+ public function testGetName(): void {
$this->assertEquals('nc-calendar-search', $this->plugin->getPluginName());
}
- public function testInitialize() {
+ public function testInitialize(): void {
$server = $this->createMock(\Sabre\DAV\Server::class);
$plugin = new SearchPlugin();
- $server->expects($this->at(0))
+ $server->expects($this->once())
->method('on')
->with('report', [$plugin, 'report']);
$server->xml = new Service();
@@ -74,25 +57,25 @@ class SearchPluginTest extends TestCase {
);
}
- public function testReportUnknown() {
+ public function testReportUnknown(): void {
$result = $this->plugin->report('{urn:ietf:params:xml:ns:caldav}calendar-query', 'REPORT', null);
$this->assertEquals($result, null);
$this->assertNotEquals($this->server->transactionType, 'report-nc-calendar-search');
}
- public function testReport() {
+ public function testReport(): void {
$report = $this->createMock(CalendarSearchReport::class);
$report->filters = [];
$calendarHome = $this->createMock(CalendarHome::class);
- $this->server->expects($this->at(0))
+ $this->server->expects($this->once())
->method('getRequestUri')
->with()
->willReturn('/re/quest/u/r/i');
- $this->server->tree->expects($this->at(0))
+ $this->server->tree->expects($this->once())
->method('getNodeForPath')
->with('/re/quest/u/r/i')
->willReturn($calendarHome);
- $this->server->expects($this->at(1))
+ $this->server->expects($this->once())
->method('getHTTPDepth')
->with(2)
->willReturn(2);
@@ -101,14 +84,14 @@ class SearchPluginTest extends TestCase {
->willReturn([
'return' => null
]);
- $calendarHome->expects($this->at(0))
+ $calendarHome->expects($this->once())
->method('calendarSearch')
->willReturn([]);
$this->plugin->report('{http://nextcloud.com/ns}calendar-search', $report, '');
}
- public function testSupportedReportSetNoCalendarHome() {
+ public function testSupportedReportSetNoCalendarHome(): void {
$this->server->tree->expects($this->once())
->method('getNodeForPath')
->with('/foo/bar')
@@ -118,7 +101,7 @@ class SearchPluginTest extends TestCase {
$this->assertEquals([], $reports);
}
- public function testSupportedReportSet() {
+ public function testSupportedReportSet(): void {
$calendarHome = $this->createMock(CalendarHome::class);
$this->server->tree->expects($this->once())
diff --git a/apps/dav/tests/unit/CalDAV/Security/RateLimitingPluginTest.php b/apps/dav/tests/unit/CalDAV/Security/RateLimitingPluginTest.php
new file mode 100644
index 00000000000..a5cf6a23c66
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Security/RateLimitingPluginTest.php
@@ -0,0 +1,188 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CalDAV\Security;
+
+use OC\Security\RateLimiting\Exception\RateLimitExceededException;
+use OC\Security\RateLimiting\Limiter;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\Security\RateLimitingPlugin;
+use OCA\DAV\Connector\Sabre\Exception\TooManyRequests;
+use OCP\IAppConfig;
+use OCP\IUser;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\DAV\Exception\Forbidden;
+use Test\TestCase;
+
+class RateLimitingPluginTest extends TestCase {
+
+ private Limiter&MockObject $limiter;
+ private CalDavBackend&MockObject $caldavBackend;
+ private IUserManager&MockObject $userManager;
+ private LoggerInterface&MockObject $logger;
+ private IAppConfig&MockObject $config;
+ private string $userId = 'user123';
+ private RateLimitingPlugin $plugin;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->limiter = $this->createMock(Limiter::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->caldavBackend = $this->createMock(CalDavBackend::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->config = $this->createMock(IAppConfig::class);
+ $this->plugin = new RateLimitingPlugin(
+ $this->limiter,
+ $this->userManager,
+ $this->caldavBackend,
+ $this->logger,
+ $this->config,
+ $this->userId,
+ );
+ }
+
+ public function testNoUserObject(): void {
+ $this->limiter->expects(self::never())
+ ->method('registerUserRequest');
+
+ $this->plugin->beforeBind('calendars/foo/cal');
+ }
+
+ public function testUnrelated(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with($this->userId)
+ ->willReturn($user);
+ $this->limiter->expects(self::never())
+ ->method('registerUserRequest');
+
+ $this->plugin->beforeBind('foo/bar');
+ }
+
+ public function testRegisterCalendarCreation(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with($this->userId)
+ ->willReturn($user);
+ $this->config
+ ->method('getValueInt')
+ ->with('dav')
+ ->willReturnArgument(2);
+ $this->limiter->expects(self::once())
+ ->method('registerUserRequest')
+ ->with(
+ 'caldav-create-calendar',
+ 10,
+ 3600,
+ $user,
+ );
+
+ $this->plugin->beforeBind('calendars/foo/cal');
+ }
+
+ public function testCalendarCreationRateLimitExceeded(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with($this->userId)
+ ->willReturn($user);
+ $this->config
+ ->method('getValueInt')
+ ->with('dav')
+ ->willReturnArgument(2);
+ $this->limiter->expects(self::once())
+ ->method('registerUserRequest')
+ ->with(
+ 'caldav-create-calendar',
+ 10,
+ 3600,
+ $user,
+ )
+ ->willThrowException(new RateLimitExceededException());
+ $this->expectException(TooManyRequests::class);
+
+ $this->plugin->beforeBind('calendars/foo/cal');
+ }
+
+ public function testCalendarLimitReached(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with($this->userId)
+ ->willReturn($user);
+ $user->method('getUID')->willReturn('user123');
+ $this->config
+ ->method('getValueInt')
+ ->with('dav')
+ ->willReturnArgument(2);
+ $this->limiter->expects(self::once())
+ ->method('registerUserRequest')
+ ->with(
+ 'caldav-create-calendar',
+ 10,
+ 3600,
+ $user,
+ );
+ $this->caldavBackend->expects(self::once())
+ ->method('getCalendarsForUserCount')
+ ->with('principals/users/user123')
+ ->willReturn(27);
+ $this->caldavBackend->expects(self::once())
+ ->method('getSubscriptionsForUserCount')
+ ->with('principals/users/user123')
+ ->willReturn(3);
+ $this->expectException(Forbidden::class);
+
+ $this->plugin->beforeBind('calendars/foo/cal');
+ }
+
+ public function testNoCalendarsSubscriptsLimit(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with($this->userId)
+ ->willReturn($user);
+ $user->method('getUID')->willReturn('user123');
+ $this->config
+ ->method('getValueInt')
+ ->with('dav')
+ ->willReturnCallback(function ($app, $key, $default) {
+ switch ($key) {
+ case 'maximumCalendarsSubscriptions':
+ return -1;
+ default:
+ return $default;
+ }
+ });
+ $this->limiter->expects(self::once())
+ ->method('registerUserRequest')
+ ->with(
+ 'caldav-create-calendar',
+ 10,
+ 3600,
+ $user,
+ );
+ $this->caldavBackend->expects(self::never())
+ ->method('getCalendarsForUserCount')
+ ->with('principals/users/user123')
+ ->willReturn(27);
+ $this->caldavBackend->expects(self::never())
+ ->method('getSubscriptionsForUserCount')
+ ->with('principals/users/user123')
+ ->willReturn(3);
+
+ $this->plugin->beforeBind('calendars/foo/cal');
+ }
+
+}
diff --git a/apps/dav/tests/unit/CalDAV/Status/StatusServiceTest.php b/apps/dav/tests/unit/CalDAV/Status/StatusServiceTest.php
new file mode 100644
index 00000000000..ee0ef2334ec
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Status/StatusServiceTest.php
@@ -0,0 +1,445 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\Status;
+
+use OC\Calendar\CalendarQuery;
+use OCA\DAV\CalDAV\CalendarImpl;
+use OCA\DAV\CalDAV\Status\StatusService;
+use OCA\UserStatus\Db\UserStatus;
+use OCA\UserStatus\Service\StatusService as UserStatusService;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Calendar\IManager;
+use OCP\ICache;
+use OCP\ICacheFactory;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\User\IAvailabilityCoordinator;
+use OCP\User\IOutOfOfficeData;
+use OCP\UserStatus\IUserStatus;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class StatusServiceTest extends TestCase {
+ private ITimeFactory&MockObject $timeFactory;
+ private IManager&MockObject $calendarManager;
+ private IUserManager&MockObject $userManager;
+ private UserStatusService&MockObject $userStatusService;
+ private IAvailabilityCoordinator&MockObject $availabilityCoordinator;
+ private ICacheFactory&MockObject $cacheFactory;
+ private LoggerInterface&MockObject $logger;
+ private ICache&MockObject $cache;
+ private StatusService $service;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->calendarManager = $this->createMock(IManager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->userStatusService = $this->createMock(UserStatusService::class);
+ $this->availabilityCoordinator = $this->createMock(IAvailabilityCoordinator::class);
+ $this->cacheFactory = $this->createMock(ICacheFactory::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->cache = $this->createMock(ICache::class);
+ $this->cacheFactory->expects(self::once())
+ ->method('createLocal')
+ ->with('CalendarStatusService')
+ ->willReturn($this->cache);
+
+ $this->service = new StatusService($this->timeFactory,
+ $this->calendarManager,
+ $this->userManager,
+ $this->userStatusService,
+ $this->availabilityCoordinator,
+ $this->cacheFactory,
+ $this->logger,
+ );
+ }
+
+ public function testNoUser(): void {
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn(null);
+ $this->availabilityCoordinator->expects(self::never())
+ ->method('getCurrentOutOfOfficeData');
+ $this->availabilityCoordinator->expects(self::never())
+ ->method('isInEffect');
+ $this->logger->expects(self::never())
+ ->method('debug');
+ $this->cache->expects(self::never())
+ ->method('get');
+ $this->cache->expects(self::never())
+ ->method('set');
+ $this->calendarManager->expects(self::never())
+ ->method('getCalendarsForPrincipal');
+ $this->calendarManager->expects(self::never())
+ ->method('newQuery');
+ $this->timeFactory->expects(self::never())
+ ->method('getDateTime');
+ $this->calendarManager->expects(self::never())
+ ->method('searchForPrincipal');
+ $this->userStatusService->expects(self::never())
+ ->method('revertUserStatus');
+ $this->userStatusService->expects(self::never())
+ ->method('setUserStatus');
+ $this->userStatusService->expects(self::never())
+ ->method('findByUserId');
+
+ $this->service->processCalendarStatus('admin');
+ }
+
+ public function testOOOInEffect(): void {
+ $user = $this->createConfiguredMock(IUser::class, [
+ 'getUID' => 'admin',
+ ]);
+
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn($user);
+ $this->availabilityCoordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->willReturn($this->createMock(IOutOfOfficeData::class));
+ $this->availabilityCoordinator->expects(self::once())
+ ->method('isInEffect')
+ ->willReturn(true);
+ $this->logger->expects(self::once())
+ ->method('debug');
+ $this->cache->expects(self::never())
+ ->method('get');
+ $this->cache->expects(self::never())
+ ->method('set');
+ $this->calendarManager->expects(self::never())
+ ->method('getCalendarsForPrincipal');
+ $this->calendarManager->expects(self::never())
+ ->method('newQuery');
+ $this->timeFactory->expects(self::never())
+ ->method('getDateTime');
+ $this->calendarManager->expects(self::never())
+ ->method('searchForPrincipal');
+ $this->userStatusService->expects(self::never())
+ ->method('revertUserStatus');
+ $this->userStatusService->expects(self::never())
+ ->method('setUserStatus');
+ $this->userStatusService->expects(self::never())
+ ->method('findByUserId');
+
+ $this->service->processCalendarStatus('admin');
+ }
+
+ public function testNoCalendars(): void {
+ $user = $this->createConfiguredMock(IUser::class, [
+ 'getUID' => 'admin',
+ ]);
+
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn($user);
+ $this->availabilityCoordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->willReturn(null);
+ $this->availabilityCoordinator->expects(self::never())
+ ->method('isInEffect');
+ $this->cache->expects(self::once())
+ ->method('get')
+ ->willReturn(null);
+ $this->cache->expects(self::once())
+ ->method('set');
+ $this->calendarManager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->willReturn([]);
+ $this->calendarManager->expects(self::never())
+ ->method('newQuery');
+ $this->timeFactory->expects(self::never())
+ ->method('getDateTime');
+ $this->calendarManager->expects(self::never())
+ ->method('searchForPrincipal');
+ $this->userStatusService->expects(self::once())
+ ->method('revertUserStatus');
+ $this->logger->expects(self::once())
+ ->method('debug');
+ $this->userStatusService->expects(self::never())
+ ->method('setUserStatus');
+ $this->userStatusService->expects(self::never())
+ ->method('findByUserId');
+
+ $this->service->processCalendarStatus('admin');
+ }
+
+ public function testNoCalendarEvents(): void {
+ $user = $this->createConfiguredMock(IUser::class, [
+ 'getUID' => 'admin',
+ ]);
+
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn($user);
+ $this->availabilityCoordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->willReturn(null);
+ $this->availabilityCoordinator->expects(self::never())
+ ->method('isInEffect');
+ $this->cache->expects(self::once())
+ ->method('get')
+ ->willReturn(null);
+ $this->cache->expects(self::once())
+ ->method('set');
+ $this->calendarManager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->willReturn([$this->createMock(CalendarImpl::class)]);
+ $this->calendarManager->expects(self::once())
+ ->method('newQuery')
+ ->willReturn(new CalendarQuery('admin'));
+ $this->timeFactory->expects(self::exactly(2))
+ ->method('getDateTime')
+ ->willReturn(new \DateTime());
+ $this->calendarManager->expects(self::once())
+ ->method('searchForPrincipal')
+ ->willReturn([]);
+ $this->userStatusService->expects(self::once())
+ ->method('revertUserStatus');
+ $this->logger->expects(self::once())
+ ->method('debug');
+ $this->userStatusService->expects(self::never())
+ ->method('setUserStatus');
+ $this->userStatusService->expects(self::never())
+ ->method('findByUserId');
+
+ $this->service->processCalendarStatus('admin');
+ }
+
+ public function testCalendarNoEventObjects(): void {
+ $user = $this->createConfiguredMock(IUser::class, [
+ 'getUID' => 'admin',
+ ]);
+
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn($user);
+ $this->availabilityCoordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->willReturn(null);
+ $this->availabilityCoordinator->expects(self::never())
+ ->method('isInEffect');
+ $this->cache->expects(self::once())
+ ->method('get')
+ ->willReturn(null);
+ $this->cache->expects(self::once())
+ ->method('set');
+ $this->calendarManager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->willReturn([$this->createMock(CalendarImpl::class)]);
+ $this->calendarManager->expects(self::once())
+ ->method('newQuery')
+ ->willReturn(new CalendarQuery('admin'));
+ $this->timeFactory->expects(self::exactly(2))
+ ->method('getDateTime')
+ ->willReturn(new \DateTime());
+ $this->userStatusService->expects(self::once())
+ ->method('findByUserId')
+ ->willThrowException(new DoesNotExistException(''));
+ $this->calendarManager->expects(self::once())
+ ->method('searchForPrincipal')
+ ->willReturn([['objects' => []]]);
+ $this->userStatusService->expects(self::once())
+ ->method('revertUserStatus');
+ $this->logger->expects(self::once())
+ ->method('debug');
+ $this->userStatusService->expects(self::never())
+ ->method('setUserStatus');
+
+
+ $this->service->processCalendarStatus('admin');
+ }
+
+ public function testCalendarEvent(): void {
+ $user = $this->createConfiguredMock(IUser::class, [
+ 'getUID' => 'admin',
+ ]);
+
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn($user);
+ $this->availabilityCoordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->willReturn(null);
+ $this->availabilityCoordinator->expects(self::never())
+ ->method('isInEffect');
+ $this->cache->expects(self::once())
+ ->method('get')
+ ->willReturn(null);
+ $this->cache->expects(self::once())
+ ->method('set');
+ $this->calendarManager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->willReturn([$this->createMock(CalendarImpl::class)]);
+ $this->calendarManager->expects(self::once())
+ ->method('newQuery')
+ ->willReturn(new CalendarQuery('admin'));
+ $this->timeFactory->expects(self::exactly(2))
+ ->method('getDateTime')
+ ->willReturn(new \DateTime());
+ $this->userStatusService->expects(self::once())
+ ->method('findByUserId')
+ ->willThrowException(new DoesNotExistException(''));
+ $this->calendarManager->expects(self::once())
+ ->method('searchForPrincipal')
+ ->willReturn([['objects' => [[]]]]);
+ $this->userStatusService->expects(self::never())
+ ->method('revertUserStatus');
+ $this->logger->expects(self::once())
+ ->method('debug');
+ $this->userStatusService->expects(self::once())
+ ->method('setUserStatus');
+
+
+ $this->service->processCalendarStatus('admin');
+ }
+
+ public function testCallStatus(): void {
+ $user = $this->createConfiguredMock(IUser::class, [
+ 'getUID' => 'admin',
+ ]);
+
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn($user);
+ $this->availabilityCoordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->willReturn(null);
+ $this->availabilityCoordinator->expects(self::never())
+ ->method('isInEffect');
+ $this->cache->expects(self::once())
+ ->method('get')
+ ->willReturn(null);
+ $this->cache->expects(self::once())
+ ->method('set');
+ $this->calendarManager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->willReturn([$this->createMock(CalendarImpl::class)]);
+ $this->calendarManager->expects(self::once())
+ ->method('newQuery')
+ ->willReturn(new CalendarQuery('admin'));
+ $this->timeFactory->expects(self::exactly(2))
+ ->method('getDateTime')
+ ->willReturn(new \DateTime());
+ $this->calendarManager->expects(self::once())
+ ->method('searchForPrincipal')
+ ->willReturn([['objects' => [[]]]]);
+ $userStatus = new UserStatus();
+ $userStatus->setMessageId(IUserStatus::MESSAGE_CALL);
+ $userStatus->setStatusTimestamp(123456);
+ $this->userStatusService->expects(self::once())
+ ->method('findByUserId')
+ ->willReturn($userStatus);
+ $this->logger->expects(self::once())
+ ->method('debug');
+ $this->userStatusService->expects(self::never())
+ ->method('revertUserStatus');
+ $this->userStatusService->expects(self::never())
+ ->method('setUserStatus');
+
+
+ $this->service->processCalendarStatus('admin');
+ }
+
+ public function testInvisibleStatus(): void {
+ $user = $this->createConfiguredMock(IUser::class, [
+ 'getUID' => 'admin',
+ ]);
+
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn($user);
+ $this->availabilityCoordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->willReturn(null);
+ $this->availabilityCoordinator->expects(self::never())
+ ->method('isInEffect');
+ $this->cache->expects(self::once())
+ ->method('get')
+ ->willReturn(null);
+ $this->cache->expects(self::once())
+ ->method('set');
+ $this->calendarManager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->willReturn([$this->createMock(CalendarImpl::class)]);
+ $this->calendarManager->expects(self::once())
+ ->method('newQuery')
+ ->willReturn(new CalendarQuery('admin'));
+ $this->timeFactory->expects(self::exactly(2))
+ ->method('getDateTime')
+ ->willReturn(new \DateTime());
+ $this->calendarManager->expects(self::once())
+ ->method('searchForPrincipal')
+ ->willReturn([['objects' => [[]]]]);
+ $userStatus = new UserStatus();
+ $userStatus->setStatus(IUserStatus::INVISIBLE);
+ $userStatus->setStatusTimestamp(123456);
+ $this->userStatusService->expects(self::once())
+ ->method('findByUserId')
+ ->willReturn($userStatus);
+ $this->logger->expects(self::once())
+ ->method('debug');
+ $this->userStatusService->expects(self::never())
+ ->method('revertUserStatus');
+ $this->userStatusService->expects(self::never())
+ ->method('setUserStatus');
+
+
+ $this->service->processCalendarStatus('admin');
+ }
+
+ public function testDNDStatus(): void {
+ $user = $this->createConfiguredMock(IUser::class, [
+ 'getUID' => 'admin',
+ ]);
+
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->willReturn($user);
+ $this->availabilityCoordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->willReturn(null);
+ $this->availabilityCoordinator->expects(self::never())
+ ->method('isInEffect');
+ $this->cache->expects(self::once())
+ ->method('get')
+ ->willReturn(null);
+ $this->cache->expects(self::once())
+ ->method('set');
+ $this->calendarManager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->willReturn([$this->createMock(CalendarImpl::class)]);
+ $this->calendarManager->expects(self::once())
+ ->method('newQuery')
+ ->willReturn(new CalendarQuery('admin'));
+ $this->timeFactory->expects(self::exactly(2))
+ ->method('getDateTime')
+ ->willReturn(new \DateTime());
+ $this->calendarManager->expects(self::once())
+ ->method('searchForPrincipal')
+ ->willReturn([['objects' => [[]]]]);
+ $userStatus = new UserStatus();
+ $userStatus->setStatus(IUserStatus::DND);
+ $userStatus->setStatusTimestamp(123456);
+ $this->userStatusService->expects(self::once())
+ ->method('findByUserId')
+ ->willReturn($userStatus);
+ $this->logger->expects(self::once())
+ ->method('debug');
+ $this->userStatusService->expects(self::never())
+ ->method('revertUserStatus');
+ $this->userStatusService->expects(self::never())
+ ->method('setUserStatus');
+
+
+ $this->service->processCalendarStatus('admin');
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/TimeZoneFactoryTest.php b/apps/dav/tests/unit/CalDAV/TimeZoneFactoryTest.php
new file mode 100644
index 00000000000..2d6d0e86358
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/TimeZoneFactoryTest.php
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use DateTimeZone;
+use OCA\DAV\CalDAV\TimeZoneFactory;
+use Test\TestCase;
+
+class TimeZoneFactoryTest extends TestCase {
+
+ private TimeZoneFactory $factory;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->factory = new TimeZoneFactory();
+ }
+
+ public function testIsMS(): void {
+ // test Microsoft time zone
+ $this->assertTrue(TimeZoneFactory::isMS('Eastern Standard Time'));
+ // test IANA time zone
+ $this->assertFalse(TimeZoneFactory::isMS('America/Toronto'));
+ // test Fake time zone
+ $this->assertFalse(TimeZoneFactory::isMS('Fake Eastern Time'));
+ }
+
+ public function testToIana(): void {
+ // test Microsoft time zone
+ $this->assertEquals('America/Toronto', TimeZoneFactory::toIANA('Eastern Standard Time'));
+ // test IANA time zone
+ $this->assertEquals(null, TimeZoneFactory::toIANA('America/Toronto'));
+ // test Fake time zone
+ $this->assertEquals(null, TimeZoneFactory::toIANA('Fake Eastern Time'));
+ }
+
+ public function testFromName(): void {
+ // test Microsoft time zone
+ $this->assertEquals(new DateTimeZone('America/Toronto'), $this->factory->fromName('Eastern Standard Time'));
+ // test IANA time zone
+ $this->assertEquals(new DateTimeZone('America/Toronto'), $this->factory->fromName('America/Toronto'));
+ // test Fake time zone
+ $this->assertEquals(null, $this->factory->fromName('Fake Eastern Time'));
+ }
+
+}
diff --git a/apps/dav/tests/unit/CalDAV/TimezoneServiceTest.php b/apps/dav/tests/unit/CalDAV/TimezoneServiceTest.php
new file mode 100644
index 00000000000..5bb87be67c1
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/TimezoneServiceTest.php
@@ -0,0 +1,142 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use DateTimeZone;
+use OCA\DAV\CalDAV\CalendarImpl;
+use OCA\DAV\CalDAV\TimezoneService;
+use OCA\DAV\Db\Property;
+use OCA\DAV\Db\PropertyMapper;
+use OCP\Calendar\ICalendar;
+use OCP\Calendar\IManager;
+use OCP\IConfig;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\VObject\Component\VTimeZone;
+use Test\TestCase;
+
+class TimezoneServiceTest extends TestCase {
+ private IConfig&MockObject $config;
+ private PropertyMapper&MockObject $propertyMapper;
+ private IManager&MockObject $calendarManager;
+ private TimezoneService $service;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->config = $this->createMock(IConfig::class);
+ $this->propertyMapper = $this->createMock(PropertyMapper::class);
+ $this->calendarManager = $this->createMock(IManager::class);
+
+ $this->service = new TimezoneService(
+ $this->config,
+ $this->propertyMapper,
+ $this->calendarManager,
+ );
+ }
+
+ public function testGetUserTimezoneFromSettings(): void {
+ $this->config->expects(self::once())
+ ->method('getUserValue')
+ ->with('test123', 'core', 'timezone', '')
+ ->willReturn('Europe/Warsaw');
+
+ $timezone = $this->service->getUserTimezone('test123');
+
+ self::assertSame('Europe/Warsaw', $timezone);
+ }
+
+ public function testGetUserTimezoneFromAvailability(): void {
+ $this->config->expects(self::once())
+ ->method('getUserValue')
+ ->with('test123', 'core', 'timezone', '')
+ ->willReturn('');
+ $property = new Property();
+ $property->setPropertyvalue('BEGIN:VCALENDAR
+PRODID:Nextcloud DAV app
+BEGIN:VTIMEZONE
+TZID:Europe/Vienna
+END:VTIMEZONE
+END:VCALENDAR');
+ $this->propertyMapper->expects(self::once())
+ ->method('findPropertyByPathAndName')
+ ->willReturn([
+ $property,
+ ]);
+
+ $timezone = $this->service->getUserTimezone('test123');
+
+ self::assertNotNull($timezone);
+ self::assertEquals('Europe/Vienna', $timezone);
+ }
+
+ public function testGetUserTimezoneFromPersonalCalendar(): void {
+ $this->config->expects(self::exactly(2))
+ ->method('getUserValue')
+ ->willReturnMap([
+ ['test123', 'core', 'timezone', '', ''],
+ ['test123', 'dav', 'defaultCalendar', '', 'personal-1'],
+ ]);
+ $other = $this->createMock(ICalendar::class);
+ $other->method('getUri')->willReturn('other');
+ $personal = $this->createMock(CalendarImpl::class);
+ $personal->method('getUri')->willReturn('personal-1');
+ $tz = new DateTimeZone('Europe/Berlin');
+ $vtz = $this->createMock(VTimeZone::class);
+ $vtz->method('getTimeZone')->willReturn($tz);
+ $personal->method('getSchedulingTimezone')->willReturn($vtz);
+ $this->calendarManager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->with('principals/users/test123')
+ ->willReturn([
+ $other,
+ $personal,
+ ]);
+
+ $timezone = $this->service->getUserTimezone('test123');
+
+ self::assertNotNull($timezone);
+ self::assertEquals('Europe/Berlin', $timezone);
+ }
+
+ public function testGetUserTimezoneFromAny(): void {
+ $this->config->expects(self::exactly(2))
+ ->method('getUserValue')
+ ->willReturnMap([
+ ['test123', 'core', 'timezone', '', ''],
+ ['test123', 'dav', 'defaultCalendar', '', 'personal-1'],
+ ]);
+ $other = $this->createMock(ICalendar::class);
+ $other->method('getUri')->willReturn('other');
+ $personal = $this->createMock(CalendarImpl::class);
+ $personal->method('getUri')->willReturn('personal-2');
+ $tz = new DateTimeZone('Europe/Prague');
+ $vtz = $this->createMock(VTimeZone::class);
+ $vtz->method('getTimeZone')->willReturn($tz);
+ $personal->method('getSchedulingTimezone')->willReturn($vtz);
+ $this->calendarManager->expects(self::once())
+ ->method('getCalendarsForPrincipal')
+ ->with('principals/users/test123')
+ ->willReturn([
+ $other,
+ $personal,
+ ]);
+
+ $timezone = $this->service->getUserTimezone('test123');
+
+ self::assertNotNull($timezone);
+ self::assertEquals('Europe/Prague', $timezone);
+ }
+
+ public function testGetUserTimezoneNoneFound(): void {
+ $timezone = $this->service->getUserTimezone('test123');
+
+ self::assertNull($timezone);
+ }
+
+}
diff --git a/apps/dav/tests/unit/CalDAV/TipBrokerTest.php b/apps/dav/tests/unit/CalDAV/TipBrokerTest.php
new file mode 100644
index 00000000000..ddf992767d6
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/TipBrokerTest.php
@@ -0,0 +1,180 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use OCA\DAV\CalDAV\TipBroker;
+use Sabre\VObject\Component\VCalendar;
+use Test\TestCase;
+
+class TipBrokerTest extends TestCase {
+
+ private TipBroker $broker;
+ private VCalendar $vCalendar1a;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->broker = new TipBroker();
+ // construct calendar with a 1 hour event and same start/end time zones
+ $this->vCalendar1a = new VCalendar();
+ /** @var VEvent $vEvent */
+ $vEvent = $this->vCalendar1a->add('VEVENT', []);
+ $vEvent->add('UID', '96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTAMP', '20240701T000000Z');
+ $vEvent->add('CREATED', '20240701T000000Z');
+ $vEvent->add('LAST-MODIFIED', '20240701T000000Z');
+ $vEvent->add('SEQUENCE', '1');
+ $vEvent->add('STATUS', 'CONFIRMED');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Test Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ }
+
+ public function testParseEventForOrganizerOnCreate(): void {
+
+ // construct calendar and generate event info for newly created event with one attendee
+ $calendar = clone $this->vCalendar1a;
+ $previousEventInfo = [
+ 'organizer' => null,
+ 'significantChangeHash' => '',
+ 'attendees' => [],
+ ];
+ $currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ // test iTip generation
+ $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
+ $this->assertCount(1, $messages);
+ $this->assertEquals('REQUEST', $messages[0]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
+ $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
+
+ }
+
+ public function testParseEventForOrganizerOnModify(): void {
+
+ // construct calendar and generate event info for modified event with one attendee
+ $calendar = clone $this->vCalendar1a;
+ $previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ $calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
+ $calendar->VEVENT->SEQUENCE->setValue(2);
+ $calendar->VEVENT->SUMMARY->setValue('Test Event Modified');
+ $currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ // test iTip generation
+ $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
+ $this->assertCount(1, $messages);
+ $this->assertEquals('REQUEST', $messages[0]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
+ $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
+
+ }
+
+ public function testParseEventForOrganizerOnDelete(): void {
+
+ // construct calendar and generate event info for modified event with one attendee
+ $calendar = clone $this->vCalendar1a;
+ $previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ $currentEventInfo = $previousEventInfo;
+ $currentEventInfo['attendees'] = [];
+ ++$currentEventInfo['sequence'];
+ // test iTip generation
+ $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
+ $this->assertCount(1, $messages);
+ $this->assertEquals('CANCEL', $messages[0]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
+ $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
+
+ }
+
+ public function testParseEventForOrganizerOnStatusCancelled(): void {
+
+ // construct calendar and generate event info for modified event with one attendee
+ $calendar = clone $this->vCalendar1a;
+ $previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ $calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
+ $calendar->VEVENT->SEQUENCE->setValue(2);
+ $calendar->VEVENT->STATUS->setValue('CANCELLED');
+ $currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ // test iTip generation
+ $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
+ $this->assertCount(1, $messages);
+ $this->assertEquals('CANCEL', $messages[0]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
+ $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
+
+ }
+
+ public function testParseEventForOrganizerOnAddAttendee(): void {
+
+ // construct calendar and generate event info for modified event with two attendees
+ $calendar = clone $this->vCalendar1a;
+ $previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ $calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
+ $calendar->VEVENT->SEQUENCE->setValue(2);
+ $calendar->VEVENT->add('ATTENDEE', 'mailto:attendee2@testing.com', [
+ 'CN' => 'Attendee Two',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ $currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ // test iTip generation
+ $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
+ $this->assertCount(2, $messages);
+ $this->assertEquals('REQUEST', $messages[0]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
+ $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
+ $this->assertEquals('REQUEST', $messages[1]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[1]->sender);
+ $this->assertEquals($calendar->VEVENT->ATTENDEE[1]->getValue(), $messages[1]->recipient);
+
+ }
+
+ public function testParseEventForOrganizerOnRemoveAttendee(): void {
+
+ // construct calendar and generate event info for modified event with two attendees
+ $calendar = clone $this->vCalendar1a;
+ $calendar->VEVENT->add('ATTENDEE', 'mailto:attendee2@testing.com', [
+ 'CN' => 'Attendee Two',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ $previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ $calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
+ $calendar->VEVENT->SEQUENCE->setValue(2);
+ $calendar->VEVENT->remove('ATTENDEE');
+ $calendar->VEVENT->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ $currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ // test iTip generation
+ $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
+ $this->assertCount(2, $messages);
+ $this->assertEquals('REQUEST', $messages[0]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
+ $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
+ $this->assertEquals('CANCEL', $messages[1]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[1]->sender);
+ $this->assertEquals('mailto:attendee2@testing.com', $messages[1]->recipient);
+
+ }
+
+}
diff --git a/apps/dav/tests/unit/CalDAV/Validation/CalDavValidatePluginTest.php b/apps/dav/tests/unit/CalDAV/Validation/CalDavValidatePluginTest.php
new file mode 100644
index 00000000000..74fb4b5e94e
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Validation/CalDavValidatePluginTest.php
@@ -0,0 +1,73 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CalDAV\Validation;
+
+use OCA\DAV\CalDAV\Validation\CalDavValidatePlugin;
+use OCP\IAppConfig;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Test\TestCase;
+
+class CalDavValidatePluginTest extends TestCase {
+ private IAppConfig&MockObject $config;
+ private RequestInterface&MockObject $request;
+ private ResponseInterface&MockObject $response;
+
+ private CalDavValidatePlugin $plugin;
+
+ protected function setUp(): void {
+ parent::setUp();
+ // construct mock objects
+ $this->config = $this->createMock(IAppConfig::class);
+ $this->request = $this->createMock(RequestInterface::class);
+ $this->response = $this->createMock(ResponseInterface::class);
+ $this->plugin = new CalDavValidatePlugin(
+ $this->config,
+ );
+ }
+
+ public function testPutSizeLessThenLimit(): void {
+
+ // construct method responses
+ $this->config
+ ->method('getValueInt')
+ ->with('dav', 'event_size_limit', 10485760)
+ ->willReturn(10485760);
+ $this->request
+ ->method('getRawServerValue')
+ ->with('CONTENT_LENGTH')
+ ->willReturn('1024');
+ // test condition
+ $this->assertTrue(
+ $this->plugin->beforePut($this->request, $this->response)
+ );
+
+ }
+
+ public function testPutSizeMoreThenLimit(): void {
+
+ // construct method responses
+ $this->config
+ ->method('getValueInt')
+ ->with('dav', 'event_size_limit', 10485760)
+ ->willReturn(10485760);
+ $this->request
+ ->method('getRawServerValue')
+ ->with('CONTENT_LENGTH')
+ ->willReturn('16242880');
+ $this->expectException(Forbidden::class);
+ // test condition
+ $this->plugin->beforePut($this->request, $this->response);
+
+ }
+
+}
diff --git a/apps/dav/tests/unit/CalDAV/WebcalCaching/ConnectionTest.php b/apps/dav/tests/unit/CalDAV/WebcalCaching/ConnectionTest.php
new file mode 100644
index 00000000000..c29415ecef3
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/WebcalCaching/ConnectionTest.php
@@ -0,0 +1,176 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV\WebcalCaching;
+
+use OCA\DAV\CalDAV\WebcalCaching\Connection;
+use OCP\Http\Client\IClient;
+use OCP\Http\Client\IClientService;
+use OCP\Http\Client\IResponse;
+use OCP\Http\Client\LocalServerException;
+use OCP\IAppConfig;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+
+use Test\TestCase;
+
+class ConnectionTest extends TestCase {
+
+ private IClientService&MockObject $clientService;
+ private IAppConfig&MockObject $config;
+ private LoggerInterface&MockObject $logger;
+ private Connection $connection;
+
+ public function setUp(): void {
+ $this->clientService = $this->createMock(IClientService::class);
+ $this->config = $this->createMock(IAppConfig::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->connection = new Connection($this->clientService, $this->config, $this->logger);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('runLocalURLDataProvider')]
+ public function testLocalUrl($source): void {
+ $subscription = [
+ 'id' => 42,
+ 'uri' => 'sub123',
+ 'refreshreate' => 'P1H',
+ 'striptodos' => 1,
+ 'stripalarms' => 1,
+ 'stripattachments' => 1,
+ 'source' => $source,
+ 'lastmodified' => 0,
+ ];
+
+ $client = $this->createMock(IClient::class);
+ $this->clientService->expects(self::once())
+ ->method('newClient')
+ ->with()
+ ->willReturn($client);
+
+ $this->config->expects(self::once())
+ ->method('getValueString')
+ ->with('dav', 'webcalAllowLocalAccess', 'no')
+ ->willReturn('no');
+
+ $localServerException = new LocalServerException();
+ $client->expects(self::once())
+ ->method('get')
+ ->willThrowException($localServerException);
+ $this->logger->expects(self::once())
+ ->method('warning')
+ ->with('Subscription 42 was not refreshed because it violates local access rules', ['exception' => $localServerException]);
+
+ $this->connection->queryWebcalFeed($subscription);
+ }
+
+ public function testInvalidUrl(): void {
+ $subscription = [
+ 'id' => 42,
+ 'uri' => 'sub123',
+ 'refreshreate' => 'P1H',
+ 'striptodos' => 1,
+ 'stripalarms' => 1,
+ 'stripattachments' => 1,
+ 'source' => '!@#$',
+ 'lastmodified' => 0,
+ ];
+
+ $client = $this->createMock(IClient::class);
+ $this->config->expects(self::never())
+ ->method('getValueString');
+ $client->expects(self::never())
+ ->method('get');
+
+ $this->connection->queryWebcalFeed($subscription);
+
+ }
+
+ /**
+ * @param string $result
+ * @param string $contentType
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('urlDataProvider')]
+ public function testConnection(string $url, string $result, string $contentType): void {
+ $client = $this->createMock(IClient::class);
+ $response = $this->createMock(IResponse::class);
+ $subscription = [
+ 'id' => 42,
+ 'uri' => 'sub123',
+ 'refreshreate' => 'P1H',
+ 'striptodos' => 1,
+ 'stripalarms' => 1,
+ 'stripattachments' => 1,
+ 'source' => $url,
+ 'lastmodified' => 0,
+ ];
+
+ $this->clientService->expects($this->once())
+ ->method('newClient')
+ ->with()
+ ->willReturn($client);
+
+ $this->config->expects($this->once())
+ ->method('getValueString')
+ ->with('dav', 'webcalAllowLocalAccess', 'no')
+ ->willReturn('no');
+
+ $client->expects($this->once())
+ ->method('get')
+ ->with('https://foo.bar/bla2')
+ ->willReturn($response);
+
+ $response->expects($this->once())
+ ->method('getBody')
+ ->with()
+ ->willReturn($result);
+ $response->expects($this->once())
+ ->method('getHeader')
+ ->with('Content-Type')
+ ->willReturn($contentType);
+
+ $this->connection->queryWebcalFeed($subscription);
+ }
+
+ public static function runLocalURLDataProvider(): array {
+ return [
+ ['localhost/foo.bar'],
+ ['localHost/foo.bar'],
+ ['random-host/foo.bar'],
+ ['[::1]/bla.blub'],
+ ['[::]/bla.blub'],
+ ['192.168.0.1'],
+ ['172.16.42.1'],
+ ['[fdf8:f53b:82e4::53]/secret.ics'],
+ ['[fe80::200:5aee:feaa:20a2]/secret.ics'],
+ ['[0:0:0:0:0:0:10.0.0.1]/secret.ics'],
+ ['[0:0:0:0:0:ffff:127.0.0.0]/secret.ics'],
+ ['10.0.0.1'],
+ ['another-host.local'],
+ ['service.localhost'],
+ ];
+ }
+
+ public static function urlDataProvider(): array {
+ return [
+ [
+ 'https://foo.bar/bla2',
+ "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
+ 'text/calendar;charset=utf8',
+ ],
+ [
+ 'https://foo.bar/bla2',
+ '["vcalendar",[["prodid",{},"text","-//Example Corp.//Example Client//EN"],["version",{},"text","2.0"]],[["vtimezone",[["last-modified",{},"date-time","2004-01-10T03:28:45Z"],["tzid",{},"text","US/Eastern"]],[["daylight",[["dtstart",{},"date-time","2000-04-04T02:00:00"],["rrule",{},"recur",{"freq":"YEARLY","byday":"1SU","bymonth":4}],["tzname",{},"text","EDT"],["tzoffsetfrom",{},"utc-offset","-05:00"],["tzoffsetto",{},"utc-offset","-04:00"]],[]],["standard",[["dtstart",{},"date-time","2000-10-26T02:00:00"],["rrule",{},"recur",{"freq":"YEARLY","byday":"1SU","bymonth":10}],["tzname",{},"text","EST"],["tzoffsetfrom",{},"utc-offset","-04:00"],["tzoffsetto",{},"utc-offset","-05:00"]],[]]]],["vevent",[["dtstamp",{},"date-time","2006-02-06T00:11:21Z"],["dtstart",{"tzid":"US/Eastern"},"date-time","2006-01-02T14:00:00"],["duration",{},"duration","PT1H"],["recurrence-id",{"tzid":"US/Eastern"},"date-time","2006-01-04T12:00:00"],["summary",{},"text","Event #2"],["uid",{},"text","12345"]],[]]]]',
+ 'application/calendar+json',
+ ],
+ [
+ 'https://foo.bar/bla2',
+ '<?xml version="1.0" encoding="utf-8" ?><icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"><vcalendar><properties><prodid><text>-//Example Inc.//Example Client//EN</text></prodid><version><text>2.0</text></version></properties><components><vevent><properties><dtstamp><date-time>2006-02-06T00:11:21Z</date-time></dtstamp><dtstart><parameters><tzid><text>US/Eastern</text></tzid></parameters><date-time>2006-01-04T14:00:00</date-time></dtstart><duration><duration>PT1H</duration></duration><recurrence-id><parameters><tzid><text>US/Eastern</text></tzid></parameters><date-time>2006-01-04T12:00:00</date-time></recurrence-id><summary><text>Event #2 bis</text></summary><uid><text>12345</text></uid></properties></vevent></components></vcalendar></icalendar>',
+ 'application/calendar+xml',
+ ],
+ ];
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/WebcalCaching/PluginTest.php b/apps/dav/tests/unit/CalDAV/WebcalCaching/PluginTest.php
index 310d266cd57..804af021d5a 100644
--- a/apps/dav/tests/unit/CalDAV/WebcalCaching/PluginTest.php
+++ b/apps/dav/tests/unit/CalDAV/WebcalCaching/PluginTest.php
@@ -1,40 +1,28 @@
<?php
+
+declare(strict_types=1);
/**
- * @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\Tests\unit\CalDAV\WebcalCaching;
use OCA\DAV\CalDAV\WebcalCaching\Plugin;
use OCP\IRequest;
+use Sabre\DAV\Server;
+use Sabre\DAV\Tree;
+use Sabre\HTTP\Request;
+use Sabre\HTTP\Response;
class PluginTest extends \Test\TestCase {
- public function testDisabled() {
+ public function testDisabled(): void {
$request = $this->createMock(IRequest::class);
- $request->expects($this->at(0))
+ $request->expects($this->once())
->method('isUserAgent')
->with(Plugin::ENABLE_FOR_CLIENTS)
->willReturn(false);
- $request->expects($this->at(1))
+ $request->expects($this->once())
->method('getHeader')
->with('X-NC-CalDAV-Webcal-Caching')
->willReturn('');
@@ -44,20 +32,121 @@ class PluginTest extends \Test\TestCase {
$this->assertEquals(false, $plugin->isCachingEnabledForThisRequest());
}
- public function testEnabled() {
+ public function testEnabledUserAgent(): void {
$request = $this->createMock(IRequest::class);
- $request->expects($this->at(0))
+ $request->expects($this->once())
->method('isUserAgent')
->with(Plugin::ENABLE_FOR_CLIENTS)
- ->willReturn(false);
+ ->willReturn(true);
+ $request->expects($this->once())
+ ->method('getHeader')
+ ->with('X-NC-CalDAV-Webcal-Caching')
+ ->willReturn('');
+ $request->expects($this->once())
+ ->method('getMethod')
+ ->willReturn('REPORT');
+ $request->expects($this->never())
+ ->method('getParams');
+
+ $plugin = new Plugin($request);
- $request->expects($this->at(1))
+ $this->assertEquals(true, $plugin->isCachingEnabledForThisRequest());
+ }
+
+ public function testEnabledWebcalCachingHeader(): void {
+ $request = $this->createMock(IRequest::class);
+ $request->expects($this->once())
+ ->method('isUserAgent')
+ ->with(Plugin::ENABLE_FOR_CLIENTS)
+ ->willReturn(false);
+ $request->expects($this->once())
->method('getHeader')
->with('X-NC-CalDAV-Webcal-Caching')
->willReturn('On');
+ $request->expects($this->once())
+ ->method('getMethod')
+ ->willReturn('REPORT');
+ $request->expects($this->never())
+ ->method('getParams');
+
+ $plugin = new Plugin($request);
+
+ $this->assertEquals(true, $plugin->isCachingEnabledForThisRequest());
+ }
+
+ public function testEnabledExportRequest(): void {
+ $request = $this->createMock(IRequest::class);
+ $request->expects($this->once())
+ ->method('isUserAgent')
+ ->with(Plugin::ENABLE_FOR_CLIENTS)
+ ->willReturn(false);
+ $request->expects($this->once())
+ ->method('getHeader')
+ ->with('X-NC-CalDAV-Webcal-Caching')
+ ->willReturn('');
+ $request->expects($this->once())
+ ->method('getMethod')
+ ->willReturn('GET');
+ $request->expects($this->once())
+ ->method('getParams')
+ ->willReturn(['export' => '']);
$plugin = new Plugin($request);
$this->assertEquals(true, $plugin->isCachingEnabledForThisRequest());
}
+
+ public function testSkipNonCalendarRequest(): void {
+ $request = $this->createMock(IRequest::class);
+ $request->expects($this->once())
+ ->method('isUserAgent')
+ ->with(Plugin::ENABLE_FOR_CLIENTS)
+ ->willReturn(false);
+
+ $request->expects($this->once())
+ ->method('getHeader')
+ ->with('X-NC-CalDAV-Webcal-Caching')
+ ->willReturn('On');
+
+ $sabreRequest = new Request('REPORT', '/remote.php/dav/principals/users/admin/');
+ $sabreRequest->setBaseUrl('/remote.php/dav/');
+
+ $tree = $this->createMock(Tree::class);
+ $tree->expects($this->never())
+ ->method('getNodeForPath');
+
+ $server = new Server($tree);
+
+ $plugin = new Plugin($request);
+ $plugin->initialize($server);
+
+ $plugin->beforeMethod($sabreRequest, new Response());
+ }
+
+ public function testProcessCalendarRequest(): void {
+ $request = $this->createMock(IRequest::class);
+ $request->expects($this->once())
+ ->method('isUserAgent')
+ ->with(Plugin::ENABLE_FOR_CLIENTS)
+ ->willReturn(false);
+
+ $request->expects($this->once())
+ ->method('getHeader')
+ ->with('X-NC-CalDAV-Webcal-Caching')
+ ->willReturn('On');
+
+ $sabreRequest = new Request('REPORT', '/remote.php/dav/calendars/admin/personal/');
+ $sabreRequest->setBaseUrl('/remote.php/dav/');
+
+ $tree = $this->createMock(Tree::class);
+ $tree->expects($this->once())
+ ->method('getNodeForPath');
+
+ $server = new Server($tree);
+
+ $plugin = new Plugin($request);
+ $plugin->initialize($server);
+
+ $plugin->beforeMethod($sabreRequest, new Response());
+ }
}
diff --git a/apps/dav/tests/unit/CalDAV/WebcalCaching/RefreshWebcalServiceTest.php b/apps/dav/tests/unit/CalDAV/WebcalCaching/RefreshWebcalServiceTest.php
index 12e91345709..d4f4b9e878f 100644
--- a/apps/dav/tests/unit/CalDAV/WebcalCaching/RefreshWebcalServiceTest.php
+++ b/apps/dav/tests/unit/CalDAV/WebcalCaching/RefreshWebcalServiceTest.php
@@ -1,41 +1,18 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Thomas Citharel <nextcloud@tcit.fr>
- *
- * @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\Tests\unit\BackgroundJob;
+namespace OCA\DAV\Tests\unit\CalDAV\WebcalCaching;
-use GuzzleHttp\HandlerStack;
use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\WebcalCaching\Connection;
use OCA\DAV\CalDAV\WebcalCaching\RefreshWebcalService;
-use OCP\Http\Client\IClient;
-use OCP\Http\Client\IClientService;
-use OCP\Http\Client\IResponse;
-use OCP\Http\Client\LocalServerException;
-use OCP\IConfig;
-use OCP\ILogger;
+use OCP\AppFramework\Utility\ITimeFactory;
use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\BadRequest;
use Sabre\VObject;
use Sabre\VObject\Recur\NoInstancesException;
@@ -43,121 +20,187 @@ use Sabre\VObject\Recur\NoInstancesException;
use Test\TestCase;
class RefreshWebcalServiceTest extends TestCase {
-
- /** @var CalDavBackend | MockObject */
- private $caldavBackend;
-
- /** @var IClientService | MockObject */
- private $clientService;
-
- /** @var IConfig | MockObject */
- private $config;
-
- /** @var ILogger | MockObject */
- private $logger;
+ private CalDavBackend&MockObject $caldavBackend;
+ private Connection&MockObject $connection;
+ private LoggerInterface&MockObject $logger;
+ private ITimeFactory&MockObject $time;
protected function setUp(): void {
parent::setUp();
$this->caldavBackend = $this->createMock(CalDavBackend::class);
- $this->clientService = $this->createMock(IClientService::class);
- $this->config = $this->createMock(IConfig::class);
- $this->logger = $this->createMock(ILogger::class);
+ $this->connection = $this->createMock(Connection::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->time = $this->createMock(ITimeFactory::class);
}
- /**
- * @param string $body
- * @param string $contentType
- * @param string $result
- *
- * @dataProvider runDataProvider
- */
- public function testRun(string $body, string $contentType, string $result) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('runDataProvider')]
+ public function testRun(string $body, string $contentType, string $result): void {
$refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class)
- ->setMethods(['getRandomCalendarObjectUri'])
- ->setConstructorArgs([$this->caldavBackend, $this->clientService, $this->config, $this->logger])
+ ->onlyMethods(['getRandomCalendarObjectUri'])
+ ->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->time])
->getMock();
$refreshWebcalService
->method('getRandomCalendarObjectUri')
->willReturn('uri-1.ics');
- $this->caldavBackend->expects($this->once())
+ $this->caldavBackend->expects(self::once())
->method('getSubscriptionsForUser')
->with('principals/users/testuser')
->willReturn([
[
'id' => '99',
'uri' => 'sub456',
- '{http://apple.com/ns/ical/}refreshrate' => 'P1D',
- '{http://calendarserver.org/ns/}subscribed-strip-todos' => '1',
- '{http://calendarserver.org/ns/}subscribed-strip-alarms' => '1',
- '{http://calendarserver.org/ns/}subscribed-strip-attachments' => '1',
- 'source' => 'webcal://foo.bar/bla'
+ RefreshWebcalService::REFRESH_RATE => 'P1D',
+ RefreshWebcalService::STRIP_TODOS => '1',
+ RefreshWebcalService::STRIP_ALARMS => '1',
+ RefreshWebcalService::STRIP_ATTACHMENTS => '1',
+ 'source' => 'webcal://foo.bar/bla',
+ 'lastmodified' => 0,
],
[
'id' => '42',
'uri' => 'sub123',
- '{http://apple.com/ns/ical/}refreshrate' => 'PT1H',
- '{http://calendarserver.org/ns/}subscribed-strip-todos' => '1',
- '{http://calendarserver.org/ns/}subscribed-strip-alarms' => '1',
- '{http://calendarserver.org/ns/}subscribed-strip-attachments' => '1',
- 'source' => 'webcal://foo.bar/bla2'
+ RefreshWebcalService::REFRESH_RATE => 'PT1H',
+ RefreshWebcalService::STRIP_TODOS => '1',
+ RefreshWebcalService::STRIP_ALARMS => '1',
+ RefreshWebcalService::STRIP_ATTACHMENTS => '1',
+ 'source' => 'webcal://foo.bar/bla2',
+ 'lastmodified' => 0,
],
]);
- $client = $this->createMock(IClient::class);
- $response = $this->createMock(IResponse::class);
- $this->clientService->expects($this->once())
- ->method('newClient')
- ->with()
- ->willReturn($client);
-
- $this->config->expects($this->once())
- ->method('getAppValue')
- ->with('dav', 'webcalAllowLocalAccess', 'no')
- ->willReturn('no');
-
- $client->expects($this->once())
- ->method('get')
- ->with('https://foo.bar/bla2', $this->callback(function ($obj) {
- return $obj['allow_redirects']['redirects'] === 10 && $obj['handler'] instanceof HandlerStack;
- }))
- ->willReturn($response);
-
- $response->expects($this->once())
- ->method('getBody')
- ->with()
- ->willReturn($body);
- $response->expects($this->once())
- ->method('getHeader')
- ->with('Content-Type')
- ->willReturn($contentType);
-
- $this->caldavBackend->expects($this->once())
- ->method('purgeAllCachedEventsForSubscription')
- ->with(42);
-
- $this->caldavBackend->expects($this->once())
+ $this->connection->expects(self::once())
+ ->method('queryWebcalFeed')
+ ->willReturn($result);
+ $this->caldavBackend->expects(self::once())
->method('createCalendarObject')
->with(42, 'uri-1.ics', $result, 1);
$refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
}
-
- /**
- * @param string $body
- * @param string $contentType
- * @param string $result
- *
- * @dataProvider runDataProvider
- */
- public function testRunCreateCalendarNoException(string $body, string $contentType, string $result) {
- $client = $this->createMock(IClient::class);
- $response = $this->createMock(IResponse::class);
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('identicalDataProvider')]
+ public function testRunIdentical(string $uid, array $calendarObject, string $body, string $contentType, string $result): void {
+ $refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class)
+ ->onlyMethods(['getRandomCalendarObjectUri'])
+ ->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->time])
+ ->getMock();
+
+ $refreshWebcalService
+ ->method('getRandomCalendarObjectUri')
+ ->willReturn('uri-1.ics');
+
+ $this->caldavBackend->expects(self::once())
+ ->method('getSubscriptionsForUser')
+ ->with('principals/users/testuser')
+ ->willReturn([
+ [
+ 'id' => '99',
+ 'uri' => 'sub456',
+ RefreshWebcalService::REFRESH_RATE => 'P1D',
+ RefreshWebcalService::STRIP_TODOS => '1',
+ RefreshWebcalService::STRIP_ALARMS => '1',
+ RefreshWebcalService::STRIP_ATTACHMENTS => '1',
+ 'source' => 'webcal://foo.bar/bla',
+ 'lastmodified' => 0,
+ ],
+ [
+ 'id' => '42',
+ 'uri' => 'sub123',
+ RefreshWebcalService::REFRESH_RATE => 'PT1H',
+ RefreshWebcalService::STRIP_TODOS => '1',
+ RefreshWebcalService::STRIP_ALARMS => '1',
+ RefreshWebcalService::STRIP_ATTACHMENTS => '1',
+ 'source' => 'webcal://foo.bar/bla2',
+ 'lastmodified' => 0,
+ ],
+ ]);
+
+ $this->connection->expects(self::once())
+ ->method('queryWebcalFeed')
+ ->willReturn($result);
+
+ $this->caldavBackend->expects(self::once())
+ ->method('getLimitedCalendarObjects')
+ ->willReturn($calendarObject);
+
+ $denormalised = [
+ 'etag' => 100,
+ 'size' => strlen($calendarObject[$uid]['calendardata']),
+ 'uid' => 'sub456'
+ ];
+
+ $this->caldavBackend->expects(self::once())
+ ->method('getDenormalizedData')
+ ->willReturn($denormalised);
+
+ $this->caldavBackend->expects(self::never())
+ ->method('createCalendarObject');
+
+ $refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub456');
+ }
+
+ public function testRunJustUpdated(): void {
+ $refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class)
+ ->onlyMethods(['getRandomCalendarObjectUri'])
+ ->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->time])
+ ->getMock();
+
+ $refreshWebcalService
+ ->method('getRandomCalendarObjectUri')
+ ->willReturn('uri-1.ics');
+
+ $this->caldavBackend->expects(self::once())
+ ->method('getSubscriptionsForUser')
+ ->with('principals/users/testuser')
+ ->willReturn([
+ [
+ 'id' => '99',
+ 'uri' => 'sub456',
+ RefreshWebcalService::REFRESH_RATE => 'P1D',
+ RefreshWebcalService::STRIP_TODOS => '1',
+ RefreshWebcalService::STRIP_ALARMS => '1',
+ RefreshWebcalService::STRIP_ATTACHMENTS => '1',
+ 'source' => 'webcal://foo.bar/bla',
+ 'lastmodified' => time(),
+ ],
+ [
+ 'id' => '42',
+ 'uri' => 'sub123',
+ RefreshWebcalService::REFRESH_RATE => 'PT1H',
+ RefreshWebcalService::STRIP_TODOS => '1',
+ RefreshWebcalService::STRIP_ALARMS => '1',
+ RefreshWebcalService::STRIP_ATTACHMENTS => '1',
+ 'source' => 'webcal://foo.bar/bla2',
+ 'lastmodified' => time(),
+ ],
+ ]);
+
+ $timeMock = $this->createMock(\DateTime::class);
+ $this->time->expects(self::once())
+ ->method('getDateTime')
+ ->willReturn($timeMock);
+ $timeMock->expects(self::once())
+ ->method('getTimestamp')
+ ->willReturn(2101724667);
+ $this->time->expects(self::once())
+ ->method('getTime')
+ ->willReturn(time());
+ $this->connection->expects(self::never())
+ ->method('queryWebcalFeed');
+ $this->caldavBackend->expects(self::never())
+ ->method('createCalendarObject');
+
+ $refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('runDataProvider')]
+ public function testRunCreateCalendarNoException(string $body, string $contentType, string $result): void {
$refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class)
- ->setMethods(['getRandomCalendarObjectUri', 'getSubscription', 'queryWebcalFeed'])
- ->setConstructorArgs([$this->caldavBackend, $this->clientService, $this->config, $this->logger])
+ ->onlyMethods(['getRandomCalendarObjectUri', 'getSubscription',])
+ ->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->time])
->getMock();
$refreshWebcalService
@@ -169,72 +212,39 @@ class RefreshWebcalServiceTest extends TestCase {
->willReturn([
'id' => '42',
'uri' => 'sub123',
- '{http://apple.com/ns/ical/}refreshrate' => 'PT1H',
- '{http://calendarserver.org/ns/}subscribed-strip-todos' => '1',
- '{http://calendarserver.org/ns/}subscribed-strip-alarms' => '1',
- '{http://calendarserver.org/ns/}subscribed-strip-attachments' => '1',
- 'source' => 'webcal://foo.bar/bla2'
+ RefreshWebcalService::REFRESH_RATE => 'PT1H',
+ RefreshWebcalService::STRIP_TODOS => '1',
+ RefreshWebcalService::STRIP_ALARMS => '1',
+ RefreshWebcalService::STRIP_ATTACHMENTS => '1',
+ 'source' => 'webcal://foo.bar/bla2',
+ 'lastmodified' => 0,
]);
- $this->clientService->expects($this->once())
- ->method('newClient')
- ->with()
- ->willReturn($client);
-
- $this->config->expects($this->once())
- ->method('getAppValue')
- ->with('dav', 'webcalAllowLocalAccess', 'no')
- ->willReturn('no');
-
- $client->expects($this->once())
- ->method('get')
- ->with('https://foo.bar/bla2', $this->callback(function ($obj) {
- return $obj['allow_redirects']['redirects'] === 10 && $obj['handler'] instanceof HandlerStack;
- }))
- ->willReturn($response);
-
- $response->expects($this->once())
- ->method('getBody')
- ->with()
- ->willReturn($body);
- $response->expects($this->once())
- ->method('getHeader')
- ->with('Content-Type')
- ->willReturn($contentType);
-
- $this->caldavBackend->expects($this->once())
- ->method('purgeAllCachedEventsForSubscription')
- ->with(42);
-
- $this->caldavBackend->expects($this->once())
+ $this->connection->expects(self::once())
+ ->method('queryWebcalFeed')
+ ->willReturn($result);
+
+ $this->caldavBackend->expects(self::once())
->method('createCalendarObject')
->with(42, 'uri-1.ics', $result, 1);
-
+
$noInstanceException = new NoInstancesException("can't add calendar object");
- $this->caldavBackend->expects($this->once())
- ->method("createCalendarObject")
+ $this->caldavBackend->expects(self::once())
+ ->method('createCalendarObject')
->willThrowException($noInstanceException);
-
- $this->logger->expects($this->once())
- ->method('logException')
- ->with($noInstanceException);
+
+ $this->logger->expects(self::once())
+ ->method('warning')
+ ->with('Unable to create calendar object from subscription {subscriptionId}', ['exception' => $noInstanceException, 'subscriptionId' => '42', 'source' => 'webcal://foo.bar/bla2']);
$refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
}
- /**
- * @param string $body
- * @param string $contentType
- * @param string $result
- *
- * @dataProvider runDataProvider
- */
- public function testRunCreateCalendarBadRequest(string $body, string $contentType, string $result) {
- $client = $this->createMock(IClient::class);
- $response = $this->createMock(IResponse::class);
+ #[\PHPUnit\Framework\Attributes\DataProvider('runDataProvider')]
+ public function testRunCreateCalendarBadRequest(string $body, string $contentType, string $result): void {
$refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class)
- ->setMethods(['getRandomCalendarObjectUri', 'getSubscription', 'queryWebcalFeed'])
- ->setConstructorArgs([$this->caldavBackend, $this->clientService, $this->config, $this->logger])
+ ->onlyMethods(['getRandomCalendarObjectUri', 'getSubscription'])
+ ->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->time])
->getMock();
$refreshWebcalService
@@ -246,63 +256,54 @@ class RefreshWebcalServiceTest extends TestCase {
->willReturn([
'id' => '42',
'uri' => 'sub123',
- '{http://apple.com/ns/ical/}refreshrate' => 'PT1H',
- '{http://calendarserver.org/ns/}subscribed-strip-todos' => '1',
- '{http://calendarserver.org/ns/}subscribed-strip-alarms' => '1',
- '{http://calendarserver.org/ns/}subscribed-strip-attachments' => '1',
- 'source' => 'webcal://foo.bar/bla2'
+ RefreshWebcalService::REFRESH_RATE => 'PT1H',
+ RefreshWebcalService::STRIP_TODOS => '1',
+ RefreshWebcalService::STRIP_ALARMS => '1',
+ RefreshWebcalService::STRIP_ATTACHMENTS => '1',
+ 'source' => 'webcal://foo.bar/bla2',
+ 'lastmodified' => 0,
]);
- $this->clientService->expects($this->once())
- ->method('newClient')
- ->with()
- ->willReturn($client);
-
- $this->config->expects($this->once())
- ->method('getAppValue')
- ->with('dav', 'webcalAllowLocalAccess', 'no')
- ->willReturn('no');
-
- $client->expects($this->once())
- ->method('get')
- ->with('https://foo.bar/bla2', $this->callback(function ($obj) {
- return $obj['allow_redirects']['redirects'] === 10 && $obj['handler'] instanceof HandlerStack;
- }))
- ->willReturn($response);
-
- $response->expects($this->once())
- ->method('getBody')
- ->with()
- ->willReturn($body);
- $response->expects($this->once())
- ->method('getHeader')
- ->with('Content-Type')
- ->willReturn($contentType);
-
- $this->caldavBackend->expects($this->once())
- ->method('purgeAllCachedEventsForSubscription')
- ->with(42);
-
- $this->caldavBackend->expects($this->once())
+ $this->connection->expects(self::once())
+ ->method('queryWebcalFeed')
+ ->willReturn($result);
+
+ $this->caldavBackend->expects(self::once())
->method('createCalendarObject')
->with(42, 'uri-1.ics', $result, 1);
-
+
$badRequestException = new BadRequest("can't add reach calendar url");
- $this->caldavBackend->expects($this->once())
- ->method("createCalendarObject")
+ $this->caldavBackend->expects(self::once())
+ ->method('createCalendarObject')
->willThrowException($badRequestException);
-
- $this->logger->expects($this->once())
- ->method('logException')
- ->with($badRequestException);
+
+ $this->logger->expects(self::once())
+ ->method('warning')
+ ->with('Unable to create calendar object from subscription {subscriptionId}', ['exception' => $badRequestException, 'subscriptionId' => '42', 'source' => 'webcal://foo.bar/bla2']);
$refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
}
- /**
- * @return array
- */
- public function runDataProvider():array {
+ public static function identicalDataProvider(): array {
+ return [
+ [
+ '12345',
+ [
+ '12345' => [
+ 'id' => 42,
+ 'etag' => 100,
+ 'uri' => 'sub456',
+ 'calendardata' => "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
+ ],
+ ],
+ "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
+ 'text/calendar;charset=utf8',
+ "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20180218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
+ ],
+ ];
+ }
+
+ public static function runDataProvider(): array {
return [
[
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
@@ -321,109 +322,4 @@ class RefreshWebcalServiceTest extends TestCase {
]
];
}
-
- /**
- * @dataProvider runLocalURLDataProvider
- *
- * @param string $source
- */
- public function testRunLocalURL($source) {
- $refreshWebcalService = new RefreshWebcalService(
- $this->caldavBackend,
- $this->clientService,
- $this->config,
- $this->logger
- );
-
- $this->caldavBackend->expects($this->once())
- ->method('getSubscriptionsForUser')
- ->with('principals/users/testuser')
- ->willReturn([
- [
- 'id' => 42,
- 'uri' => 'sub123',
- 'refreshreate' => 'P1H',
- 'striptodos' => 1,
- 'stripalarms' => 1,
- 'stripattachments' => 1,
- 'source' => $source
- ],
- ]);
-
- $client = $this->createMock(IClient::class);
- $this->clientService->expects($this->once())
- ->method('newClient')
- ->with()
- ->willReturn($client);
-
- $this->config->expects($this->once())
- ->method('getAppValue')
- ->with('dav', 'webcalAllowLocalAccess', 'no')
- ->willReturn('no');
-
- $client->expects($this->once())
- ->method('get')
- ->willThrowException(new LocalServerException());
-
- $this->logger->expects($this->once())
- ->method('logException')
- ->with($this->isInstanceOf(LocalServerException::class), $this->anything());
-
- $refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
- }
-
- public function runLocalURLDataProvider():array {
- return [
- ['localhost/foo.bar'],
- ['localHost/foo.bar'],
- ['random-host/foo.bar'],
- ['[::1]/bla.blub'],
- ['[::]/bla.blub'],
- ['192.168.0.1'],
- ['172.16.42.1'],
- ['[fdf8:f53b:82e4::53]/secret.ics'],
- ['[fe80::200:5aee:feaa:20a2]/secret.ics'],
- ['[0:0:0:0:0:0:10.0.0.1]/secret.ics'],
- ['[0:0:0:0:0:ffff:127.0.0.0]/secret.ics'],
- ['10.0.0.1'],
- ['another-host.local'],
- ['service.localhost'],
- ];
- }
-
- public function testInvalidUrl() {
- $refreshWebcalService = new RefreshWebcalService($this->caldavBackend,
- $this->clientService, $this->config, $this->logger);
-
- $this->caldavBackend->expects($this->once())
- ->method('getSubscriptionsForUser')
- ->with('principals/users/testuser')
- ->willReturn([
- [
- 'id' => 42,
- 'uri' => 'sub123',
- 'refreshreate' => 'P1H',
- 'striptodos' => 1,
- 'stripalarms' => 1,
- 'stripattachments' => 1,
- 'source' => '!@#$'
- ],
- ]);
-
- $client = $this->createMock(IClient::class);
- $this->clientService->expects($this->once())
- ->method('newClient')
- ->with()
- ->willReturn($client);
-
- $this->config->expects($this->once())
- ->method('getAppValue')
- ->with('dav', 'webcalAllowLocalAccess', 'no')
- ->willReturn('no');
-
- $client->expects($this->never())
- ->method('get');
-
- $refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
- }
}
diff --git a/apps/dav/tests/unit/CapabilitiesTest.php b/apps/dav/tests/unit/CapabilitiesTest.php
index 7abfd08854b..ad70d576d48 100644
--- a/apps/dav/tests/unit/CapabilitiesTest.php
+++ b/apps/dav/tests/unit/CapabilitiesTest.php
@@ -1,42 +1,79 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016 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: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Tests\unit;
use OCA\DAV\Capabilities;
+use OCP\IConfig;
+use OCP\User\IAvailabilityCoordinator;
use Test\TestCase;
/**
* @package OCA\DAV\Tests\unit
*/
class CapabilitiesTest extends TestCase {
- public function testGetCapabilities() {
- $capabilities = new Capabilities();
+ public function testGetCapabilities(): void {
+ $config = $this->createMock(IConfig::class);
+ $config->expects($this->once())
+ ->method('getSystemValueBool')
+ ->with('bulkupload.enabled', $this->isType('bool'))
+ ->willReturn(false);
+ $coordinator = $this->createMock(IAvailabilityCoordinator::class);
+ $coordinator->expects($this->once())
+ ->method('isEnabled')
+ ->willReturn(false);
+ $capabilities = new Capabilities($config, $coordinator);
+ $expected = [
+ 'dav' => [
+ 'chunking' => '1.0',
+ 'public_shares_chunking' => true,
+ ],
+ ];
+ $this->assertSame($expected, $capabilities->getCapabilities());
+ }
+
+ public function testGetCapabilitiesWithBulkUpload(): void {
+ $config = $this->createMock(IConfig::class);
+ $config->expects($this->once())
+ ->method('getSystemValueBool')
+ ->with('bulkupload.enabled', $this->isType('bool'))
+ ->willReturn(true);
+ $coordinator = $this->createMock(IAvailabilityCoordinator::class);
+ $coordinator->expects($this->once())
+ ->method('isEnabled')
+ ->willReturn(false);
+ $capabilities = new Capabilities($config, $coordinator);
+ $expected = [
+ 'dav' => [
+ 'chunking' => '1.0',
+ 'public_shares_chunking' => true,
+ 'bulkupload' => '1.0',
+ ],
+ ];
+ $this->assertSame($expected, $capabilities->getCapabilities());
+ }
+
+ public function testGetCapabilitiesWithAbsence(): void {
+ $config = $this->createMock(IConfig::class);
+ $config->expects($this->once())
+ ->method('getSystemValueBool')
+ ->with('bulkupload.enabled', $this->isType('bool'))
+ ->willReturn(false);
+ $coordinator = $this->createMock(IAvailabilityCoordinator::class);
+ $coordinator->expects($this->once())
+ ->method('isEnabled')
+ ->willReturn(true);
+ $capabilities = new Capabilities($config, $coordinator);
$expected = [
'dav' => [
'chunking' => '1.0',
- // disabled because of https://github.com/nextcloud/desktop/issues/4243
- // 'bulkupload' => '1.0',
+ 'public_shares_chunking' => true,
+ 'absence-supported' => true,
+ 'absence-replacement' => true,
],
];
$this->assertSame($expected, $capabilities->getCapabilities());
diff --git a/apps/dav/tests/unit/CardDAV/Activity/BackendTest.php b/apps/dav/tests/unit/CardDAV/Activity/BackendTest.php
new file mode 100644
index 00000000000..a070a3d7131
--- /dev/null
+++ b/apps/dav/tests/unit/CardDAV/Activity/BackendTest.php
@@ -0,0 +1,483 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CardDAV\Activity;
+
+use OCA\DAV\CardDAV\Activity\Backend;
+use OCA\DAV\CardDAV\Activity\Provider\Addressbook;
+use OCA\DAV\CardDAV\Activity\Provider\Card;
+use OCP\Activity\IEvent;
+use OCP\Activity\IManager;
+use OCP\App\IAppManager;
+use OCP\IGroup;
+use OCP\IGroupManager;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class BackendTest extends TestCase {
+ protected IManager&MockObject $activityManager;
+ protected IGroupManager&MockObject $groupManager;
+ protected IUserSession&MockObject $userSession;
+ protected IAppManager&MockObject $appManager;
+ protected IUserManager&MockObject $userManager;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->activityManager = $this->createMock(IManager::class);
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->appManager = $this->createMock(IAppManager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ }
+
+ /**
+ * @return Backend|MockObject
+ */
+ protected function getBackend(array $methods = []): Backend {
+ if (empty($methods)) {
+ return new Backend(
+ $this->activityManager,
+ $this->groupManager,
+ $this->userSession,
+ $this->appManager,
+ $this->userManager
+ );
+ } else {
+ return $this->getMockBuilder(Backend::class)
+ ->setConstructorArgs([
+ $this->activityManager,
+ $this->groupManager,
+ $this->userSession,
+ $this->appManager,
+ $this->userManager
+ ])
+ ->onlyMethods($methods)
+ ->getMock();
+ }
+ }
+
+ public static function dataCallTriggerAddressBookActivity(): array {
+ return [
+ ['onAddressbookCreate', [['data']], Addressbook::SUBJECT_ADD, [['data'], [], []]],
+ ['onAddressbookUpdate', [['data'], ['shares'], ['changed-properties']], Addressbook::SUBJECT_UPDATE, [['data'], ['shares'], ['changed-properties']]],
+ ['onAddressbookDelete', [['data'], ['shares']], Addressbook::SUBJECT_DELETE, [['data'], ['shares'], []]],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataCallTriggerAddressBookActivity')]
+ public function testCallTriggerAddressBookActivity(string $method, array $payload, string $expectedSubject, array $expectedPayload): void {
+ $backend = $this->getBackend(['triggerAddressbookActivity']);
+ $backend->expects($this->once())
+ ->method('triggerAddressbookActivity')
+ ->willReturnCallback(function () use ($expectedPayload, $expectedSubject): void {
+ $arguments = func_get_args();
+ $this->assertSame($expectedSubject, array_shift($arguments));
+ $this->assertEquals($expectedPayload, $arguments);
+ });
+
+ call_user_func_array([$backend, $method], $payload);
+ }
+
+ public static function dataTriggerAddressBookActivity(): array {
+ return [
+ // Add addressbook
+ [Addressbook::SUBJECT_ADD, [], [], [], '', '', null, []],
+ [Addressbook::SUBJECT_ADD, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], [], [], '', 'admin', null, ['admin']],
+ [Addressbook::SUBJECT_ADD, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], [], [], 'test2', 'test2', null, ['admin']],
+
+ // Update addressbook
+ [Addressbook::SUBJECT_UPDATE, [], [], [], '', '', null, []],
+ // No visible change - owner only
+ [Addressbook::SUBJECT_UPDATE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], [], '', 'admin', null, ['admin']],
+ // Visible change
+ [Addressbook::SUBJECT_UPDATE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], ['{DAV:}displayname' => 'Name'], '', 'admin', ['user1'], ['user1', 'admin']],
+ [Addressbook::SUBJECT_UPDATE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], ['{DAV:}displayname' => 'Name'], 'test2', 'test2', ['user1'], ['user1', 'admin']],
+
+ // Delete addressbook
+ [Addressbook::SUBJECT_DELETE, [], [], [], '', '', null, []],
+ [Addressbook::SUBJECT_DELETE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], [], '', 'admin', [], ['admin']],
+ [Addressbook::SUBJECT_DELETE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], [], '', 'admin', ['user1'], ['user1', 'admin']],
+ [Addressbook::SUBJECT_DELETE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], [], 'test2', 'test2', ['user1'], ['user1', 'admin']],
+ ];
+ }
+
+ /**
+ * @param string[]|null $shareUsers
+ * @param string[] $users
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTriggerAddressBookActivity')]
+ public function testTriggerAddressBookActivity(string $action, array $data, array $shares, array $changedProperties, string $currentUser, string $author, ?array $shareUsers, array $users): void {
+ $backend = $this->getBackend(['getUsersForShares']);
+
+ if ($shareUsers === null) {
+ $backend->expects($this->never())
+ ->method('getUsersForShares');
+ } else {
+ $backend->expects($this->once())
+ ->method('getUsersForShares')
+ ->with($shares)
+ ->willReturn($shareUsers);
+ }
+
+ if ($author !== '') {
+ if ($currentUser !== '') {
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($this->getUserMock($currentUser));
+ } else {
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn(null);
+ }
+
+ $event = $this->createMock(IEvent::class);
+ $this->activityManager->expects($this->once())
+ ->method('generateEvent')
+ ->willReturn($event);
+
+ $event->expects($this->once())
+ ->method('setApp')
+ ->with('dav')
+ ->willReturnSelf();
+ $event->expects($this->once())
+ ->method('setObject')
+ ->with('addressbook', $data['id'])
+ ->willReturnSelf();
+ $event->expects($this->once())
+ ->method('setType')
+ ->with('contacts')
+ ->willReturnSelf();
+ $event->expects($this->once())
+ ->method('setAuthor')
+ ->with($author)
+ ->willReturnSelf();
+
+ $this->userManager->expects($action === Addressbook::SUBJECT_DELETE ? $this->exactly(sizeof($users)) : $this->never())
+ ->method('userExists')
+ ->willReturn(true);
+
+ $event->expects($this->exactly(count($users)))
+ ->method('setAffectedUser')
+ ->willReturnSelf();
+ $event->expects($this->exactly(count($users)))
+ ->method('setSubject')
+ ->willReturnSelf();
+ $this->activityManager->expects($this->exactly(count($users)))
+ ->method('publish')
+ ->with($event);
+ } else {
+ $this->activityManager->expects($this->never())
+ ->method('generateEvent');
+ }
+
+ $this->invokePrivate($backend, 'triggerAddressbookActivity', [$action, $data, $shares, $changedProperties]);
+ }
+
+ public function testNoAddressbookActivityCreatedForSystemAddressbook(): void {
+ $backend = $this->getBackend();
+ $this->activityManager->expects($this->never())
+ ->method('generateEvent');
+ $this->assertEmpty($this->invokePrivate($backend, 'triggerAddressbookActivity', [Addressbook::SUBJECT_ADD, ['principaluri' => 'principals/system/system'], [], [], '', '', null, []]));
+ }
+
+ public function testUserDeletionDoesNotCreateActivity(): void {
+ $backend = $this->getBackend();
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->willReturn(false);
+
+ $this->activityManager->expects($this->never())
+ ->method('publish');
+
+ $this->invokePrivate($backend, 'triggerAddressbookActivity', [Addressbook::SUBJECT_DELETE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], [], []]);
+ }
+
+ public static function dataTriggerCardActivity(): array {
+ $cardData = "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.4.8//EN\r\nUID:test-user\r\nFN:test-user\r\nN:test-user;;;;\r\nEND:VCARD\r\n\r\n";
+
+ return [
+ // Add card
+ [Card::SUBJECT_ADD, [], [], [], '', '', null, []],
+ [Card::SUBJECT_ADD, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], [], [
+ 'carddata' => $cardData
+ ], '', 'admin', [], ['admin']],
+ [Card::SUBJECT_ADD, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], [], ['carddata' => $cardData], 'test2', 'test2', [], ['admin']],
+
+ // Update card
+ [Card::SUBJECT_UPDATE, [], [], [], '', '', null, []],
+ // No visible change - owner only
+ [Card::SUBJECT_UPDATE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], ['carddata' => $cardData], '', 'admin', [], ['admin']],
+ // Visible change
+ [Card::SUBJECT_UPDATE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], ['carddata' => $cardData], '', 'admin', ['user1'], ['user1', 'admin']],
+ [Card::SUBJECT_UPDATE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], ['carddata' => $cardData], 'test2', 'test2', ['user1'], ['user1', 'admin']],
+
+ // Delete card
+ [Card::SUBJECT_DELETE, [], [], ['carddata' => $cardData], '', '', null, []],
+ [Card::SUBJECT_DELETE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], ['carddata' => $cardData], '', 'admin', [], ['admin']],
+ [Card::SUBJECT_DELETE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], ['carddata' => $cardData], '', 'admin', ['user1'], ['user1', 'admin']],
+ [Card::SUBJECT_DELETE, [
+ 'principaluri' => 'principal/user/admin',
+ 'id' => 42,
+ 'uri' => 'this-uri',
+ '{DAV:}displayname' => 'Name of addressbook',
+ ], ['shares'], ['carddata' => $cardData], 'test2', 'test2', ['user1'], ['user1', 'admin']],
+ ];
+ }
+
+ /**
+ * @param string[]|null $shareUsers
+ * @param string[] $users
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTriggerCardActivity')]
+ public function testTriggerCardActivity(string $action, array $addressBookData, array $shares, array $cardData, string $currentUser, string $author, ?array $shareUsers, array $users): void {
+ $backend = $this->getBackend(['getUsersForShares']);
+
+ if ($shareUsers === null) {
+ $backend->expects($this->never())
+ ->method('getUsersForShares');
+ } else {
+ $backend->expects($this->once())
+ ->method('getUsersForShares')
+ ->with($shares)
+ ->willReturn($shareUsers);
+ }
+
+ if ($author !== '') {
+ if ($currentUser !== '') {
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($this->getUserMock($currentUser));
+ } else {
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn(null);
+ }
+
+ $event = $this->createMock(IEvent::class);
+ $this->activityManager->expects($this->once())
+ ->method('generateEvent')
+ ->willReturn($event);
+
+ $event->expects($this->once())
+ ->method('setApp')
+ ->with('dav')
+ ->willReturnSelf();
+ $event->expects($this->once())
+ ->method('setObject')
+ ->with('addressbook', $addressBookData['id'])
+ ->willReturnSelf();
+ $event->expects($this->once())
+ ->method('setType')
+ ->with('contacts')
+ ->willReturnSelf();
+ $event->expects($this->once())
+ ->method('setAuthor')
+ ->with($author)
+ ->willReturnSelf();
+
+ $event->expects($this->exactly(count($users)))
+ ->method('setAffectedUser')
+ ->willReturnSelf();
+ $event->expects($this->exactly(count($users)))
+ ->method('setSubject')
+ ->willReturnSelf();
+ $this->activityManager->expects($this->exactly(count($users)))
+ ->method('publish')
+ ->with($event);
+ } else {
+ $this->activityManager->expects($this->never())
+ ->method('generateEvent');
+ }
+
+ $this->invokePrivate($backend, 'triggerCardActivity', [$action, $addressBookData, $shares, $cardData]);
+ }
+
+ public function testNoCardActivityCreatedForSystemAddressbook(): void {
+ $backend = $this->getBackend();
+ $this->activityManager->expects($this->never())
+ ->method('generateEvent');
+ $this->assertEmpty($this->invokePrivate($backend, 'triggerCardActivity', [Card::SUBJECT_UPDATE, ['principaluri' => 'principals/system/system'], [], []]));
+ }
+
+ public static function dataGetUsersForShares(): array {
+ return [
+ [
+ [],
+ [],
+ [],
+ ],
+ [
+ [
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user1'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user3'],
+ ],
+ [],
+ ['user1', 'user2', 'user3'],
+ ],
+ [
+ [
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user1'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/groups/group2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/groups/group3'],
+ ],
+ ['group2' => null, 'group3' => null],
+ ['user1', 'user2'],
+ ],
+ [
+ [
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user1'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/users/user2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/groups/group2'],
+ ['{http://owncloud.org/ns}principal' => 'principal/groups/group3'],
+ ],
+ ['group2' => ['user1', 'user2', 'user3'], 'group3' => ['user2', 'user3', 'user4']],
+ ['user1', 'user2', 'user3', 'user4'],
+ ],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataGetUsersForShares')]
+ public function testGetUsersForShares(array $shares, array $groups, array $expected): void {
+ $backend = $this->getBackend();
+
+ $getGroups = [];
+ foreach ($groups as $gid => $members) {
+ if ($members === null) {
+ $getGroups[] = [$gid, null];
+ continue;
+ }
+
+ $group = $this->createMock(IGroup::class);
+ $group->expects($this->once())
+ ->method('getUsers')
+ ->willReturn($this->getUsers($members));
+
+ $getGroups[] = [$gid, $group];
+ }
+
+ $this->groupManager->expects($this->exactly(sizeof($getGroups)))
+ ->method('get')
+ ->willReturnMap($getGroups);
+
+ $users = $this->invokePrivate($backend, 'getUsersForShares', [$shares]);
+ sort($users);
+ $this->assertEquals($expected, $users);
+ }
+
+ /**
+ * @param string[] $users
+ * @return IUser[]|MockObject[]
+ */
+ protected function getUsers(array $users): array {
+ $list = [];
+ foreach ($users as $user) {
+ $list[] = $this->getUserMock($user);
+ }
+ return $list;
+ }
+
+ /**
+ * @return IUser|MockObject
+ */
+ protected function getUserMock(string $uid): IUser {
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->once())
+ ->method('getUID')
+ ->willReturn($uid);
+ return $user;
+ }
+}
diff --git a/apps/dav/tests/unit/CardDAV/AddressBookImplTest.php b/apps/dav/tests/unit/CardDAV/AddressBookImplTest.php
index eb87901f3ac..74699cf3925 100644
--- a/apps/dav/tests/unit/CardDAV/AddressBookImplTest.php
+++ b/apps/dav/tests/unit/CardDAV/AddressBookImplTest.php
@@ -1,63 +1,31 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @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 Georg Ehrke <oc.list@georgehrke.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>
- * @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\Tests\unit\CardDAV;
use OCA\DAV\CardDAV\AddressBook;
use OCA\DAV\CardDAV\AddressBookImpl;
use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\Db\PropertyMapper;
use OCP\IURLGenerator;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\VObject\Component\VCard;
use Sabre\VObject\Property\Text;
//use Sabre\VObject\Property\;
use Test\TestCase;
class AddressBookImplTest extends TestCase {
-
- /** @var AddressBookImpl */
- private $addressBookImpl;
-
- /** @var array */
- private $addressBookInfo;
-
- /** @var AddressBook | \PHPUnit\Framework\MockObject\MockObject */
- private $addressBook;
-
- /** @var IURLGenerator | \PHPUnit\Framework\MockObject\MockObject */
- private $urlGenerator;
-
- /** @var CardDavBackend | \PHPUnit\Framework\MockObject\MockObject */
- private $backend;
-
- /** @var VCard | \PHPUnit\Framework\MockObject\MockObject */
- private $vCard;
+ private array $addressBookInfo;
+ private AddressBook&MockObject $addressBook;
+ private IURLGenerator&MockObject $urlGenerator;
+ private CardDavBackend&MockObject $backend;
+ private PropertyMapper&MockObject $propertyMapper;
+ private VCard&MockObject $vCard;
+ private AddressBookImpl $addressBookImpl;
protected function setUp(): void {
parent::setUp();
@@ -68,34 +36,34 @@ class AddressBookImplTest extends TestCase {
'principaluri' => 'principals/system/system',
'{DAV:}displayname' => 'display name',
];
- $this->addressBook = $this->getMockBuilder(AddressBook::class)
- ->disableOriginalConstructor()->getMock();
- $this->backend = $this->getMockBuilder(CardDavBackend::class)
- ->disableOriginalConstructor()->getMock();
+ $this->addressBook = $this->createMock(AddressBook::class);
+ $this->backend = $this->createMock(CardDavBackend::class);
$this->vCard = $this->createMock(VCard::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->propertyMapper = $this->createMock(PropertyMapper::class);
$this->addressBookImpl = new AddressBookImpl(
$this->addressBook,
$this->addressBookInfo,
$this->backend,
- $this->urlGenerator
+ $this->urlGenerator,
+ $this->propertyMapper,
+ null
);
}
- public function testGetKey() {
+ public function testGetKey(): void {
$this->assertSame($this->addressBookInfo['id'],
$this->addressBookImpl->getKey());
}
- public function testGetDisplayName() {
+ public function testGetDisplayName(): void {
$this->assertSame($this->addressBookInfo['{DAV:}displayname'],
$this->addressBookImpl->getDisplayName());
}
- public function testSearch() {
-
- /** @var \PHPUnit\Framework\MockObject\MockObject | AddressBookImpl $addressBookImpl */
+ public function testSearch(): void {
+ /** @var MockObject&AddressBookImpl $addressBookImpl */
$addressBookImpl = $this->getMockBuilder(AddressBookImpl::class)
->setConstructorArgs(
[
@@ -103,9 +71,11 @@ class AddressBookImplTest extends TestCase {
$this->addressBookInfo,
$this->backend,
$this->urlGenerator,
+ $this->propertyMapper,
+ null
]
)
- ->setMethods(['vCard2Array', 'readCard'])
+ ->onlyMethods(['vCard2Array', 'readCard'])
->getMock();
$pattern = 'pattern';
@@ -123,25 +93,21 @@ class AddressBookImplTest extends TestCase {
$addressBookImpl->expects($this->exactly(2))->method('readCard')
->willReturn($this->vCard);
$addressBookImpl->expects($this->exactly(2))->method('vCard2Array')
- ->withConsecutive(
- ['foo.vcf', $this->vCard],
- ['bar.vcf', $this->vCard]
- )->willReturn('vCard');
+ ->willReturnMap([
+ ['foo.vcf', $this->vCard, 'vCard'],
+ ['bar.vcf', $this->vCard, 'vCard'],
+ ]);
$result = $addressBookImpl->search($pattern, $searchProperties, []);
$this->assertTrue((is_array($result)));
$this->assertSame(2, count($result));
}
- /**
- * @dataProvider dataTestCreate
- *
- * @param array $properties
- */
- public function testCreate($properties) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTestCreate')]
+ public function testCreate(array $properties): void {
$uid = 'uid';
- /** @var \PHPUnit\Framework\MockObject\MockObject | AddressBookImpl $addressBookImpl */
+ /** @var MockObject&AddressBookImpl $addressBookImpl */
$addressBookImpl = $this->getMockBuilder(AddressBookImpl::class)
->setConstructorArgs(
[
@@ -149,9 +115,11 @@ class AddressBookImplTest extends TestCase {
$this->addressBookInfo,
$this->backend,
$this->urlGenerator,
+ $this->propertyMapper,
+ null
]
)
- ->setMethods(['vCard2Array', 'createUid', 'createEmptyVCard'])
+ ->onlyMethods(['vCard2Array', 'createUid', 'createEmptyVCard'])
->getMock();
$expectedProperties = 0;
@@ -178,7 +146,7 @@ class AddressBookImplTest extends TestCase {
$this->assertTrue($addressBookImpl->createOrUpdate($properties));
}
- public function dataTestCreate() {
+ public static function dataTestCreate(): array {
return [
[[]],
[['FN' => 'John Doe']],
@@ -186,12 +154,12 @@ class AddressBookImplTest extends TestCase {
];
}
- public function testUpdate() {
+ public function testUpdate(): void {
$uid = 'uid';
$uri = 'bla.vcf';
$properties = ['URI' => $uri, 'UID' => $uid, 'FN' => 'John Doe'];
- /** @var \PHPUnit\Framework\MockObject\MockObject | AddressBookImpl $addressBookImpl */
+ /** @var MockObject&AddressBookImpl $addressBookImpl */
$addressBookImpl = $this->getMockBuilder(AddressBookImpl::class)
->setConstructorArgs(
[
@@ -199,9 +167,11 @@ class AddressBookImplTest extends TestCase {
$this->addressBookInfo,
$this->backend,
$this->urlGenerator,
+ $this->propertyMapper,
+ null
]
)
- ->setMethods(['vCard2Array', 'createUid', 'createEmptyVCard', 'readCard'])
+ ->onlyMethods(['vCard2Array', 'createUid', 'createEmptyVCard', 'readCard'])
->getMock();
$addressBookImpl->expects($this->never())->method('createUid');
@@ -221,14 +191,14 @@ class AddressBookImplTest extends TestCase {
$this->assertTrue($addressBookImpl->createOrUpdate($properties));
}
- public function testUpdateWithTypes() {
+ public function testUpdateWithTypes(): void {
$uid = 'uid';
$uri = 'bla.vcf';
$properties = ['URI' => $uri, 'UID' => $uid, 'FN' => 'John Doe', 'ADR' => [['type' => 'HOME', 'value' => ';;street;city;;;country']]];
$vCard = new vCard;
- $textProperty = $vCard->createProperty('KEY','value');
+ $textProperty = $vCard->createProperty('KEY', 'value');
- /** @var \PHPUnit\Framework\MockObject\MockObject | AddressBookImpl $addressBookImpl */
+ /** @var MockObject&AddressBookImpl $addressBookImpl */
$addressBookImpl = $this->getMockBuilder(AddressBookImpl::class)
->setConstructorArgs(
[
@@ -236,9 +206,11 @@ class AddressBookImplTest extends TestCase {
$this->addressBookInfo,
$this->backend,
$this->urlGenerator,
+ $this->propertyMapper,
+ null
]
)
- ->setMethods(['vCard2Array', 'createUid', 'createEmptyVCard', 'readCard'])
+ ->onlyMethods(['vCard2Array', 'createUid', 'createEmptyVCard', 'readCard'])
->getMock();
$this->backend->expects($this->once())->method('getCard')
@@ -256,13 +228,8 @@ class AddressBookImplTest extends TestCase {
$addressBookImpl->createOrUpdate($properties);
}
- /**
- * @dataProvider dataTestGetPermissions
- *
- * @param array $permissions
- * @param int $expected
- */
- public function testGetPermissions($permissions, $expected) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTestGetPermissions')]
+ public function testGetPermissions(array $permissions, int $expected): void {
$this->addressBook->expects($this->once())->method('getACL')
->willReturn($permissions);
@@ -271,21 +238,22 @@ class AddressBookImplTest extends TestCase {
);
}
- public function dataTestGetPermissions() {
+ public static function dataTestGetPermissions(): array {
return [
[[], 0],
- [[['privilege' => '{DAV:}read']], 1],
- [[['privilege' => '{DAV:}write']], 6],
- [[['privilege' => '{DAV:}all']], 31],
- [[['privilege' => '{DAV:}read'],['privilege' => '{DAV:}write']], 7],
- [[['privilege' => '{DAV:}read'],['privilege' => '{DAV:}all']], 31],
- [[['privilege' => '{DAV:}all'],['privilege' => '{DAV:}write']], 31],
- [[['privilege' => '{DAV:}read'],['privilege' => '{DAV:}write'],['privilege' => '{DAV:}all']], 31],
- [[['privilege' => '{DAV:}all'],['privilege' => '{DAV:}read'],['privilege' => '{DAV:}write']], 31],
+ [[['privilege' => '{DAV:}read', 'principal' => 'principals/system/system']], 1],
+ [[['privilege' => '{DAV:}read', 'principal' => 'principals/system/system'], ['privilege' => '{DAV:}write', 'principal' => 'principals/someone/else']], 1],
+ [[['privilege' => '{DAV:}write', 'principal' => 'principals/system/system']], 6],
+ [[['privilege' => '{DAV:}all', 'principal' => 'principals/system/system']], 31],
+ [[['privilege' => '{DAV:}read', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}write', 'principal' => 'principals/system/system']], 7],
+ [[['privilege' => '{DAV:}read', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}all', 'principal' => 'principals/system/system']], 31],
+ [[['privilege' => '{DAV:}all', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}write', 'principal' => 'principals/system/system']], 31],
+ [[['privilege' => '{DAV:}read', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}write', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}all', 'principal' => 'principals/system/system']], 31],
+ [[['privilege' => '{DAV:}all', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}read', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}write', 'principal' => 'principals/system/system']], 31],
];
}
- public function testDelete() {
+ public function testDelete(): void {
$cardId = 1;
$cardUri = 'cardUri';
$this->backend->expects($this->once())->method('getCardUri')
@@ -297,7 +265,7 @@ class AddressBookImplTest extends TestCase {
$this->assertTrue($this->addressBookImpl->delete($cardId));
}
- public function testReadCard() {
+ public function testReadCard(): void {
$vCard = new VCard();
$vCard->add(new Text($vCard, 'UID', 'uid'));
$vCardSerialized = $vCard->serialize();
@@ -308,8 +276,8 @@ class AddressBookImplTest extends TestCase {
$this->assertSame($vCardSerialized, $resultSerialized);
}
- public function testCreateUid() {
- /** @var \PHPUnit\Framework\MockObject\MockObject | AddressBookImpl $addressBookImpl */
+ public function testCreateUid(): void {
+ /** @var MockObject&AddressBookImpl $addressBookImpl */
$addressBookImpl = $this->getMockBuilder(AddressBookImpl::class)
->setConstructorArgs(
[
@@ -317,13 +285,19 @@ class AddressBookImplTest extends TestCase {
$this->addressBookInfo,
$this->backend,
$this->urlGenerator,
+ $this->propertyMapper,
+ null
]
)
- ->setMethods(['getUid'])
+ ->onlyMethods(['getUid'])
->getMock();
- $addressBookImpl->expects($this->at(0))->method('getUid')->willReturn('uid0');
- $addressBookImpl->expects($this->at(1))->method('getUid')->willReturn('uid1');
+ $addressBookImpl->expects($this->exactly(2))
+ ->method('getUid')
+ ->willReturnOnConsecutiveCalls(
+ 'uid0',
+ 'uid1',
+ );
// simulate that 'uid0' already exists, so the second uid will be returned
$this->backend->expects($this->exactly(2))->method('getContact')
@@ -338,7 +312,7 @@ class AddressBookImplTest extends TestCase {
);
}
- public function testCreateEmptyVCard() {
+ public function testCreateEmptyVCard(): void {
$uid = 'uid';
$expectedVCard = new VCard();
$expectedVCard->UID = $uid;
@@ -350,7 +324,7 @@ class AddressBookImplTest extends TestCase {
$this->assertSame($expectedVCardSerialized, $resultSerialized);
}
- public function testVCard2Array() {
+ public function testVCard2Array(): void {
$vCard = new VCard();
$vCard->add($vCard->createProperty('FN', 'Full Name'));
@@ -417,7 +391,7 @@ class AddressBookImplTest extends TestCase {
], $array);
}
- public function testVCard2ArrayWithTypes() {
+ public function testVCard2ArrayWithTypes(): void {
$vCard = new VCard();
$vCard->add($vCard->createProperty('FN', 'Full Name'));
@@ -509,7 +483,9 @@ class AddressBookImplTest extends TestCase {
$this->addressBook,
$addressBookInfo,
$this->backend,
- $this->urlGenerator
+ $this->urlGenerator,
+ $this->propertyMapper,
+ null
);
$this->assertTrue($addressBookImpl->isSystemAddressBook());
@@ -528,7 +504,9 @@ class AddressBookImplTest extends TestCase {
$this->addressBook,
$addressBookInfo,
$this->backend,
- $this->urlGenerator
+ $this->urlGenerator,
+ $this->propertyMapper,
+ 'user2'
);
$this->assertFalse($addressBookImpl->isSystemAddressBook());
@@ -548,7 +526,9 @@ class AddressBookImplTest extends TestCase {
$this->addressBook,
$addressBookInfo,
$this->backend,
- $this->urlGenerator
+ $this->urlGenerator,
+ $this->propertyMapper,
+ 'user2'
);
$this->assertFalse($addressBookImpl->isSystemAddressBook());
diff --git a/apps/dav/tests/unit/CardDAV/AddressBookTest.php b/apps/dav/tests/unit/CardDAV/AddressBookTest.php
index 23e3e4f3b2a..cf28b7b8a8e 100644
--- a/apps/dav/tests/unit/CardDAV/AddressBookTest.php
+++ b/apps/dav/tests/unit/CardDAV/AddressBookTest.php
@@ -1,119 +1,144 @@
<?php
+
+declare(strict_types=1);
/**
- * @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>
- *
- * @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\Tests\unit\CardDAV;
use OCA\DAV\CardDAV\AddressBook;
+use OCA\DAV\CardDAV\Card;
use OCA\DAV\CardDAV\CardDavBackend;
use OCP\IL10N;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\PropPatch;
use Test\TestCase;
class AddressBookTest extends TestCase {
- public function testDelete() {
- /** @var \PHPUnit\Framework\MockObject\MockObject | CardDavBackend $backend */
- $backend = $this->getMockBuilder(CardDavBackend::class)->disableOriginalConstructor()->getMock();
+ public function testMove(): void {
+ $backend = $this->createMock(CardDavBackend::class);
+ $addressBookInfo = [
+ '{http://owncloud.org/ns}owner-principal' => 'user1',
+ '{DAV:}displayname' => 'Test address book',
+ 'principaluri' => 'user2',
+ 'id' => 666,
+ 'uri' => 'default',
+ ];
+ $l10n = $this->createMock(IL10N::class);
+ $addressBook = new AddressBook($backend, $addressBookInfo, $l10n);
+
+ $card = new Card($backend, $addressBookInfo, ['id' => 5, 'carddata' => 'RANDOM VCF DATA', 'uri' => 'something', 'addressbookid' => 23]);
+
+ $backend->expects($this->once())->method('moveCard')
+ ->with(23, 'something', 666, 'new')
+ ->willReturn(true);
+
+ $addressBook->moveInto('new', 'old', $card);
+ }
+
+ public function testDelete(): void {
+ /** @var MockObject | CardDavBackend $backend */
+ $backend = $this->createMock(CardDavBackend::class);
$backend->expects($this->once())->method('updateShares');
$backend->expects($this->any())->method('getShares')->willReturn([
['href' => 'principal:user2']
]);
- $calendarInfo = [
+ $addressBookInfo = [
'{http://owncloud.org/ns}owner-principal' => 'user1',
'{DAV:}displayname' => 'Test address book',
'principaluri' => 'user2',
'id' => 666,
'uri' => 'default',
];
- $l = $this->createMock(IL10N::class);
- $c = new AddressBook($backend, $calendarInfo, $l);
- $c->delete();
+ $l10n = $this->createMock(IL10N::class);
+ $logger = $this->createMock(LoggerInterface::class);
+ $addressBook = new AddressBook($backend, $addressBookInfo, $l10n);
+ $addressBook->delete();
}
- public function testDeleteFromGroup() {
- $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ public function testDeleteFromGroup(): void {
+ $this->expectException(Forbidden::class);
- /** @var \PHPUnit\Framework\MockObject\MockObject | CardDavBackend $backend */
- $backend = $this->getMockBuilder(CardDavBackend::class)->disableOriginalConstructor()->getMock();
+ /** @var MockObject | CardDavBackend $backend */
+ $backend = $this->createMock(CardDavBackend::class);
$backend->expects($this->never())->method('updateShares');
$backend->expects($this->any())->method('getShares')->willReturn([
['href' => 'principal:group2']
]);
- $calendarInfo = [
+ $addressBookInfo = [
'{http://owncloud.org/ns}owner-principal' => 'user1',
'{DAV:}displayname' => 'Test address book',
'principaluri' => 'user2',
'id' => 666,
'uri' => 'default',
];
- $l = $this->createMock(IL10N::class);
- $c = new AddressBook($backend, $calendarInfo, $l);
- $c->delete();
+ $l10n = $this->createMock(IL10N::class);
+ $logger = $this->createMock(LoggerInterface::class);
+ $addressBook = new AddressBook($backend, $addressBookInfo, $l10n);
+ $addressBook->delete();
}
- public function testPropPatch() {
- $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
-
- /** @var \PHPUnit\Framework\MockObject\MockObject | CardDavBackend $backend */
- $backend = $this->getMockBuilder(CardDavBackend::class)->disableOriginalConstructor()->getMock();
- $calendarInfo = [
+ public function testPropPatchShared(): void {
+ /** @var MockObject | CardDavBackend $backend */
+ $backend = $this->createMock(CardDavBackend::class);
+ $backend->expects($this->never())->method('updateAddressBook');
+ $addressBookInfo = [
'{http://owncloud.org/ns}owner-principal' => 'user1',
'{DAV:}displayname' => 'Test address book',
'principaluri' => 'user2',
'id' => 666,
'uri' => 'default',
];
- $l = $this->createMock(IL10N::class);
- $c = new AddressBook($backend, $calendarInfo, $l);
- $c->propPatch(new PropPatch([]));
+ $l10n = $this->createMock(IL10N::class);
+ $logger = $this->createMock(LoggerInterface::class);
+ $addressBook = new AddressBook($backend, $addressBookInfo, $l10n);
+ $addressBook->propPatch(new PropPatch(['{DAV:}displayname' => 'Test address book']));
}
- /**
- * @dataProvider providesReadOnlyInfo
- */
- public function testAcl($expectsWrite, $readOnlyValue, $hasOwnerSet) {
- /** @var \PHPUnit\Framework\MockObject\MockObject | CardDavBackend $backend */
- $backend = $this->getMockBuilder(CardDavBackend::class)->disableOriginalConstructor()->getMock();
+ public function testPropPatchNotShared(): void {
+ /** @var MockObject | CardDavBackend $backend */
+ $backend = $this->createMock(CardDavBackend::class);
+ $backend->expects($this->atLeast(1))->method('updateAddressBook');
+ $addressBookInfo = [
+ '{DAV:}displayname' => 'Test address book',
+ 'principaluri' => 'user1',
+ 'id' => 666,
+ 'uri' => 'default',
+ ];
+ $l10n = $this->createMock(IL10N::class);
+ $logger = $this->createMock(LoggerInterface::class);
+ $addressBook = new AddressBook($backend, $addressBookInfo, $l10n);
+ $addressBook->propPatch(new PropPatch(['{DAV:}displayname' => 'Test address book']));
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesReadOnlyInfo')]
+ public function testAcl(bool $expectsWrite, ?bool $readOnlyValue, bool $hasOwnerSet): void {
+ /** @var MockObject | CardDavBackend $backend */
+ $backend = $this->createMock(CardDavBackend::class);
$backend->expects($this->any())->method('applyShareAcl')->willReturnArgument(1);
- $calendarInfo = [
+ $addressBookInfo = [
'{DAV:}displayname' => 'Test address book',
'principaluri' => 'user2',
'id' => 666,
'uri' => 'default'
];
if (!is_null($readOnlyValue)) {
- $calendarInfo['{http://owncloud.org/ns}read-only'] = $readOnlyValue;
+ $addressBookInfo['{http://owncloud.org/ns}read-only'] = $readOnlyValue;
}
if ($hasOwnerSet) {
- $calendarInfo['{http://owncloud.org/ns}owner-principal'] = 'user1';
+ $addressBookInfo['{http://owncloud.org/ns}owner-principal'] = 'user1';
}
- $l = $this->createMock(IL10N::class);
- $c = new AddressBook($backend, $calendarInfo, $l);
- $acl = $c->getACL();
- $childAcl = $c->getChildACL();
+ $l10n = $this->createMock(IL10N::class);
+ $logger = $this->createMock(LoggerInterface::class);
+ $addressBook = new AddressBook($backend, $addressBookInfo, $l10n);
+ $acl = $addressBook->getACL();
+ $childAcl = $addressBook->getChildACL();
$expectedAcl = [[
'privilege' => '{DAV:}read',
@@ -123,6 +148,10 @@ class AddressBookTest extends TestCase {
'privilege' => '{DAV:}write',
'principal' => $hasOwnerSet ? 'user1' : 'user2',
'protected' => true
+ ], [
+ 'privilege' => '{DAV:}write-properties',
+ 'principal' => $hasOwnerSet ? 'user1' : 'user2',
+ 'protected' => true
]];
if ($hasOwnerSet) {
$expectedAcl[] = [
@@ -142,7 +171,7 @@ class AddressBookTest extends TestCase {
$this->assertEquals($expectedAcl, $childAcl);
}
- public function providesReadOnlyInfo() {
+ public static function providesReadOnlyInfo(): array {
return [
'read-only property not set' => [true, null, true],
'read-only property is false' => [true, false, true],
diff --git a/apps/dav/tests/unit/CardDAV/BirthdayServiceTest.php b/apps/dav/tests/unit/CardDAV/BirthdayServiceTest.php
index bb45c88b10d..6908dfd17bc 100644
--- a/apps/dav/tests/unit/CardDAV/BirthdayServiceTest.php
+++ b/apps/dav/tests/unit/CardDAV/BirthdayServiceTest.php
@@ -1,28 +1,10 @@
<?php
+
+declare(strict_types=1);
/**
- * @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 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\Tests\unit\CardDAV;
@@ -33,26 +15,19 @@ use OCA\DAV\DAV\GroupPrincipalBackend;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IL10N;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Reader;
use Test\TestCase;
class BirthdayServiceTest extends TestCase {
-
- /** @var BirthdayService */
- private $service;
- /** @var CalDavBackend | \PHPUnit\Framework\MockObject\MockObject */
- private $calDav;
- /** @var CardDavBackend | \PHPUnit\Framework\MockObject\MockObject */
- private $cardDav;
- /** @var GroupPrincipalBackend | \PHPUnit\Framework\MockObject\MockObject */
- private $groupPrincipalBackend;
- /** @var IConfig | \PHPUnit\Framework\MockObject\MockObject */
- private $config;
- /** @var IDBConnection | \PHPUnit\Framework\MockObject\MockObject */
- private $dbConnection;
- /** @var IL10N | \PHPUnit\Framework\MockObject\MockObject */
- private $l10n;
+ private CalDavBackend&MockObject $calDav;
+ private CardDavBackend&MockObject $cardDav;
+ private GroupPrincipalBackend&MockObject $groupPrincipalBackend;
+ private IConfig&MockObject $config;
+ private IDBConnection&MockObject $dbConnection;
+ private IL10N&MockObject $l10n;
+ private BirthdayService $service;
protected function setUp(): void {
parent::setUp();
@@ -70,23 +45,15 @@ class BirthdayServiceTest extends TestCase {
return vsprintf($string, $args);
});
- $this->service = new BirthdayService($this->calDav,$this->cardDav,
+ $this->service = new BirthdayService($this->calDav, $this->cardDav,
$this->groupPrincipalBackend, $this->config,
$this->dbConnection, $this->l10n);
}
- /**
- * @dataProvider providesVCards
- * @param string $expectedSummary
- * @param string $expectedDTStart
- * @param string $expectedFieldType
- * @param string $expectedUnknownYear
- * @param string $expectedOriginalYear
- * @param string | null $data
- */
- public function testBuildBirthdayFromContact($expectedSummary, $expectedDTStart, $expectedFieldType, $expectedUnknownYear, $expectedOriginalYear, $data, $fieldType, $prefix, $supports4Bytes) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesVCards')]
+ public function testBuildBirthdayFromContact(?string $expectedSummary, ?string $expectedDTStart, ?string $expectedRrule, ?string $expectedFieldType, ?string $expectedUnknownYear, ?string $expectedOriginalYear, ?string $expectedReminder, ?string $data, string $fieldType, string $prefix, bool $supports4Bytes, ?string $configuredReminder): void {
$this->dbConnection->method('supports4ByteText')->willReturn($supports4Bytes);
- $cal = $this->service->buildDateFromContact($data, $fieldType, $prefix);
+ $cal = $this->service->buildDateFromContact($data, $fieldType, $prefix, $configuredReminder);
if ($expectedSummary === null) {
$this->assertNull($cal);
@@ -94,7 +61,7 @@ class BirthdayServiceTest extends TestCase {
$this->assertInstanceOf('Sabre\VObject\Component\VCalendar', $cal);
$this->assertEquals('-//IDN nextcloud.com//Birthday calendar//EN', $cal->PRODID->getValue());
$this->assertTrue(isset($cal->VEVENT));
- $this->assertEquals('FREQ=YEARLY', $cal->VEVENT->RRULE->getValue());
+ $this->assertEquals($expectedRrule, $cal->VEVENT->RRULE->getValue());
$this->assertEquals($expectedSummary, $cal->VEVENT->SUMMARY->getValue());
$this->assertEquals($expectedDTStart, $cal->VEVENT->DTSTART->getValue());
$this->assertEquals($expectedFieldType, $cal->VEVENT->{'X-NEXTCLOUD-BC-FIELD-TYPE'}->getValue());
@@ -104,11 +71,16 @@ class BirthdayServiceTest extends TestCase {
$this->assertEquals($expectedOriginalYear, $cal->VEVENT->{'X-NEXTCLOUD-BC-YEAR'}->getValue());
}
+ if ($expectedReminder) {
+ $this->assertEquals($expectedReminder, $cal->VEVENT->VALARM->TRIGGER->getValue());
+ $this->assertEquals('DURATION', $cal->VEVENT->VALARM->TRIGGER->getValueType());
+ }
+
$this->assertEquals('TRANSPARENT', $cal->VEVENT->TRANSP->getValue());
}
}
- public function testOnCardDeleteGloballyDisabled() {
+ public function testOnCardDeleteGloballyDisabled(): void {
$this->config->expects($this->once())
->method('getAppValue')
->with('dav', 'generateBirthdayCalendar', 'yes')
@@ -119,7 +91,7 @@ class BirthdayServiceTest extends TestCase {
$this->service->onCardDeleted(666, 'gump.vcf');
}
- public function testOnCardDeleteUserDisabled() {
+ public function testOnCardDeleteUserDisabled(): void {
$this->config->expects($this->once())
->method('getAppValue')
->with('dav', 'generateBirthdayCalendar', 'yes')
@@ -143,7 +115,7 @@ class BirthdayServiceTest extends TestCase {
$this->service->onCardDeleted(666, 'gump.vcf');
}
- public function testOnCardDeleted() {
+ public function testOnCardDeleted(): void {
$this->config->expects($this->once())
->method('getAppValue')
->with('dav', 'generateBirthdayCalendar', 'yes')
@@ -165,15 +137,23 @@ class BirthdayServiceTest extends TestCase {
->willReturn([
'id' => 1234
]);
- $this->calDav->expects($this->at(1))->method('deleteCalendarObject')->with(1234, 'default-gump.vcf.ics');
- $this->calDav->expects($this->at(2))->method('deleteCalendarObject')->with(1234, 'default-gump.vcf-death.ics');
- $this->calDav->expects($this->at(3))->method('deleteCalendarObject')->with(1234, 'default-gump.vcf-anniversary.ics');
+ $calls = [
+ [1234, 'default-gump.vcf.ics'],
+ [1234, 'default-gump.vcf-death.ics'],
+ [1234, 'default-gump.vcf-anniversary.ics'],
+ ];
+ $this->calDav->expects($this->exactly(count($calls)))
+ ->method('deleteCalendarObject')
+ ->willReturnCallback(function ($calendarId, $objectUri) use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, [$calendarId, $objectUri]);
+ });
$this->cardDav->expects($this->once())->method('getShares')->willReturn([]);
$this->service->onCardDeleted(666, 'gump.vcf');
}
- public function testOnCardChangedGloballyDisabled() {
+ public function testOnCardChangedGloballyDisabled(): void {
$this->config->expects($this->once())
->method('getAppValue')
->with('dav', 'generateBirthdayCalendar', 'yes')
@@ -182,14 +162,14 @@ class BirthdayServiceTest extends TestCase {
$this->cardDav->expects($this->never())->method('getAddressBookById');
$service = $this->getMockBuilder(BirthdayService::class)
- ->setMethods(['buildDateFromContact', 'birthdayEvenChanged'])
+ ->onlyMethods(['buildDateFromContact', 'birthdayEvenChanged'])
->setConstructorArgs([$this->calDav, $this->cardDav, $this->groupPrincipalBackend, $this->config, $this->dbConnection, $this->l10n])
->getMock();
$service->onCardChanged(666, 'gump.vcf', '');
}
- public function testOnCardChangedUserDisabled() {
+ public function testOnCardChangedUserDisabled(): void {
$this->config->expects($this->once())
->method('getAppValue')
->with('dav', 'generateBirthdayCalendar', 'yes')
@@ -209,28 +189,28 @@ class BirthdayServiceTest extends TestCase {
$this->cardDav->expects($this->once())->method('getShares')->willReturn([]);
$this->calDav->expects($this->never())->method('getCalendarByUri');
- /** @var BirthdayService | \PHPUnit\Framework\MockObject\MockObject $service */
+ /** @var BirthdayService&MockObject $service */
$service = $this->getMockBuilder(BirthdayService::class)
- ->setMethods(['buildDateFromContact', 'birthdayEvenChanged'])
+ ->onlyMethods(['buildDateFromContact', 'birthdayEvenChanged'])
->setConstructorArgs([$this->calDav, $this->cardDav, $this->groupPrincipalBackend, $this->config, $this->dbConnection, $this->l10n])
->getMock();
$service->onCardChanged(666, 'gump.vcf', '');
}
- /**
- * @dataProvider providesCardChanges
- */
- public function testOnCardChanged($expectedOp) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesCardChanges')]
+ public function testOnCardChanged(string $expectedOp): void {
$this->config->expects($this->once())
->method('getAppValue')
->with('dav', 'generateBirthdayCalendar', 'yes')
->willReturn('yes');
- $this->config->expects($this->once())
+ $this->config->expects($this->exactly(2))
->method('getUserValue')
- ->with('user01', 'dav', 'generateBirthdayCalendar', 'yes')
- ->willReturn('yes');
+ ->willReturnMap([
+ ['user01', 'dav', 'generateBirthdayCalendar', 'yes', 'yes'],
+ ['user01', 'dav', 'birthdayCalendarReminderOffset', 'PT9H', 'PT9H'],
+ ]);
$this->cardDav->expects($this->once())->method('getAddressBookById')
->with(666)
@@ -245,31 +225,45 @@ class BirthdayServiceTest extends TestCase {
]);
$this->cardDav->expects($this->once())->method('getShares')->willReturn([]);
- /** @var BirthdayService | \PHPUnit\Framework\MockObject\MockObject $service */
+ /** @var BirthdayService&MockObject $service */
$service = $this->getMockBuilder(BirthdayService::class)
- ->setMethods(['buildDateFromContact', 'birthdayEvenChanged'])
+ ->onlyMethods(['buildDateFromContact', 'birthdayEvenChanged'])
->setConstructorArgs([$this->calDav, $this->cardDav, $this->groupPrincipalBackend, $this->config, $this->dbConnection, $this->l10n])
->getMock();
if ($expectedOp === 'delete') {
$this->calDav->expects($this->exactly(3))->method('getCalendarObject')->willReturn('');
$service->expects($this->exactly(3))->method('buildDateFromContact')->willReturn(null);
- $this->calDav->expects($this->exactly(3))->method('deleteCalendarObject')->withConsecutive(
+
+ $calls = [
[1234, 'default-gump.vcf.ics'],
[1234, 'default-gump.vcf-death.ics'],
[1234, 'default-gump.vcf-anniversary.ics']
- );
+ ];
+ $this->calDav->expects($this->exactly(count($calls)))
+ ->method('deleteCalendarObject')
+ ->willReturnCallback(function ($calendarId, $objectUri) use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, [$calendarId, $objectUri]);
+ });
}
if ($expectedOp === 'create') {
$vCal = new VCalendar();
$vCal->PRODID = '-//Nextcloud testing//mocked object//';
$service->expects($this->exactly(3))->method('buildDateFromContact')->willReturn($vCal);
- $this->calDav->expects($this->exactly(3))->method('createCalendarObject')->withConsecutive(
+
+ $createCalendarObjectCalls = [
[1234, 'default-gump.vcf.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"],
[1234, 'default-gump.vcf-death.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"],
[1234, 'default-gump.vcf-anniversary.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"]
- );
+ ];
+ $this->calDav->expects($this->exactly(count($createCalendarObjectCalls)))
+ ->method('createCalendarObject')
+ ->willReturnCallback(function ($calendarId, $objectUri, $calendarData) use (&$createCalendarObjectCalls): void {
+ $expected = array_shift($createCalendarObjectCalls);
+ $this->assertEquals($expected, [$calendarId, $objectUri, $calendarData]);
+ });
}
if ($expectedOp === 'update') {
$vCal = new VCalendar();
@@ -278,28 +272,30 @@ class BirthdayServiceTest extends TestCase {
$service->expects($this->exactly(3))->method('buildDateFromContact')->willReturn($vCal);
$service->expects($this->exactly(3))->method('birthdayEvenChanged')->willReturn(true);
$this->calDav->expects($this->exactly(3))->method('getCalendarObject')->willReturn(['calendardata' => '']);
- $this->calDav->expects($this->exactly(3))->method('updateCalendarObject')->withConsecutive(
+
+ $updateCalendarObjectCalls = [
[1234, 'default-gump.vcf.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"],
[1234, 'default-gump.vcf-death.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"],
[1234, 'default-gump.vcf-anniversary.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"]
- );
+ ];
+ $this->calDav->expects($this->exactly(count($updateCalendarObjectCalls)))
+ ->method('updateCalendarObject')
+ ->willReturnCallback(function ($calendarId, $objectUri, $calendarData) use (&$updateCalendarObjectCalls): void {
+ $expected = array_shift($updateCalendarObjectCalls);
+ $this->assertEquals($expected, [$calendarId, $objectUri, $calendarData]);
+ });
}
$service->onCardChanged(666, 'gump.vcf', '');
}
- /**
- * @dataProvider providesBirthday
- * @param $expected
- * @param $old
- * @param $new
- */
- public function testBirthdayEvenChanged($expected, $old, $new) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesBirthday')]
+ public function testBirthdayEvenChanged(bool $expected, string $old, string $new): void {
$new = Reader::read($new);
$this->assertEquals($expected, $this->service->birthdayEvenChanged($old, $new));
}
- public function testGetAllAffectedPrincipals() {
+ public function testGetAllAffectedPrincipals(): void {
$this->cardDav->expects($this->once())->method('getShares')->willReturn([
[
'{http://owncloud.org/ns}group-share' => false,
@@ -338,7 +334,7 @@ class BirthdayServiceTest extends TestCase {
], $users);
}
- public function testBirthdayCalendarHasComponentEvent() {
+ public function testBirthdayCalendarHasComponentEvent(): void {
$this->calDav->expects($this->once())
->method('createCalendar')
->with('principal001', 'contact_birthdays', [
@@ -349,33 +345,33 @@ class BirthdayServiceTest extends TestCase {
$this->service->ensureCalendarExists('principal001');
}
- public function testResetForUser() {
- $this->calDav->expects($this->at(0))
+ public function testResetForUser(): void {
+ $this->calDav->expects($this->once())
->method('getCalendarByUri')
->with('principals/users/user123', 'contact_birthdays')
->willReturn(['id' => 42]);
- $this->calDav->expects($this->at(1))
+ $this->calDav->expects($this->once())
->method('getCalendarObjects')
->with(42, 0)
->willReturn([['uri' => '1.ics'], ['uri' => '2.ics'], ['uri' => '3.ics']]);
- $this->calDav->expects($this->at(2))
- ->method('deleteCalendarObject')
- ->with(42, '1.ics', 0);
-
- $this->calDav->expects($this->at(3))
- ->method('deleteCalendarObject')
- ->with(42, '2.ics', 0);
-
- $this->calDav->expects($this->at(4))
+ $calls = [
+ [42, '1.ics', 0],
+ [42, '2.ics', 0],
+ [42, '3.ics', 0],
+ ];
+ $this->calDav->expects($this->exactly(count($calls)))
->method('deleteCalendarObject')
- ->with(42, '3.ics', 0);
+ ->willReturnCallback(function ($calendarId, $objectUri, $calendarType) use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, [$calendarId, $objectUri, $calendarType]);
+ });
$this->service->resetForUser('user123');
}
- public function providesBirthday() {
+ public static function providesBirthday(): array {
return [
[true,
'',
@@ -392,7 +388,7 @@ class BirthdayServiceTest extends TestCase {
];
}
- public function providesCardChanges() {
+ public static function providesCardChanges(): array {
return[
['delete'],
['create'],
@@ -400,31 +396,38 @@ class BirthdayServiceTest extends TestCase {
];
}
- public function providesVCards() {
+ public static function providesVCards(): array {
return [
- // $expectedSummary, $expectedDTStart, $expectedFieldType, $expectedUnknownYear, $expectedOriginalYear, $data, $fieldType, $prefix, $supports4Byte
- [null, null, null, null, null, 'yasfewf', '', '', true],
- [null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n", 'BDAY', '', true],
- [null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:\r\nEND:VCARD\r\n", 'BDAY', '', true],
- [null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:someday\r\nEND:VCARD\r\n", 'BDAY', '', true],
- ['🎂 12345 (1900)', '19700101', 'BDAY', '0', '1900', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19000101\r\nEND:VCARD\r\n", 'BDAY', '', true],
- ['🎂 12345 (1900)', '19701231', 'BDAY', '0', '1900', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19001231\r\nEND:VCARD\r\n", 'BDAY', '', true],
- ['Death of 12345 (1900)', '19701231', 'DEATHDATE', '0', '1900', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nDEATHDATE:19001231\r\nEND:VCARD\r\n", 'DEATHDATE', '-death', true],
- ['Death of 12345 (1900)', '19701231', 'DEATHDATE', '0', '1900', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nDEATHDATE:19001231\r\nEND:VCARD\r\n", 'DEATHDATE', '-death', false],
- ['💍 12345 (1900)', '19701231', 'ANNIVERSARY', '0', '1900', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nANNIVERSARY:19001231\r\nEND:VCARD\r\n", 'ANNIVERSARY', '-anniversary', true],
- ['12345 (⚭1900)', '19701231', 'ANNIVERSARY', '0', '1900', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nANNIVERSARY:19001231\r\nEND:VCARD\r\n", 'ANNIVERSARY', '-anniversary', false],
- ['🎂 12345', '19701231', 'BDAY', '1', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:--1231\r\nEND:VCARD\r\n", 'BDAY', '', true],
- ['🎂 12345', '19701231', 'BDAY', '1', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY;X-APPLE-OMIT-YEAR=1604:16041231\r\nEND:VCARD\r\n", 'BDAY', '', true],
- [null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:;VALUE=text:circa 1800\r\nEND:VCARD\r\n", 'BDAY', '', true],
- [null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nN:12345;;;;\r\nBDAY:20031231\r\nEND:VCARD\r\n", 'BDAY', '', true],
- ['🎂 12345 (900)', '19701231', 'BDAY', '0', '900', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:09001231\r\nEND:VCARD\r\n", 'BDAY', '', true],
- ['12345 (*1900)', '19700101', 'BDAY', '0', '1900', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19000101\r\nEND:VCARD\r\n", 'BDAY', '', false],
- ['12345 (*1900)', '19701231', 'BDAY', '0', '1900', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19001231\r\nEND:VCARD\r\n", 'BDAY', '', false],
- ['12345 *', '19701231', 'BDAY', '1', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:--1231\r\nEND:VCARD\r\n", 'BDAY', '', false],
- ['12345 *', '19701231', 'BDAY', '1', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY;X-APPLE-OMIT-YEAR=1604:16041231\r\nEND:VCARD\r\n", 'BDAY', '', false],
- [null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:;VALUE=text:circa 1800\r\nEND:VCARD\r\n", 'BDAY', '', false],
- [null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nN:12345;;;;\r\nBDAY:20031231\r\nEND:VCARD\r\n", 'BDAY', '', false],
- ['12345 (*900)', '19701231', 'BDAY', '0', '900', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:09001231\r\nEND:VCARD\r\n", 'BDAY', '', false],
+ // $expectedSummary, $expectedDTStart, $expectedRrule, $expectedFieldType, $expectedUnknownYear, $expectedOriginalYear, $expectedReminder, $data, $fieldType, $prefix, $supports4Byte, $configuredReminder
+ [null, null, null, null, null, null, null, 'yasfewf', '', '', true, null],
+ [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
+ [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
+ [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:someday\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
+ ['🎂 12345 (1900)', '19700101', 'FREQ=YEARLY', 'BDAY', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19000101\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
+ ['🎂 12345 (1900)', '19701231', 'FREQ=YEARLY', 'BDAY', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19001231\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
+ ['Death of 12345 (1900)', '19701231', 'FREQ=YEARLY', 'DEATHDATE', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nDEATHDATE:19001231\r\nEND:VCARD\r\n", 'DEATHDATE', '-death', true, null],
+ ['Death of 12345 (1900)', '19701231', 'FREQ=YEARLY', 'DEATHDATE', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nDEATHDATE:19001231\r\nEND:VCARD\r\n", 'DEATHDATE', '-death', false, null],
+ ['💍 12345 (1900)', '19701231', 'FREQ=YEARLY', 'ANNIVERSARY', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nANNIVERSARY:19001231\r\nEND:VCARD\r\n", 'ANNIVERSARY', '-anniversary', true, null],
+ ['12345 (⚭1900)', '19701231', 'FREQ=YEARLY', 'ANNIVERSARY', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nANNIVERSARY:19001231\r\nEND:VCARD\r\n", 'ANNIVERSARY', '-anniversary', false, null],
+ ['🎂 12345', '19701231', 'FREQ=YEARLY', 'BDAY', '1', null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:--1231\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
+ ['🎂 12345', '19701231', 'FREQ=YEARLY', 'BDAY', '1', null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY;X-APPLE-OMIT-YEAR=1604:16041231\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
+ [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:;VALUE=text:circa 1800\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
+ [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nN:12345;;;;\r\nBDAY:20031231\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
+ ['🎂 12345 (900)', '19701231', 'FREQ=YEARLY', 'BDAY', '0', '900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:09001231\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
+ ['12345 (*1900)', '19700101', 'FREQ=YEARLY', 'BDAY', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19000101\r\nEND:VCARD\r\n", 'BDAY', '', false, null],
+ ['12345 (*1900)', '19701231', 'FREQ=YEARLY', 'BDAY', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19001231\r\nEND:VCARD\r\n", 'BDAY', '', false, null],
+ ['12345 *', '19701231', 'FREQ=YEARLY', 'BDAY', '1', null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:--1231\r\nEND:VCARD\r\n", 'BDAY', '', false, null],
+ ['12345 *', '19701231', 'FREQ=YEARLY', 'BDAY', '1', null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY;X-APPLE-OMIT-YEAR=1604:16041231\r\nEND:VCARD\r\n", 'BDAY', '', false, null],
+ [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:;VALUE=text:circa 1800\r\nEND:VCARD\r\n", 'BDAY', '', false, null],
+ [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nN:12345;;;;\r\nBDAY:20031231\r\nEND:VCARD\r\n", 'BDAY', '', false, null],
+ ['12345 (*900)', '19701231', 'FREQ=YEARLY', 'BDAY', '0', '900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:09001231\r\nEND:VCARD\r\n", 'BDAY', '', false, null],
+ ['12345 (*1900)', '19701231', 'FREQ=YEARLY', 'BDAY', '0', '1900', 'PT9H', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19001231\r\nEND:VCARD\r\n", 'BDAY', '', false, 'PT9H'],
+ ['12345 (*1900)', '19701231', 'FREQ=YEARLY', 'BDAY', '0', '1900', '-PT15H', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19001231\r\nEND:VCARD\r\n", 'BDAY', '', false, '-PT15H'],
+ ['12345 (*1900)', '19701231', 'FREQ=YEARLY', 'BDAY', '0', '1900', '-P6DT15H', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19001231\r\nEND:VCARD\r\n", 'BDAY', '', false, '-P6DT15H'],
+ [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19000101\r\nX-NC-EXCLUDE-FROM-BIRTHDAY-CALENDAR;TYPE=boolean:true\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
+ [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nX-NC-EXCLUDE-FROM-BIRTHDAY-CALENDAR;TYPE=boolean:true\r\nDEATHDATE:19001231\r\nEND:VCARD\r\n", 'DEATHDATE', '-death', true, null],
+ [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nANNIVERSARY:19001231\r\nX-NC-EXCLUDE-FROM-BIRTHDAY-CALENDAR;TYPE=boolean:true\r\nEND:VCARD\r\n", 'ANNIVERSARY', '-anniversary', true, null],
+ ['🎂 12345 (1902)', '19720229', 'FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=-1', 'BDAY', '0', null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19020229\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
];
}
}
diff --git a/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php b/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php
index 7eda691d199..c5eafa0764a 100644
--- a/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php
+++ b/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php
@@ -1,34 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arne Hamann <kontakt+github@arne.email>
- * @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 Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.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>
- *
- * @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\Tests\unit\CardDAV;
@@ -36,10 +11,15 @@ use OC\KnownUser\KnownUserService;
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
use OCA\DAV\CardDAV\AddressBook;
use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\CardDAV\Sharing\Backend;
+use OCA\DAV\CardDAV\Sharing\Service;
use OCA\DAV\Connector\Sabre\Principal;
+use OCA\DAV\DAV\Sharing\SharingMapper;
+use OCP\Accounts\IAccountManager;
use OCP\App\IAppManager;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\EventDispatcher\IEventDispatcher;
+use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IGroupManager;
@@ -47,14 +27,16 @@ use OCP\IL10N;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\L10N\IFactory;
+use OCP\Server;
use OCP\Share\IManager as ShareManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\PropPatch;
use Sabre\VObject\Component\VCard;
use Sabre\VObject\Property\Text;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
-use Symfony\Component\EventDispatcher\GenericEvent;
use Test\TestCase;
+use function time;
/**
* Class CardDavBackendTest
@@ -64,87 +46,72 @@ use Test\TestCase;
* @package OCA\DAV\Tests\unit\CardDAV
*/
class CardDavBackendTest extends TestCase {
-
- /** @var CardDavBackend */
- private $backend;
-
- /** @var Principal | \PHPUnit\Framework\MockObject\MockObject */
- private $principal;
-
- /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */
- private $userManager;
-
- /** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject */
- private $groupManager;
-
- /** @var EventDispatcherInterface|\PHPUnit\Framework\MockObject\MockObject */
- private $legacyDispatcher;
-
- /** @var IEventDispatcher|\PHPUnit\Framework\MockObject\MockObject */
- private $dispatcher;
-
- /** @var IDBConnection */
- private $db;
-
- /** @var string */
- private $dbCardsTable = 'cards';
-
- /** @var string */
- private $dbCardsPropertiesTable = 'cards_properties';
+ private Principal&MockObject $principal;
+ private IUserManager&MockObject $userManager;
+ private IGroupManager&MockObject $groupManager;
+ private IEventDispatcher&MockObject $dispatcher;
+ private IConfig&MockObject $config;
+ private Backend $sharingBackend;
+ private IDBConnection $db;
+ private CardDavBackend $backend;
+ private string $dbCardsTable = 'cards';
+ private string $dbCardsPropertiesTable = 'cards_properties';
public const UNIT_TEST_USER = 'principals/users/carddav-unit-test';
public const UNIT_TEST_USER1 = 'principals/users/carddav-unit-test1';
public const UNIT_TEST_GROUP = 'principals/groups/carddav-unit-test-group';
- private $vcardTest0 = 'BEGIN:VCARD'.PHP_EOL.
- 'VERSION:3.0'.PHP_EOL.
- 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN'.PHP_EOL.
- 'UID:Test'.PHP_EOL.
- 'FN:Test'.PHP_EOL.
- 'N:Test;;;;'.PHP_EOL.
- 'END:VCARD';
-
- private $vcardTest1 = 'BEGIN:VCARD'.PHP_EOL.
- 'VERSION:3.0'.PHP_EOL.
- 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN'.PHP_EOL.
- 'UID:Test2'.PHP_EOL.
- 'FN:Test2'.PHP_EOL.
- 'N:Test2;;;;'.PHP_EOL.
- 'END:VCARD';
-
- private $vcardTest2 = 'BEGIN:VCARD'.PHP_EOL.
- 'VERSION:3.0'.PHP_EOL.
- 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN'.PHP_EOL.
- 'UID:Test3'.PHP_EOL.
- 'FN:Test3'.PHP_EOL.
- 'N:Test3;;;;'.PHP_EOL.
- 'END:VCARD';
-
- private $vcardTestNoUID = 'BEGIN:VCARD'.PHP_EOL.
- 'VERSION:3.0'.PHP_EOL.
- 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN'.PHP_EOL.
- 'FN:TestNoUID'.PHP_EOL.
- 'N:TestNoUID;;;;'.PHP_EOL.
- 'END:VCARD';
+ private $vcardTest0 = 'BEGIN:VCARD' . PHP_EOL
+ . 'VERSION:3.0' . PHP_EOL
+ . 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN' . PHP_EOL
+ . 'UID:Test' . PHP_EOL
+ . 'FN:Test' . PHP_EOL
+ . 'N:Test;;;;' . PHP_EOL
+ . 'END:VCARD';
+
+ private $vcardTest1 = 'BEGIN:VCARD' . PHP_EOL
+ . 'VERSION:3.0' . PHP_EOL
+ . 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN' . PHP_EOL
+ . 'UID:Test2' . PHP_EOL
+ . 'FN:Test2' . PHP_EOL
+ . 'N:Test2;;;;' . PHP_EOL
+ . 'END:VCARD';
+
+ private $vcardTest2 = 'BEGIN:VCARD' . PHP_EOL
+ . 'VERSION:3.0' . PHP_EOL
+ . 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN' . PHP_EOL
+ . 'UID:Test3' . PHP_EOL
+ . 'FN:Test3' . PHP_EOL
+ . 'N:Test3;;;;' . PHP_EOL
+ . 'END:VCARD';
+
+ private $vcardTestNoUID = 'BEGIN:VCARD' . PHP_EOL
+ . 'VERSION:3.0' . PHP_EOL
+ . 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN' . PHP_EOL
+ . 'FN:TestNoUID' . PHP_EOL
+ . 'N:TestNoUID;;;;' . PHP_EOL
+ . 'END:VCARD';
protected function setUp(): void {
parent::setUp();
$this->userManager = $this->createMock(IUserManager::class);
$this->groupManager = $this->createMock(IGroupManager::class);
+ $this->config = $this->createMock(IConfig::class);
$this->principal = $this->getMockBuilder(Principal::class)
->setConstructorArgs([
$this->userManager,
$this->groupManager,
+ $this->createMock(IAccountManager::class),
$this->createMock(ShareManager::class),
$this->createMock(IUserSession::class),
$this->createMock(IAppManager::class),
$this->createMock(ProxyMapper::class),
$this->createMock(KnownUserService::class),
- $this->createMock(IConfig::class),
+ $this->config,
$this->createMock(IFactory::class)
])
- ->setMethods(['getPrincipalByPath', 'getGroupMembership'])
+ ->onlyMethods(['getPrincipalByPath', 'getGroupMembership', 'findByUri'])
->getMock();
$this->principal->method('getPrincipalByPath')
->willReturn([
@@ -155,23 +122,39 @@ class CardDavBackendTest extends TestCase {
->withAnyParameters()
->willReturn([self::UNIT_TEST_GROUP]);
$this->dispatcher = $this->createMock(IEventDispatcher::class);
- $this->legacyDispatcher = $this->createMock(EventDispatcherInterface::class);
- $this->db = \OC::$server->getDatabaseConnection();
-
- $this->backend = new CardDavBackend($this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher, $this->legacyDispatcher);
+ $this->db = Server::get(IDBConnection::class);
+ $this->sharingBackend = new Backend($this->userManager,
+ $this->groupManager,
+ $this->principal,
+ $this->createMock(ICacheFactory::class),
+ new Service(new SharingMapper($this->db)),
+ $this->createMock(LoggerInterface::class)
+ );
+
+ $this->backend = new CardDavBackend($this->db,
+ $this->principal,
+ $this->userManager,
+ $this->dispatcher,
+ $this->sharingBackend,
+ $this->config,
+ );
// start every test with a empty cards_properties and cards table
$query = $this->db->getQueryBuilder();
- $query->delete('cards_properties')->execute();
+ $query->delete('cards_properties')->executeStatement();
$query = $this->db->getQueryBuilder();
- $query->delete('cards')->execute();
+ $query->delete('cards')->executeStatement();
- $this->tearDown();
+ $this->principal->method('getGroupMembership')
+ ->withAnyParameters()
+ ->willReturn([self::UNIT_TEST_GROUP]);
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ foreach ($books as $book) {
+ $this->backend->deleteAddressBook($book['id']);
+ }
}
protected function tearDown(): void {
- parent::tearDown();
-
if (is_null($this->backend)) {
return;
}
@@ -183,10 +166,11 @@ class CardDavBackendTest extends TestCase {
foreach ($books as $book) {
$this->backend->deleteAddressBook($book['id']);
}
- }
- public function testAddressBookOperations() {
+ parent::tearDown();
+ }
+ public function testAddressBookOperations(): void {
// create a new address book
$this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
@@ -196,7 +180,7 @@ class CardDavBackendTest extends TestCase {
$this->assertEquals('Example', $books[0]['{DAV:}displayname']);
$this->assertEquals('User\'s displayname', $books[0]['{http://nextcloud.com/ns}owner-displayname']);
- // update it's display name
+ // update its display name
$patch = new PropPatch([
'{DAV:}displayname' => 'Unit test',
'{urn:ietf:params:xml:ns:carddav}addressbook-description' => 'Addressbook used for unit testing'
@@ -214,14 +198,16 @@ class CardDavBackendTest extends TestCase {
$this->assertEquals(0, count($books));
}
- public function testAddressBookSharing() {
+ public function testAddressBookSharing(): void {
$this->userManager->expects($this->any())
->method('userExists')
->willReturn(true);
-
$this->groupManager->expects($this->any())
->method('groupExists')
->willReturn(true);
+ $this->principal->expects(self::atLeastOnce())
+ ->method('findByUri')
+ ->willReturnOnConsecutiveCalls(self::UNIT_TEST_USER1, self::UNIT_TEST_GROUP);
$this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
$books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
@@ -245,12 +231,12 @@ class CardDavBackendTest extends TestCase {
$this->assertEquals(0, count($books));
}
- public function testCardOperations() {
-
- /** @var CardDavBackend | \PHPUnit\Framework\MockObject\MockObject $backend */
+ public function testCardOperations(): void {
+ /** @var CardDavBackend&MockObject $backend */
$backend = $this->getMockBuilder(CardDavBackend::class)
- ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher, $this->legacyDispatcher])
- ->setMethods(['updateProperties', 'purgeProperties'])->getMock();
+ ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend,$this->config])
+ ->onlyMethods(['updateProperties', 'purgeProperties'])
+ ->getMock();
// create a new address book
$backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
@@ -260,17 +246,21 @@ class CardDavBackendTest extends TestCase {
$uri = $this->getUniqueID('card');
// updateProperties is expected twice, once for createCard and once for updateCard
- $backend->expects($this->at(0))->method('updateProperties')->with($bookId, $uri, $this->vcardTest0);
- $backend->expects($this->at(1))->method('updateProperties')->with($bookId, $uri, $this->vcardTest1);
+ $calls = [
+ [$bookId, $uri, $this->vcardTest0],
+ [$bookId, $uri, $this->vcardTest1],
+ ];
+ $backend->expects($this->exactly(count($calls)))
+ ->method('updateProperties')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
// Expect event
- $this->legacyDispatcher->expects($this->at(0))
- ->method('dispatch')
- ->with('\OCA\DAV\CardDAV\CardDavBackend::createCard', $this->callback(function (GenericEvent $e) use ($bookId, $uri) {
- return $e->getArgument('addressBookId') === $bookId &&
- $e->getArgument('cardUri') === $uri &&
- $e->getArgument('cardData') === $this->vcardTest0;
- }));
+ $this->dispatcher
+ ->expects($this->exactly(3))
+ ->method('dispatchTyped');
// create a card
$backend->createCard($bookId, $uri, $this->vcardTest0);
@@ -290,28 +280,11 @@ class CardDavBackendTest extends TestCase {
$this->assertArrayHasKey('size', $card);
$this->assertEquals($this->vcardTest0, $card['carddata']);
- // Expect event
- $this->legacyDispatcher->expects($this->at(0))
- ->method('dispatch')
- ->with('\OCA\DAV\CardDAV\CardDavBackend::updateCard', $this->callback(function (GenericEvent $e) use ($bookId, $uri) {
- return $e->getArgument('addressBookId') === $bookId &&
- $e->getArgument('cardUri') === $uri &&
- $e->getArgument('cardData') === $this->vcardTest1;
- }));
-
// update the card
$backend->updateCard($bookId, $uri, $this->vcardTest1);
$card = $backend->getCard($bookId, $uri);
$this->assertEquals($this->vcardTest1, $card['carddata']);
- // Expect event
- $this->legacyDispatcher->expects($this->at(0))
- ->method('dispatch')
- ->with('\OCA\DAV\CardDAV\CardDavBackend::deleteCard', $this->callback(function (GenericEvent $e) use ($bookId, $uri) {
- return $e->getArgument('addressBookId') === $bookId &&
- $e->getArgument('cardUri') === $uri;
- }));
-
// delete the card
$backend->expects($this->once())->method('purgeProperties')->with($bookId, $card['id']);
$backend->deleteCard($bookId, $uri);
@@ -319,10 +292,11 @@ class CardDavBackendTest extends TestCase {
$this->assertEquals(0, count($cards));
}
- public function testMultiCard() {
+ public function testMultiCard(): void {
$this->backend = $this->getMockBuilder(CardDavBackend::class)
- ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher, $this->legacyDispatcher])
- ->setMethods(['updateProperties'])->getMock();
+ ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend,$this->config])
+ ->onlyMethods(['updateProperties'])
+ ->getMock();
// create a new address book
$this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
@@ -361,7 +335,7 @@ class CardDavBackendTest extends TestCase {
$this->assertArrayHasKey('lastmodified', $card);
$this->assertArrayHasKey('etag', $card);
$this->assertArrayHasKey('size', $card);
- $this->assertEquals($this->{ 'vcardTest'.($index + 1) }, $card['carddata']);
+ $this->assertEquals($this->{ 'vcardTest' . ($index + 1) }, $card['carddata']);
}
// delete the card
@@ -372,10 +346,11 @@ class CardDavBackendTest extends TestCase {
$this->assertEquals(0, count($cards));
}
- public function testMultipleUIDOnDifferentAddressbooks() {
+ public function testMultipleUIDOnDifferentAddressbooks(): void {
$this->backend = $this->getMockBuilder(CardDavBackend::class)
- ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher, $this->legacyDispatcher])
- ->setMethods(['updateProperties'])->getMock();
+ ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend,$this->config])
+ ->onlyMethods(['updateProperties'])
+ ->getMock();
// create 2 new address books
$this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
@@ -394,10 +369,11 @@ class CardDavBackendTest extends TestCase {
$this->backend->createCard($bookId1, $uri1, $this->vcardTest0);
}
- public function testMultipleUIDDenied() {
+ public function testMultipleUIDDenied(): void {
$this->backend = $this->getMockBuilder(CardDavBackend::class)
- ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher, $this->legacyDispatcher])
- ->setMethods(['updateProperties'])->getMock();
+ ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config])
+ ->onlyMethods(['updateProperties'])
+ ->getMock();
// create a new address book
$this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
@@ -415,10 +391,11 @@ class CardDavBackendTest extends TestCase {
$test = $this->backend->createCard($bookId, $uri1, $this->vcardTest0);
}
- public function testNoValidUID() {
+ public function testNoValidUID(): void {
$this->backend = $this->getMockBuilder(CardDavBackend::class)
- ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher, $this->legacyDispatcher])
- ->setMethods(['updateProperties'])->getMock();
+ ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config])
+ ->onlyMethods(['updateProperties'])
+ ->getMock();
// create a new address book
$this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
@@ -432,10 +409,10 @@ class CardDavBackendTest extends TestCase {
$test = $this->backend->createCard($bookId, $uri1, $this->vcardTestNoUID);
}
- public function testDeleteWithoutCard() {
+ public function testDeleteWithoutCard(): void {
$this->backend = $this->getMockBuilder(CardDavBackend::class)
- ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher, $this->legacyDispatcher])
- ->setMethods([
+ ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config])
+ ->onlyMethods([
'getCardId',
'addChange',
'purgeProperties',
@@ -456,12 +433,17 @@ class CardDavBackendTest extends TestCase {
->method('getCardId')
->with($bookId, $uri)
->willThrowException(new \InvalidArgumentException());
- $this->backend->expects($this->exactly(2))
+
+ $calls = [
+ [$bookId, $uri, 1],
+ [$bookId, $uri, 3],
+ ];
+ $this->backend->expects($this->exactly(count($calls)))
->method('addChange')
- ->withConsecutive(
- [$bookId, $uri, 1],
- [$bookId, $uri, 3]
- );
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
$this->backend->expects($this->never())
->method('purgeProperties');
@@ -472,10 +454,11 @@ class CardDavBackendTest extends TestCase {
$this->assertTrue($this->backend->deleteCard($bookId, $uri));
}
- public function testSyncSupport() {
+ public function testSyncSupport(): void {
$this->backend = $this->getMockBuilder(CardDavBackend::class)
- ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher, $this->legacyDispatcher])
- ->setMethods(['updateProperties'])->getMock();
+ ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config])
+ ->onlyMethods(['updateProperties'])
+ ->getMock();
// create a new address book
$this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
@@ -496,14 +479,16 @@ class CardDavBackendTest extends TestCase {
$this->assertEquals($uri0, $changes['added'][0]);
}
- public function testSharing() {
+ public function testSharing(): void {
$this->userManager->expects($this->any())
->method('userExists')
->willReturn(true);
-
$this->groupManager->expects($this->any())
->method('groupExists')
->willReturn(true);
+ $this->principal->expects(self::any())
+ ->method('findByUri')
+ ->willReturn(self::UNIT_TEST_USER1);
$this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
$books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
@@ -534,14 +519,14 @@ class CardDavBackendTest extends TestCase {
$this->assertEquals(0, count($books));
}
- public function testUpdateProperties() {
+ public function testUpdateProperties(): void {
$bookId = 42;
$cardUri = 'card-uri';
$cardId = 2;
$backend = $this->getMockBuilder(CardDavBackend::class)
- ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher, $this->legacyDispatcher])
- ->setMethods(['getCardId'])->getMock();
+ ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config])
+ ->onlyMethods(['getCardId'])->getMock();
$backend->expects($this->any())->method('getCardId')->willReturn($cardId);
@@ -553,7 +538,8 @@ class CardDavBackendTest extends TestCase {
$query = $this->db->getQueryBuilder();
$query->select('*')
- ->from('cards_properties');
+ ->from('cards_properties')
+ ->orderBy('name');
$qResult = $query->execute();
$result = $qResult->fetchAll();
@@ -561,13 +547,13 @@ class CardDavBackendTest extends TestCase {
$this->assertSame(2, count($result));
- $this->assertSame('UID', $result[0]['name']);
- $this->assertSame($cardUri, $result[0]['value']);
+ $this->assertSame('FN', $result[0]['name']);
+ $this->assertSame('John Doe', $result[0]['value']);
$this->assertSame($bookId, (int)$result[0]['addressbookid']);
$this->assertSame($cardId, (int)$result[0]['cardid']);
- $this->assertSame('FN', $result[1]['name']);
- $this->assertSame('John Doe', $result[1]['value']);
+ $this->assertSame('UID', $result[1]['name']);
+ $this->assertSame($cardUri, $result[1]['value']);
$this->assertSame($bookId, (int)$result[1]['addressbookid']);
$this->assertSame($cardId, (int)$result[1]['cardid']);
@@ -592,7 +578,7 @@ class CardDavBackendTest extends TestCase {
$this->assertSame($cardId, (int)$result[0]['cardid']);
}
- public function testPurgeProperties() {
+ public function testPurgeProperties(): void {
$query = $this->db->getQueryBuilder();
$query->insert('cards_properties')
->values(
@@ -630,11 +616,11 @@ class CardDavBackendTest extends TestCase {
$qResult->closeCursor();
$this->assertSame(1, count($result));
- $this->assertSame(1 ,(int)$result[0]['addressbookid']);
- $this->assertSame(2 ,(int)$result[0]['cardid']);
+ $this->assertSame(1, (int)$result[0]['addressbookid']);
+ $this->assertSame(2, (int)$result[0]['cardid']);
}
- public function testGetCardId() {
+ public function testGetCardId(): void {
$query = $this->db->getQueryBuilder();
$query->insert('cards')
@@ -656,21 +642,14 @@ class CardDavBackendTest extends TestCase {
}
- public function testGetCardIdFailed() {
+ public function testGetCardIdFailed(): void {
$this->expectException(\InvalidArgumentException::class);
$this->invokePrivate($this->backend, 'getCardId', [1, 'uri']);
}
- /**
- * @dataProvider dataTestSearch
- *
- * @param string $pattern
- * @param array $properties
- * @param array $options
- * @param array $expected
- */
- public function testSearch($pattern, $properties, $options, $expected) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTestSearch')]
+ public function testSearch(string $pattern, array $properties, array $options, array $expected): void {
/** @var VCard $vCards */
$vCards = [];
$vCards[0] = new VCard();
@@ -689,20 +668,21 @@ class CardDavBackendTest extends TestCase {
$query = $this->db->getQueryBuilder();
for ($i = 0; $i < 3; $i++) {
$query->insert($this->dbCardsTable)
- ->values(
- [
- 'addressbookid' => $query->createNamedParameter(0),
- 'carddata' => $query->createNamedParameter($vCards[$i]->serialize(), IQueryBuilder::PARAM_LOB),
- 'uri' => $query->createNamedParameter('uri' . $i),
- 'lastmodified' => $query->createNamedParameter(time()),
- 'etag' => $query->createNamedParameter('etag' . $i),
- 'size' => $query->createNamedParameter(120),
- ]
- );
+ ->values(
+ [
+ 'addressbookid' => $query->createNamedParameter(0),
+ 'carddata' => $query->createNamedParameter($vCards[$i]->serialize(), IQueryBuilder::PARAM_LOB),
+ 'uri' => $query->createNamedParameter('uri' . $i),
+ 'lastmodified' => $query->createNamedParameter(time()),
+ 'etag' => $query->createNamedParameter('etag' . $i),
+ 'size' => $query->createNamedParameter(120),
+ ]
+ );
$query->execute();
$vCardIds[] = $query->getLastInsertId();
}
+ $query = $this->db->getQueryBuilder();
$query->insert($this->dbCardsPropertiesTable)
->values(
[
@@ -714,6 +694,7 @@ class CardDavBackendTest extends TestCase {
]
);
$query->execute();
+ $query = $this->db->getQueryBuilder();
$query->insert($this->dbCardsPropertiesTable)
->values(
[
@@ -725,6 +706,7 @@ class CardDavBackendTest extends TestCase {
]
);
$query->execute();
+ $query = $this->db->getQueryBuilder();
$query->insert($this->dbCardsPropertiesTable)
->values(
[
@@ -736,6 +718,7 @@ class CardDavBackendTest extends TestCase {
]
);
$query->execute();
+ $query = $this->db->getQueryBuilder();
$query->insert($this->dbCardsPropertiesTable)
->values(
[
@@ -747,6 +730,7 @@ class CardDavBackendTest extends TestCase {
]
);
$query->execute();
+ $query = $this->db->getQueryBuilder();
$query->insert($this->dbCardsPropertiesTable)
->values(
[
@@ -776,7 +760,7 @@ class CardDavBackendTest extends TestCase {
$this->assertSame(count($expected), count($found));
}
- public function dataTestSearch() {
+ public static function dataTestSearch(): array {
return [
['John', ['FN'], [], [['uri0', 'John Doe'], ['uri1', 'John M. Doe']]],
['M. Doe', ['FN'], [], [['uri1', 'John M. Doe']]],
@@ -790,19 +774,19 @@ class CardDavBackendTest extends TestCase {
];
}
- public function testGetCardUri() {
+ public function testGetCardUri(): void {
$query = $this->db->getQueryBuilder();
$query->insert($this->dbCardsTable)
- ->values(
- [
- 'addressbookid' => $query->createNamedParameter(1),
- 'carddata' => $query->createNamedParameter('carddata', IQueryBuilder::PARAM_LOB),
- 'uri' => $query->createNamedParameter('uri'),
- 'lastmodified' => $query->createNamedParameter(5489543),
- 'etag' => $query->createNamedParameter('etag'),
- 'size' => $query->createNamedParameter(120),
- ]
- );
+ ->values(
+ [
+ 'addressbookid' => $query->createNamedParameter(1),
+ 'carddata' => $query->createNamedParameter('carddata', IQueryBuilder::PARAM_LOB),
+ 'uri' => $query->createNamedParameter('uri'),
+ 'lastmodified' => $query->createNamedParameter(5489543),
+ 'etag' => $query->createNamedParameter('etag'),
+ 'size' => $query->createNamedParameter(120),
+ ]
+ );
$query->execute();
$id = $query->getLastInsertId();
@@ -811,26 +795,26 @@ class CardDavBackendTest extends TestCase {
}
- public function testGetCardUriFailed() {
+ public function testGetCardUriFailed(): void {
$this->expectException(\InvalidArgumentException::class);
$this->backend->getCardUri(1);
}
- public function testGetContact() {
+ public function testGetContact(): void {
$query = $this->db->getQueryBuilder();
for ($i = 0; $i < 2; $i++) {
$query->insert($this->dbCardsTable)
- ->values(
- [
- 'addressbookid' => $query->createNamedParameter($i),
- 'carddata' => $query->createNamedParameter('carddata' . $i, IQueryBuilder::PARAM_LOB),
- 'uri' => $query->createNamedParameter('uri' . $i),
- 'lastmodified' => $query->createNamedParameter(5489543),
- 'etag' => $query->createNamedParameter('etag' . $i),
- 'size' => $query->createNamedParameter(120),
- ]
- );
+ ->values(
+ [
+ 'addressbookid' => $query->createNamedParameter($i),
+ 'carddata' => $query->createNamedParameter('carddata' . $i, IQueryBuilder::PARAM_LOB),
+ 'uri' => $query->createNamedParameter('uri' . $i),
+ 'lastmodified' => $query->createNamedParameter(5489543),
+ 'etag' => $query->createNamedParameter('etag' . $i),
+ 'size' => $query->createNamedParameter(120),
+ ]
+ );
$query->execute();
}
@@ -848,11 +832,11 @@ class CardDavBackendTest extends TestCase {
$this->assertEmpty($result);
}
- public function testGetContactFail() {
+ public function testGetContactFail(): void {
$this->assertEmpty($this->backend->getContact(0, 'uri'));
}
- public function testCollectCardProperties() {
+ public function testCollectCardProperties(): void {
$query = $this->db->getQueryBuilder();
$query->insert($this->dbCardsPropertiesTable)
->values(
@@ -864,9 +848,71 @@ class CardDavBackendTest extends TestCase {
'preferred' => $query->createNamedParameter(0)
]
)
- ->execute();
+ ->execute();
$result = $this->backend->collectCardProperties(666, 'FN');
$this->assertEquals(['John Doe'], $result);
}
+
+ /**
+ * @throws \OCP\DB\Exception
+ * @throws \Sabre\DAV\Exception\BadRequest
+ */
+ public function testPruneOutdatedSyncTokens(): void {
+ $addressBookId = $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
+ $changes = $this->backend->getChangesForAddressBook($addressBookId, '', 1);
+ $syncToken = $changes['syncToken'];
+
+ $uri = $this->getUniqueID('card');
+ $this->backend->createCard($addressBookId, $uri, $this->vcardTest0);
+ $this->backend->updateCard($addressBookId, $uri, $this->vcardTest1);
+
+ // Do not delete anything if week data as old as ts=0
+ $deleted = $this->backend->pruneOutdatedSyncTokens(0, 0);
+ self::assertSame(0, $deleted);
+
+ $deleted = $this->backend->pruneOutdatedSyncTokens(0, time());
+ // At least one from the object creation and one from the object update
+ $this->assertGreaterThanOrEqual(2, $deleted);
+ $changes = $this->backend->getChangesForAddressBook($addressBookId, $syncToken, 1);
+ $this->assertEmpty($changes['added']);
+ $this->assertEmpty($changes['modified']);
+ $this->assertEmpty($changes['deleted']);
+
+ // Test that objects remain
+
+ // Currently changes are empty
+ $changes = $this->backend->getChangesForAddressBook($addressBookId, $syncToken, 100);
+ $this->assertEquals(0, count($changes['added'] + $changes['modified'] + $changes['deleted']));
+
+ // Create card
+ $uri = $this->getUniqueID('card');
+ $this->backend->createCard($addressBookId, $uri, $this->vcardTest0);
+ // We now have one add
+ $changes = $this->backend->getChangesForAddressBook($addressBookId, $syncToken, 100);
+ $this->assertEquals(1, count($changes['added']));
+ $this->assertEmpty($changes['modified']);
+ $this->assertEmpty($changes['deleted']);
+
+ // Update card
+ $this->backend->updateCard($addressBookId, $uri, $this->vcardTest1);
+ // One add, one modify, but shortened to modify
+ $changes = $this->backend->getChangesForAddressBook($addressBookId, $syncToken, 100);
+ $this->assertEmpty($changes['added']);
+ $this->assertEquals(1, count($changes['modified']));
+ $this->assertEmpty($changes['deleted']);
+
+ // Delete all but last change
+ $deleted = $this->backend->pruneOutdatedSyncTokens(1, time());
+ $this->assertEquals(1, $deleted); // We had two changes before, now one
+
+ // Only update should remain
+ $changes = $this->backend->getChangesForAddressBook($addressBookId, $syncToken, 100);
+ $this->assertEmpty($changes['added']);
+ $this->assertEquals(1, count($changes['modified']));
+ $this->assertEmpty($changes['deleted']);
+
+ // Check that no crash occurs when prune is called without current changes
+ $deleted = $this->backend->pruneOutdatedSyncTokens(1, time());
+ }
}
diff --git a/apps/dav/tests/unit/CardDAV/ContactsManagerTest.php b/apps/dav/tests/unit/CardDAV/ContactsManagerTest.php
index e9153c4d197..bdd826f671b 100644
--- a/apps/dav/tests/unit/CardDAV/ContactsManagerTest.php
+++ b/apps/dav/tests/unit/CardDAV/ContactsManagerTest.php
@@ -1,51 +1,37 @@
<?php
+
+declare(strict_types=1);
/**
- * @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 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\Tests\unit\CardDAV;
use OCA\DAV\CardDAV\CardDavBackend;
use OCA\DAV\CardDAV\ContactsManager;
+use OCA\DAV\Db\PropertyMapper;
use OCP\Contacts\IManager;
use OCP\IL10N;
use OCP\IURLGenerator;
+use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class ContactsManagerTest extends TestCase {
- public function test() {
- /** @var IManager | \PHPUnit\Framework\MockObject\MockObject $cm */
- $cm = $this->getMockBuilder(IManager::class)->disableOriginalConstructor()->getMock();
+ public function test(): void {
+ /** @var IManager&MockObject $cm */
+ $cm = $this->createMock(IManager::class);
$cm->expects($this->exactly(2))->method('registerAddressBook');
- $urlGenerator = $this->getMockBuilder(IURLGenerator::class)->disableOriginalConstructor()->getMock();
- /** @var CardDavBackend | \PHPUnit\Framework\MockObject\MockObject $backEnd */
- $backEnd = $this->getMockBuilder(CardDavBackend::class)->disableOriginalConstructor()->getMock();
+ $urlGenerator = $this->createMock(IURLGenerator::class);
+ /** @var CardDavBackend&MockObject $backEnd */
+ $backEnd = $this->createMock(CardDavBackend::class);
$backEnd->method('getAddressBooksForUser')->willReturn([
['{DAV:}displayname' => 'Test address book', 'uri' => 'default'],
]);
+ $propertyMapper = $this->createMock(PropertyMapper::class);
$l = $this->createMock(IL10N::class);
- $app = new ContactsManager($backEnd, $l);
+ $app = new ContactsManager($backEnd, $l, $propertyMapper);
$app->setupContactsProvider($cm, 'user01', $urlGenerator);
}
}
diff --git a/apps/dav/tests/unit/CardDAV/ConverterTest.php b/apps/dav/tests/unit/CardDAV/ConverterTest.php
index cc5f707bb36..00519b82766 100644
--- a/apps/dav/tests/unit/CardDAV/ConverterTest.php
+++ b/apps/dav/tests/unit/CardDAV/ConverterTest.php
@@ -1,29 +1,11 @@
<?php
+
+declare(strict_types=1);
+
/**
- * @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\Tests\unit\CardDAV;
@@ -32,23 +14,30 @@ use OCP\Accounts\IAccount;
use OCP\Accounts\IAccountManager;
use OCP\Accounts\IAccountProperty;
use OCP\IImage;
+use OCP\IURLGenerator;
use OCP\IUser;
+use OCP\IUserManager;
use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
use Test\TestCase;
class ConverterTest extends TestCase {
-
- /** @var IAccountManager|\PHPUnit\Framework\MockObject\MockObject */
- private $accountManager;
+ private IAccountManager&MockObject $accountManager;
+ private IUserManager&MockObject $userManager;
+ private IURLGenerator&MockObject $urlGenerator;
+ private LoggerInterface&MockObject $logger;
protected function setUp(): void {
parent::setUp();
$this->accountManager = $this->createMock(IAccountManager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
}
/**
- * @return IAccountProperty|MockObject
+ * @return IAccountProperty&MockObject
*/
protected function getAccountPropertyMock(string $name, ?string $value, string $scope) {
$property = $this->createMock(IAccountProperty::class);
@@ -70,35 +59,32 @@ class ConverterTest extends TestCase {
public function getAccountManager(IUser $user) {
$account = $this->createMock(IAccount::class);
$account->expects($this->any())
- ->method('getProperties')
+ ->method('getAllProperties')
->willReturnCallback(function () use ($user) {
- return [
- $this->getAccountPropertyMock(IAccountManager::PROPERTY_DISPLAYNAME, $user->getDisplayName(), IAccountManager::SCOPE_FEDERATED),
- $this->getAccountPropertyMock(IAccountManager::PROPERTY_ADDRESS, '', IAccountManager::SCOPE_LOCAL),
- $this->getAccountPropertyMock(IAccountManager::PROPERTY_WEBSITE, '', IAccountManager::SCOPE_LOCAL),
- $this->getAccountPropertyMock(IAccountManager::PROPERTY_EMAIL, $user->getEMailAddress(), IAccountManager::SCOPE_FEDERATED),
- $this->getAccountPropertyMock(IAccountManager::PROPERTY_AVATAR, $user->getAvatarImage(-1)->data(), IAccountManager::SCOPE_FEDERATED),
- $this->getAccountPropertyMock(IAccountManager::PROPERTY_PHONE, '', IAccountManager::SCOPE_LOCAL),
- $this->getAccountPropertyMock(IAccountManager::PROPERTY_TWITTER, '', IAccountManager::SCOPE_LOCAL),
- ];
+ yield $this->getAccountPropertyMock(IAccountManager::PROPERTY_DISPLAYNAME, $user->getDisplayName(), IAccountManager::SCOPE_FEDERATED);
+ yield $this->getAccountPropertyMock(IAccountManager::PROPERTY_ADDRESS, '', IAccountManager::SCOPE_LOCAL);
+ yield $this->getAccountPropertyMock(IAccountManager::PROPERTY_WEBSITE, '', IAccountManager::SCOPE_LOCAL);
+ yield $this->getAccountPropertyMock(IAccountManager::PROPERTY_EMAIL, $user->getEMailAddress(), IAccountManager::SCOPE_FEDERATED);
+ yield $this->getAccountPropertyMock(IAccountManager::PROPERTY_AVATAR, $user->getAvatarImage(-1)->data(), IAccountManager::SCOPE_FEDERATED);
+ yield $this->getAccountPropertyMock(IAccountManager::PROPERTY_PHONE, '', IAccountManager::SCOPE_LOCAL);
+ yield $this->getAccountPropertyMock(IAccountManager::PROPERTY_TWITTER, '', IAccountManager::SCOPE_LOCAL);
});
- $accountManager = $this->getMockBuilder(IAccountManager::class)
- ->disableOriginalConstructor()->getMock();
+ $accountManager = $this->createMock(IAccountManager::class);
- $accountManager->expects($this->any())->method('getAccount')->willReturn($account);
+ $accountManager->expects($this->any())
+ ->method('getAccount')
+ ->willReturn($account);
return $accountManager;
}
- /**
- * @dataProvider providesNewUsers
- */
- public function testCreation($expectedVCard, $displayName = null, $eMailAddress = null, $cloudId = null) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesNewUsers')]
+ public function testCreation($expectedVCard, $displayName = null, $eMailAddress = null, $cloudId = null): void {
$user = $this->getUserMock((string)$displayName, $eMailAddress, $cloudId);
$accountManager = $this->getAccountManager($user);
- $converter = new Converter($accountManager);
+ $converter = new Converter($accountManager, $this->userManager, $this->urlGenerator, $this->logger);
$vCard = $converter->createCardFromUser($user);
if ($expectedVCard !== null) {
$this->assertInstanceOf('Sabre\VObject\Component\VCard', $vCard);
@@ -109,7 +95,30 @@ class ConverterTest extends TestCase {
}
}
- protected function compareData($expected, $data) {
+ public function testManagerProp(): void {
+ $user = $this->getUserMock('user', 'user@domain.tld', 'user@cloud.domain.tld');
+ $user->method('getManagerUids')
+ ->willReturn(['mgr']);
+ $this->userManager->expects(self::once())
+ ->method('getDisplayName')
+ ->with('mgr')
+ ->willReturn('Manager');
+ $accountManager = $this->getAccountManager($user);
+
+ $converter = new Converter($accountManager, $this->userManager, $this->urlGenerator, $this->logger);
+ $vCard = $converter->createCardFromUser($user);
+
+ $this->compareData(
+ [
+ 'cloud' => 'user@cloud.domain.tld',
+ 'email' => 'user@domain.tld',
+ 'x-managersname' => 'Manager',
+ ],
+ $vCard->jsonSerialize()
+ );
+ }
+
+ protected function compareData(array $expected, array $data): void {
foreach ($expected as $key => $value) {
$found = false;
foreach ($data[1] as $d) {
@@ -124,7 +133,7 @@ class ConverterTest extends TestCase {
}
}
- public function providesNewUsers() {
+ public static function providesNewUsers(): array {
return [
[
null
@@ -151,8 +160,8 @@ class ConverterTest extends TestCase {
'fn' => 'Dr. Foo Bar',
'photo' => 'MTIzNDU2Nzg5',
],
- "Dr. Foo Bar",
- "foo@bar.net",
+ 'Dr. Foo Bar',
+ 'foo@bar.net',
'foo@cloud.net'
],
[
@@ -161,9 +170,9 @@ class ConverterTest extends TestCase {
'fn' => 'Dr. Foo Bar',
'photo' => 'MTIzNDU2Nzg5',
],
- "Dr. Foo Bar",
+ 'Dr. Foo Bar',
null,
- "foo@cloud.net"
+ 'foo@cloud.net'
],
[
[
@@ -178,19 +187,15 @@ class ConverterTest extends TestCase {
];
}
- /**
- * @dataProvider providesNames
- * @param $expected
- * @param $fullName
- */
- public function testNameSplitter($expected, $fullName) {
- $converter = new Converter($this->accountManager);
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesNames')]
+ public function testNameSplitter(string $expected, string $fullName): void {
+ $converter = new Converter($this->accountManager, $this->userManager, $this->urlGenerator, $this->logger);
$r = $converter->splitFullName($fullName);
$r = implode(';', $r);
$this->assertEquals($expected, $r);
}
- public function providesNames() {
+ public static function providesNames(): array {
return [
['Sauron;;;;', 'Sauron'],
['Baggins;Bilbo;;;', 'Bilbo Baggins'],
@@ -199,16 +204,13 @@ class ConverterTest extends TestCase {
}
/**
- * @param $displayName
- * @param $eMailAddress
- * @param $cloudId
- * @return IUser | \PHPUnit\Framework\MockObject\MockObject
+ * @return IUser&MockObject
*/
protected function getUserMock(string $displayName, ?string $eMailAddress, ?string $cloudId) {
- $image0 = $this->getMockBuilder(IImage::class)->disableOriginalConstructor()->getMock();
+ $image0 = $this->createMock(IImage::class);
$image0->method('mimeType')->willReturn('image/jpeg');
$image0->method('data')->willReturn('123456789');
- $user = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
+ $user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('12345');
$user->method('getDisplayName')->willReturn($displayName);
$user->method('getEMailAddress')->willReturn($eMailAddress);
diff --git a/apps/dav/tests/unit/CardDAV/ImageExportPluginTest.php b/apps/dav/tests/unit/CardDAV/ImageExportPluginTest.php
index ed5ec544fb6..d47f53bddcd 100644
--- a/apps/dav/tests/unit/CardDAV/ImageExportPluginTest.php
+++ b/apps/dav/tests/unit/CardDAV/ImageExportPluginTest.php
@@ -1,35 +1,20 @@
<?php
+
+declare(strict_types=1);
/**
- * @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 Morris Jobke <hey@morrisjobke.de>
- * @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\Tests\unit\CardDAV;
use OCA\DAV\CardDAV\AddressBook;
use OCA\DAV\CardDAV\ImageExportPlugin;
use OCA\DAV\CardDAV\PhotoCache;
+use OCP\AppFramework\Http;
use OCP\Files\NotFoundException;
use OCP\Files\SimpleFS\ISimpleFile;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\CardDAV\Card;
use Sabre\DAV\Node;
use Sabre\DAV\Server;
@@ -39,19 +24,12 @@ use Sabre\HTTP\ResponseInterface;
use Test\TestCase;
class ImageExportPluginTest extends TestCase {
-
- /** @var ResponseInterface|\PHPUnit\Framework\MockObject\MockObject */
- private $response;
- /** @var RequestInterface|\PHPUnit\Framework\MockObject\MockObject */
- private $request;
- /** @var ImageExportPlugin|\PHPUnit\Framework\MockObject\MockObject */
- private $plugin;
- /** @var Server */
- private $server;
- /** @var Tree|\PHPUnit\Framework\MockObject\MockObject */
- private $tree;
- /** @var PhotoCache|\PHPUnit\Framework\MockObject\MockObject */
- private $cache;
+ private ResponseInterface&MockObject $response;
+ private RequestInterface&MockObject $request;
+ private Server&MockObject $server;
+ private Tree&MockObject $tree;
+ private PhotoCache&MockObject $cache;
+ private ImageExportPlugin $plugin;
protected function setUp(): void {
parent::setUp();
@@ -63,24 +41,18 @@ class ImageExportPluginTest extends TestCase {
$this->server->tree = $this->tree;
$this->cache = $this->createMock(PhotoCache::class);
- $this->plugin = $this->getMockBuilder(ImageExportPlugin::class)
- ->setMethods(['getPhoto'])
- ->setConstructorArgs([$this->cache])
- ->getMock();
+ $this->plugin = new ImageExportPlugin($this->cache);
$this->plugin->initialize($this->server);
}
- /**
- * @dataProvider providesQueryParams
- * @param $param
- */
- public function testQueryParams($param) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesQueryParams')]
+ public function testQueryParams(array $param): void {
$this->request->expects($this->once())->method('getQueryParameters')->willReturn($param);
$result = $this->plugin->httpGet($this->request, $this->response);
$this->assertTrue($result);
}
- public function providesQueryParams() {
+ public static function providesQueryParams(): array {
return [
[[]],
[['1']],
@@ -88,7 +60,7 @@ class ImageExportPluginTest extends TestCase {
];
}
- public function testNoCard() {
+ public function testNoCard(): void {
$this->request->method('getQueryParameters')
->willReturn([
'photo'
@@ -105,7 +77,7 @@ class ImageExportPluginTest extends TestCase {
$this->assertTrue($result);
}
- public function dataTestCard() {
+ public static function dataTestCard(): array {
return [
[null, false],
[null, true],
@@ -114,13 +86,8 @@ class ImageExportPluginTest extends TestCase {
];
}
- /**
- * @dataProvider dataTestCard
- *
- * @param $size
- * @param bool $photo
- */
- public function testCard($size, $photo) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTestCard')]
+ public function testCard(?int $size, bool $photo): void {
$query = ['photo' => null];
if ($size !== null) {
$query['size'] = $size;
@@ -150,16 +117,6 @@ class ImageExportPluginTest extends TestCase {
$this->fail();
});
- $this->response->expects($this->at(0))
- ->method('setHeader')
- ->with('Cache-Control', 'private, max-age=3600, must-revalidate');
- $this->response->expects($this->at(1))
- ->method('setHeader')
- ->with('Etag', '"myEtag"');
- $this->response->expects($this->at(2))
- ->method('setHeader')
- ->with('Pragma', 'public');
-
$size = $size === null ? -1 : $size;
if ($photo) {
@@ -173,12 +130,18 @@ class ImageExportPluginTest extends TestCase {
->with(1, 'card', $size, $card)
->willReturn($file);
- $this->response->expects($this->at(3))
+ $setHeaderCalls = [
+ ['Cache-Control', 'private, max-age=3600, must-revalidate'],
+ ['Etag', '"myEtag"'],
+ ['Content-Type', 'image/jpeg'],
+ ['Content-Disposition', 'attachment; filename=card.jpg'],
+ ];
+ $this->response->expects($this->exactly(count($setHeaderCalls)))
->method('setHeader')
- ->with('Content-Type', 'image/jpeg');
- $this->response->expects($this->at(4))
- ->method('setHeader')
- ->with('Content-Disposition', 'attachment; filename=card.jpg');
+ ->willReturnCallback(function () use (&$setHeaderCalls): void {
+ $expected = array_shift($setHeaderCalls);
+ $this->assertEquals($expected, func_get_args());
+ });
$this->response->expects($this->once())
->method('setStatus')
@@ -187,12 +150,22 @@ class ImageExportPluginTest extends TestCase {
->method('setBody')
->with('imgdata');
} else {
+ $setHeaderCalls = [
+ ['Cache-Control', 'private, max-age=3600, must-revalidate'],
+ ['Etag', '"myEtag"'],
+ ];
+ $this->response->expects($this->exactly(count($setHeaderCalls)))
+ ->method('setHeader')
+ ->willReturnCallback(function () use (&$setHeaderCalls): void {
+ $expected = array_shift($setHeaderCalls);
+ $this->assertEquals($expected, func_get_args());
+ });
$this->cache->method('get')
->with(1, 'card', $size, $card)
->willThrowException(new NotFoundException());
$this->response->expects($this->once())
->method('setStatus')
- ->with(404);
+ ->with(Http::STATUS_NO_CONTENT);
}
$result = $this->plugin->httpGet($this->request, $this->response);
diff --git a/apps/dav/tests/unit/CardDAV/Security/CardDavRateLimitingPluginTest.php b/apps/dav/tests/unit/CardDAV/Security/CardDavRateLimitingPluginTest.php
new file mode 100644
index 00000000000..ee599d5a76c
--- /dev/null
+++ b/apps/dav/tests/unit/CardDAV/Security/CardDavRateLimitingPluginTest.php
@@ -0,0 +1,146 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CardDAV\Security;
+
+use OC\Security\RateLimiting\Exception\RateLimitExceededException;
+use OC\Security\RateLimiting\Limiter;
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\CardDAV\Security\CardDavRateLimitingPlugin;
+use OCA\DAV\Connector\Sabre\Exception\TooManyRequests;
+use OCP\IAppConfig;
+use OCP\IUser;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\DAV\Exception\Forbidden;
+use Test\TestCase;
+
+class CardDavRateLimitingPluginTest extends TestCase {
+
+ private Limiter&MockObject $limiter;
+ private CardDavBackend&MockObject $cardDavBackend;
+ private IUserManager&MockObject $userManager;
+ private LoggerInterface&MockObject $logger;
+ private IAppConfig&MockObject $config;
+ private string $userId = 'user123';
+ private CardDavRateLimitingPlugin $plugin;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->limiter = $this->createMock(Limiter::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->cardDavBackend = $this->createMock(CardDavBackend::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->config = $this->createMock(IAppConfig::class);
+ $this->plugin = new CardDavRateLimitingPlugin(
+ $this->limiter,
+ $this->userManager,
+ $this->cardDavBackend,
+ $this->logger,
+ $this->config,
+ $this->userId,
+ );
+ }
+
+ public function testNoUserObject(): void {
+ $this->limiter->expects(self::never())
+ ->method('registerUserRequest');
+
+ $this->plugin->beforeBind('addressbooks/users/foo/addressbookname');
+ }
+
+ public function testUnrelated(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with($this->userId)
+ ->willReturn($user);
+ $this->limiter->expects(self::never())
+ ->method('registerUserRequest');
+
+ $this->plugin->beforeBind('foo/bar');
+ }
+
+ public function testRegisterAddressBookrCreation(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with($this->userId)
+ ->willReturn($user);
+ $this->config
+ ->method('getValueInt')
+ ->with('dav')
+ ->willReturnArgument(2);
+ $this->limiter->expects(self::once())
+ ->method('registerUserRequest')
+ ->with(
+ 'carddav-create-address-book',
+ 10,
+ 3600,
+ $user,
+ );
+
+ $this->plugin->beforeBind('addressbooks/users/foo/addressbookname');
+ }
+
+ public function testAddressBookCreationRateLimitExceeded(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with($this->userId)
+ ->willReturn($user);
+ $this->config
+ ->method('getValueInt')
+ ->with('dav')
+ ->willReturnArgument(2);
+ $this->limiter->expects(self::once())
+ ->method('registerUserRequest')
+ ->with(
+ 'carddav-create-address-book',
+ 10,
+ 3600,
+ $user,
+ )
+ ->willThrowException(new RateLimitExceededException());
+ $this->expectException(TooManyRequests::class);
+
+ $this->plugin->beforeBind('addressbooks/users/foo/addressbookname');
+ }
+
+ public function testAddressBookLimitReached(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects(self::once())
+ ->method('get')
+ ->with($this->userId)
+ ->willReturn($user);
+ $user->method('getUID')->willReturn('user123');
+ $this->config
+ ->method('getValueInt')
+ ->with('dav')
+ ->willReturnArgument(2);
+ $this->limiter->expects(self::once())
+ ->method('registerUserRequest')
+ ->with(
+ 'carddav-create-address-book',
+ 10,
+ 3600,
+ $user,
+ );
+ $this->cardDavBackend->expects(self::once())
+ ->method('getAddressBooksForUserCount')
+ ->with('principals/users/user123')
+ ->willReturn(11);
+ $this->expectException(Forbidden::class);
+
+ $this->plugin->beforeBind('addressbooks/users/foo/addressbookname');
+ }
+
+}
diff --git a/apps/dav/tests/unit/CardDAV/Sharing/PluginTest.php b/apps/dav/tests/unit/CardDAV/Sharing/PluginTest.php
index e9429e4e18f..1e934a69a53 100644
--- a/apps/dav/tests/unit/CardDAV/Sharing/PluginTest.php
+++ b/apps/dav/tests/unit/CardDAV/Sharing/PluginTest.php
@@ -1,28 +1,10 @@
<?php
+
+declare(strict_types=1);
/**
- * @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 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\Tests\unit\CardDAV\Sharing;
@@ -31,6 +13,7 @@ use OCA\DAV\DAV\Sharing\IShareable;
use OCA\DAV\DAV\Sharing\Plugin;
use OCP\IConfig;
use OCP\IRequest;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\Server;
use Sabre\DAV\SimpleCollection;
use Sabre\HTTP\Request;
@@ -38,36 +21,30 @@ use Sabre\HTTP\Response;
use Test\TestCase;
class PluginTest extends TestCase {
-
- /** @var Plugin */
- private $plugin;
- /** @var Server */
- private $server;
- /** @var IShareable | \PHPUnit\Framework\MockObject\MockObject */
- private $book;
+ private Plugin $plugin;
+ private Server $server;
+ private IShareable&MockObject $book;
protected function setUp(): void {
parent::setUp();
- /** @var Auth | \PHPUnit\Framework\MockObject\MockObject $authBackend */
- $authBackend = $this->getMockBuilder(Auth::class)->disableOriginalConstructor()->getMock();
- $authBackend->method('isDavAuthenticated')->willReturn(true);
-
- /** @var IRequest $request */
- $request = $this->getMockBuilder(IRequest::class)->disableOriginalConstructor()->getMock();
+ $authBackend = $this->createMock(Auth::class);
+ $authBackend->method('isDavAuthenticated')
+ ->willReturn(true);
+ $request = $this->createMock(IRequest::class);
$config = $this->createMock(IConfig::class);
$this->plugin = new Plugin($authBackend, $request, $config);
$root = new SimpleCollection('root');
$this->server = new \Sabre\DAV\Server($root);
- /** @var SimpleCollection $node */
- $this->book = $this->getMockBuilder(IShareable::class)->disableOriginalConstructor()->getMock();
- $this->book->method('getName')->willReturn('addressbook1.vcf');
+ $this->book = $this->createMock(IShareable::class);
+ $this->book->method('getName')
+ ->willReturn('addressbook1.vcf');
$root->addChild($this->book);
$this->plugin->initialize($this->server);
}
- public function testSharing() {
+ public function testSharing(): void {
$this->book->expects($this->once())->method('updateShares')->with([[
'href' => 'principal:principals/admin',
'commonName' => null,
diff --git a/apps/dav/tests/unit/CardDAV/SyncServiceTest.php b/apps/dav/tests/unit/CardDAV/SyncServiceTest.php
index d22a246bbec..77caed336f4 100644
--- a/apps/dav/tests/unit/CardDAV/SyncServiceTest.php
+++ b/apps/dav/tests/unit/CardDAV/SyncServiceTest.php
@@ -1,112 +1,335 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Joas Schilling <coding@schilljs.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 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\Tests\unit\CardDAV;
+use GuzzleHttp\Exception\ClientException;
+use GuzzleHttp\Psr7\Request as PsrRequest;
+use GuzzleHttp\Psr7\Response as PsrResponse;
+use OC\Http\Client\Response;
use OCA\DAV\CardDAV\CardDavBackend;
use OCA\DAV\CardDAV\Converter;
use OCA\DAV\CardDAV\SyncService;
-use OCP\ILogger;
+use OCP\Http\Client\IClient;
+use OCP\Http\Client\IClientService;
+use OCP\IConfig;
+use OCP\IDBConnection;
use OCP\IUser;
use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Http\Client\ClientExceptionInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
use Sabre\VObject\Component\VCard;
use Test\TestCase;
class SyncServiceTest extends TestCase {
- public function testEmptySync() {
- $backend = $this->getBackendMock(0, 0, 0);
- $ss = $this->getSyncServiceMock($backend, []);
- $return = $ss->syncRemoteAddressBook('', 'system', 'system', '1234567890', null, '1', 'principals/system/system', []);
- $this->assertEquals('sync-token-1', $return);
+ protected CardDavBackend&MockObject $backend;
+ protected IUserManager&MockObject $userManager;
+ protected IDBConnection&MockObject $dbConnection;
+ protected LoggerInterface $logger;
+ protected Converter&MockObject $converter;
+ protected IClient&MockObject $client;
+ protected IConfig&MockObject $config;
+ protected SyncService $service;
+
+ public function setUp(): void {
+ parent::setUp();
+
+ $addressBook = [
+ 'id' => 1,
+ 'uri' => 'system',
+ 'principaluri' => 'principals/system/system',
+ '{DAV:}displayname' => 'system',
+ // watch out, incomplete address book mock.
+ ];
+
+ $this->backend = $this->createMock(CardDavBackend::class);
+ $this->backend->method('getAddressBooksByUri')
+ ->with('principals/system/system', 1)
+ ->willReturn($addressBook);
+
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->dbConnection = $this->createMock(IDBConnection::class);
+ $this->logger = new NullLogger();
+ $this->converter = $this->createMock(Converter::class);
+ $this->client = $this->createMock(IClient::class);
+ $this->config = $this->createMock(IConfig::class);
+
+ $clientService = $this->createMock(IClientService::class);
+ $clientService->method('newClient')
+ ->willReturn($this->client);
+
+ $this->service = new SyncService(
+ $this->backend,
+ $this->userManager,
+ $this->dbConnection,
+ $this->logger,
+ $this->converter,
+ $clientService,
+ $this->config
+ );
}
- public function testSyncWithNewElement() {
- $backend = $this->getBackendMock(1, 0, 0);
- $backend->method('getCard')->willReturn(false);
+ public function testEmptySync(): void {
+ $this->backend->expects($this->exactly(0))
+ ->method('createCard');
+ $this->backend->expects($this->exactly(0))
+ ->method('updateCard');
+ $this->backend->expects($this->exactly(0))
+ ->method('deleteCard');
- $ss = $this->getSyncServiceMock($backend, ['0' => [200 => '']]);
- $return = $ss->syncRemoteAddressBook('', 'system', 'system', '1234567890', null, '1', 'principals/system/system', []);
- $this->assertEquals('sync-token-1', $return);
+ $body = '<?xml version="1.0"?>
+<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:oc="http://owncloud.org/ns">
+ <d:sync-token>http://sabre.io/ns/sync/1</d:sync-token>
+</d:multistatus>';
+
+ $requestResponse = new Response(new PsrResponse(
+ 207,
+ ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)],
+ $body
+ ));
+
+ $this->client
+ ->method('request')
+ ->willReturn($requestResponse);
+
+ $token = $this->service->syncRemoteAddressBook(
+ '',
+ 'system',
+ 'system',
+ '1234567890',
+ null,
+ '1',
+ 'principals/system/system',
+ []
+ )[0];
+
+ $this->assertEquals('http://sabre.io/ns/sync/1', $token);
}
- public function testSyncWithUpdatedElement() {
- $backend = $this->getBackendMock(0, 1, 0);
- $backend->method('getCard')->willReturn(true);
+ public function testSyncWithNewElement(): void {
+ $this->backend->expects($this->exactly(1))
+ ->method('createCard');
+ $this->backend->expects($this->exactly(0))
+ ->method('updateCard');
+ $this->backend->expects($this->exactly(0))
+ ->method('deleteCard');
+
+ $this->backend->method('getCard')
+ ->willReturn(false);
+
+
+ $body = '<?xml version="1.0"?>
+<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:oc="http://owncloud.org/ns">
+ <d:response>
+ <d:href>/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getcontenttype>text/vcard; charset=utf-8</d:getcontenttype>
+ <d:getetag>&quot;2df155fa5c2a24cd7f750353fc63f037&quot;</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:sync-token>http://sabre.io/ns/sync/2</d:sync-token>
+</d:multistatus>';
- $ss = $this->getSyncServiceMock($backend, ['0' => [200 => '']]);
- $return = $ss->syncRemoteAddressBook('', 'system', 'system', '1234567890', null, '1', 'principals/system/system', []);
- $this->assertEquals('sync-token-1', $return);
+ $reportResponse = new Response(new PsrResponse(
+ 207,
+ ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)],
+ $body
+ ));
+
+ $this->client
+ ->method('request')
+ ->willReturn($reportResponse);
+
+ $vCard = 'BEGIN:VCARD
+VERSION:3.0
+PRODID:-//Sabre//Sabre VObject 4.5.4//EN
+UID:alice
+FN;X-NC-SCOPE=v2-federated:alice
+N;X-NC-SCOPE=v2-federated:alice;;;;
+X-SOCIALPROFILE;TYPE=NEXTCLOUD;X-NC-SCOPE=v2-published:https://server2.internal/index.php/u/alice
+CLOUD:alice@server2.internal
+END:VCARD';
+
+ $getResponse = new Response(new PsrResponse(
+ 200,
+ ['Content-Type' => 'text/vcard; charset=utf-8', 'Content-Length' => strlen($vCard)],
+ $vCard,
+ ));
+
+ $this->client
+ ->method('get')
+ ->willReturn($getResponse);
+
+ $token = $this->service->syncRemoteAddressBook(
+ '',
+ 'system',
+ 'system',
+ '1234567890',
+ null,
+ '1',
+ 'principals/system/system',
+ []
+ )[0];
+
+ $this->assertEquals('http://sabre.io/ns/sync/2', $token);
}
- public function testSyncWithDeletedElement() {
- $backend = $this->getBackendMock(0, 0, 1);
+ public function testSyncWithUpdatedElement(): void {
+ $this->backend->expects($this->exactly(0))
+ ->method('createCard');
+ $this->backend->expects($this->exactly(1))
+ ->method('updateCard');
+ $this->backend->expects($this->exactly(0))
+ ->method('deleteCard');
+
+ $this->backend->method('getCard')
+ ->willReturn(true);
+
+
+ $body = '<?xml version="1.0"?>
+<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:oc="http://owncloud.org/ns">
+ <d:response>
+ <d:href>/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getcontenttype>text/vcard; charset=utf-8</d:getcontenttype>
+ <d:getetag>&quot;2df155fa5c2a24cd7f750353fc63f037&quot;</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:sync-token>http://sabre.io/ns/sync/3</d:sync-token>
+</d:multistatus>';
+
+ $reportResponse = new Response(new PsrResponse(
+ 207,
+ ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)],
+ $body
+ ));
+
+ $this->client
+ ->method('request')
+ ->willReturn($reportResponse);
+
+ $vCard = 'BEGIN:VCARD
+VERSION:3.0
+PRODID:-//Sabre//Sabre VObject 4.5.4//EN
+UID:alice
+FN;X-NC-SCOPE=v2-federated:alice
+N;X-NC-SCOPE=v2-federated:alice;;;;
+X-SOCIALPROFILE;TYPE=NEXTCLOUD;X-NC-SCOPE=v2-published:https://server2.internal/index.php/u/alice
+CLOUD:alice@server2.internal
+END:VCARD';
+
+ $getResponse = new Response(new PsrResponse(
+ 200,
+ ['Content-Type' => 'text/vcard; charset=utf-8', 'Content-Length' => strlen($vCard)],
+ $vCard,
+ ));
+
+ $this->client
+ ->method('get')
+ ->willReturn($getResponse);
- $ss = $this->getSyncServiceMock($backend, ['0' => [404 => '']]);
- $return = $ss->syncRemoteAddressBook('', 'system', 'system', '1234567890', null, '1', 'principals/system/system', []);
- $this->assertEquals('sync-token-1', $return);
+ $token = $this->service->syncRemoteAddressBook(
+ '',
+ 'system',
+ 'system',
+ '1234567890',
+ null,
+ '1',
+ 'principals/system/system',
+ []
+ )[0];
+
+ $this->assertEquals('http://sabre.io/ns/sync/3', $token);
}
- public function testEnsureSystemAddressBookExists() {
- /** @var CardDavBackend | \PHPUnit\Framework\MockObject\MockObject $backend */
- $backend = $this->getMockBuilder(CardDavBackend::class)->disableOriginalConstructor()->getMock();
+ public function testSyncWithDeletedElement(): void {
+ $this->backend->expects($this->exactly(0))
+ ->method('createCard');
+ $this->backend->expects($this->exactly(0))
+ ->method('updateCard');
+ $this->backend->expects($this->exactly(1))
+ ->method('deleteCard');
+
+ $body = '<?xml version="1.0"?>
+<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:oc="http://owncloud.org/ns">
+<d:response>
+ <d:href>/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf</d:href>
+ <d:status>HTTP/1.1 404 Not Found</d:status>
+</d:response>
+<d:sync-token>http://sabre.io/ns/sync/4</d:sync-token>
+</d:multistatus>';
+
+ $reportResponse = new Response(new PsrResponse(
+ 207,
+ ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)],
+ $body
+ ));
+
+ $this->client
+ ->method('request')
+ ->willReturn($reportResponse);
+
+ $token = $this->service->syncRemoteAddressBook(
+ '',
+ 'system',
+ 'system',
+ '1234567890',
+ null,
+ '1',
+ 'principals/system/system',
+ []
+ )[0];
+
+ $this->assertEquals('http://sabre.io/ns/sync/4', $token);
+ }
+
+ public function testEnsureSystemAddressBookExists(): void {
+ /** @var CardDavBackend&MockObject $backend */
+ $backend = $this->createMock(CardDavBackend::class);
$backend->expects($this->exactly(1))->method('createAddressBook');
- $backend->expects($this->at(0))->method('getAddressBooksByUri')->willReturn(null);
- $backend->expects($this->at(1))->method('getAddressBooksByUri')->willReturn([]);
+ $backend->expects($this->exactly(2))
+ ->method('getAddressBooksByUri')
+ ->willReturnOnConsecutiveCalls(
+ null,
+ [],
+ );
- /** @var IUserManager $userManager */
- $userManager = $this->getMockBuilder(IUserManager::class)->disableOriginalConstructor()->getMock();
- $logger = $this->getMockBuilder(ILogger::class)->disableOriginalConstructor()->getMock();
+ $userManager = $this->createMock(IUserManager::class);
+ $dbConnection = $this->createMock(IDBConnection::class);
+ $logger = $this->createMock(LoggerInterface::class);
$converter = $this->createMock(Converter::class);
+ $clientService = $this->createMock(IClientService::class);
+ $config = $this->createMock(IConfig::class);
- $ss = new SyncService($backend, $userManager, $logger, $converter);
+ $ss = new SyncService($backend, $userManager, $dbConnection, $logger, $converter, $clientService, $config);
$ss->ensureSystemAddressBookExists('principals/users/adam', 'contacts', []);
}
- public function dataActivatedUsers() {
+ public static function dataActivatedUsers(): array {
return [
[true, 1, 1, 1],
[false, 0, 0, 3],
];
}
- /**
- * @dataProvider dataActivatedUsers
- *
- * @param boolean $activated
- * @param integer $createCalls
- * @param integer $updateCalls
- * @param integer $deleteCalls
- * @return void
- */
- public function testUpdateAndDeleteUser($activated, $createCalls, $updateCalls, $deleteCalls) {
- /** @var CardDavBackend | \PHPUnit\Framework\MockObject\MockObject $backend */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataActivatedUsers')]
+ public function testUpdateAndDeleteUser(bool $activated, int $createCalls, int $updateCalls, int $deleteCalls): void {
+ /** @var CardDavBackend | MockObject $backend */
$backend = $this->getMockBuilder(CardDavBackend::class)->disableOriginalConstructor()->getMock();
- $logger = $this->getMockBuilder(ILogger::class)->disableOriginalConstructor()->getMock();
+ $logger = $this->getMockBuilder(LoggerInterface::class)->disableOriginalConstructor()->getMock();
$backend->expects($this->exactly($createCalls))->method('createCard');
$backend->expects($this->exactly($updateCalls))->method('updateCard');
@@ -120,11 +343,9 @@ class SyncServiceTest extends TestCase {
->with('principals/system/system', 'system')
->willReturn(['id' => -1]);
- /** @var IUserManager | \PHPUnit\Framework\MockObject\MockObject $userManager */
- $userManager = $this->getMockBuilder(IUserManager::class)->disableOriginalConstructor()->getMock();
-
- /** @var IUser | \PHPUnit\Framework\MockObject\MockObject $user */
- $user = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
+ $userManager = $this->createMock(IUserManager::class);
+ $dbConnection = $this->createMock(IDBConnection::class);
+ $user = $this->createMock(IUser::class);
$user->method('getBackendClassName')->willReturn('unittest');
$user->method('getUID')->willReturn('test-user');
$user->method('getCloudId')->willReturn('cloudId');
@@ -135,7 +356,10 @@ class SyncServiceTest extends TestCase {
->method('createCardFromUser')
->willReturn($this->createMock(VCard::class));
- $ss = new SyncService($backend, $userManager, $logger, $converter);
+ $clientService = $this->createMock(IClientService::class);
+ $config = $this->createMock(IConfig::class);
+
+ $ss = new SyncService($backend, $userManager, $dbConnection, $logger, $converter, $clientService, $config);
$ss->updateUser($user);
$ss->updateUser($user);
@@ -143,44 +367,114 @@ class SyncServiceTest extends TestCase {
$ss->deleteUser($user);
}
- /**
- * @param int $createCount
- * @param int $updateCount
- * @param int $deleteCount
- * @return \PHPUnit\Framework\MockObject\MockObject
- */
- private function getBackendMock($createCount, $updateCount, $deleteCount) {
- $backend = $this->getMockBuilder(CardDavBackend::class)
- ->disableOriginalConstructor()
- ->getMock();
- $backend->expects($this->exactly($createCount))->method('createCard');
- $backend->expects($this->exactly($updateCount))->method('updateCard');
- $backend->expects($this->exactly($deleteCount))->method('deleteCard');
- return $backend;
+ public function testDeleteAddressbookWhenAccessRevoked(): void {
+ $this->expectException(ClientExceptionInterface::class);
+
+ $this->backend->expects($this->exactly(0))
+ ->method('createCard');
+ $this->backend->expects($this->exactly(0))
+ ->method('updateCard');
+ $this->backend->expects($this->exactly(0))
+ ->method('deleteCard');
+ $this->backend->expects($this->exactly(1))
+ ->method('deleteAddressBook');
+
+ $request = new PsrRequest(
+ 'REPORT',
+ 'https://server2.internal/remote.php/dav/addressbooks/system/system/system',
+ ['Content-Type' => 'application/xml'],
+ );
+
+ $body = '<?xml version="1.0" encoding="utf-8"?>
+<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns">
+ <s:exception>Sabre\DAV\Exception\NotAuthenticated</s:exception>
+ <s:message>No public access to this resource., Username or password was incorrect, No \'Authorization: Bearer\' header found. Either the client didn\'t send one, or the server is mis-configured, Username or password was incorrect</s:message>
+</d:error>';
+
+ $response = new PsrResponse(
+ 401,
+ ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)],
+ $body
+ );
+
+ $message = 'Client error: `REPORT https://server2.internal/cloud/remote.php/dav/addressbooks/system/system/system` resulted in a `401 Unauthorized` response:
+<?xml version="1.0" encoding="utf-8"?>
+<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns">
+ <s:exception>Sabre\DA (truncated...)
+';
+
+ $reportException = new ClientException(
+ $message,
+ $request,
+ $response
+ );
+
+ $this->client
+ ->method('request')
+ ->willThrowException($reportException);
+
+ $this->service->syncRemoteAddressBook(
+ '',
+ 'system',
+ 'system',
+ '1234567890',
+ null,
+ '1',
+ 'principals/system/system',
+ []
+ );
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providerUseAbsoluteUriReport')]
+ public function testUseAbsoluteUriReport(string $host, string $expected): void {
+ $body = '<?xml version="1.0"?>
+<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:oc="http://owncloud.org/ns">
+ <d:sync-token>http://sabre.io/ns/sync/1</d:sync-token>
+</d:multistatus>';
+
+ $requestResponse = new Response(new PsrResponse(
+ 207,
+ ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)],
+ $body
+ ));
+
+ $this->client
+ ->method('request')
+ ->with(
+ 'REPORT',
+ $this->callback(function ($uri) use ($expected) {
+ $this->assertEquals($expected, $uri);
+ return true;
+ }),
+ $this->callback(function ($options) {
+ $this->assertIsArray($options);
+ return true;
+ }),
+ )
+ ->willReturn($requestResponse);
+
+ $this->service->syncRemoteAddressBook(
+ $host,
+ 'system',
+ 'remote.php/dav/addressbooks/system/system/system',
+ '1234567890',
+ null,
+ '1',
+ 'principals/system/system',
+ []
+ );
}
- /**
- * @param $backend
- * @param $response
- * @return SyncService|\PHPUnit\Framework\MockObject\MockObject
- */
- private function getSyncServiceMock($backend, $response) {
- $userManager = $this->getMockBuilder(IUserManager::class)->disableOriginalConstructor()->getMock();
- $logger = $this->getMockBuilder(ILogger::class)->disableOriginalConstructor()->getMock();
- $converter = $this->createMock(Converter::class);
- /** @var SyncService | \PHPUnit\Framework\MockObject\MockObject $ss */
- $ss = $this->getMockBuilder(SyncService::class)
- ->setMethods(['ensureSystemAddressBookExists', 'requestSyncReport', 'download', 'getCertPath'])
- ->setConstructorArgs([$backend, $userManager, $logger, $converter])
- ->getMock();
- $ss->method('requestSyncReport')->withAnyParameters()->willReturn(['response' => $response, 'token' => 'sync-token-1']);
- $ss->method('ensureSystemAddressBookExists')->willReturn(['id' => 1]);
- $ss->method('download')->willReturn([
- 'body' => '',
- 'statusCode' => 200,
- 'headers' => []
- ]);
- $ss->method('getCertPath')->willReturn('');
- return $ss;
+ public static function providerUseAbsoluteUriReport(): array {
+ return [
+ ['https://server.internal', 'https://server.internal/remote.php/dav/addressbooks/system/system/system'],
+ ['https://server.internal/', 'https://server.internal/remote.php/dav/addressbooks/system/system/system'],
+ ['https://server.internal/nextcloud', 'https://server.internal/nextcloud/remote.php/dav/addressbooks/system/system/system'],
+ ['https://server.internal/nextcloud/', 'https://server.internal/nextcloud/remote.php/dav/addressbooks/system/system/system'],
+ ['https://server.internal:8080', 'https://server.internal:8080/remote.php/dav/addressbooks/system/system/system'],
+ ['https://server.internal:8080/', 'https://server.internal:8080/remote.php/dav/addressbooks/system/system/system'],
+ ['https://server.internal:8080/nextcloud', 'https://server.internal:8080/nextcloud/remote.php/dav/addressbooks/system/system/system'],
+ ['https://server.internal:8080/nextcloud/', 'https://server.internal:8080/nextcloud/remote.php/dav/addressbooks/system/system/system'],
+ ];
}
}
diff --git a/apps/dav/tests/unit/CardDAV/SystemAddressBookTest.php b/apps/dav/tests/unit/CardDAV/SystemAddressBookTest.php
new file mode 100644
index 00000000000..4a218fa4616
--- /dev/null
+++ b/apps/dav/tests/unit/CardDAV/SystemAddressBookTest.php
@@ -0,0 +1,428 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CardDAV;
+
+use OC\AppFramework\Http\Request;
+use OCA\DAV\CardDAV\SyncService;
+use OCA\DAV\CardDAV\SystemAddressbook;
+use OCA\Federation\TrustedServers;
+use OCP\Accounts\IAccountManager;
+use OCP\IConfig;
+use OCP\IGroup;
+use OCP\IGroupManager;
+use OCP\IL10N;
+use OCP\IRequest;
+use OCP\IUser;
+use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\CardDAV\Backend\BackendInterface;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\VObject\Component\VCard;
+use Sabre\VObject\Reader;
+use Test\TestCase;
+
+class SystemAddressBookTest extends TestCase {
+ private BackendInterface&MockObject $cardDavBackend;
+ private array $addressBookInfo;
+ private IL10N&MockObject $l10n;
+ private IConfig&MockObject $config;
+ private IUserSession $userSession;
+ private IRequest&MockObject $request;
+ private array $server;
+ private TrustedServers&MockObject $trustedServers;
+ private IGroupManager&MockObject $groupManager;
+ private SystemAddressbook $addressBook;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->cardDavBackend = $this->createMock(BackendInterface::class);
+ $this->addressBookInfo = [
+ 'id' => 123,
+ '{DAV:}displayname' => 'Accounts',
+ 'principaluri' => 'principals/system/system',
+ ];
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->request = $this->createMock(Request::class);
+ $this->server = [
+ 'PHP_AUTH_USER' => 'system',
+ 'PHP_AUTH_PW' => 'shared123',
+ ];
+ $this->request->method('__get')->with('server')->willReturn($this->server);
+ $this->trustedServers = $this->createMock(TrustedServers::class);
+ $this->groupManager = $this->createMock(IGroupManager::class);
+
+ $this->addressBook = new SystemAddressbook(
+ $this->cardDavBackend,
+ $this->addressBookInfo,
+ $this->l10n,
+ $this->config,
+ $this->userSession,
+ $this->request,
+ $this->trustedServers,
+ $this->groupManager,
+ );
+ }
+
+ public function testGetChildrenAsGuest(): void {
+ $this->config->expects(self::exactly(3))
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
+ ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
+ ]);
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user');
+ $user->method('getBackendClassName')->willReturn('Guests');
+ $this->userSession->expects(self::once())
+ ->method('getUser')
+ ->willReturn($user);
+ $vcfWithScopes = <<<VCF
+BEGIN:VCARD
+VERSION:3.0
+PRODID:-//Sabre//Sabre VObject 4.4.2//EN
+UID:admin
+FN;X-NC-SCOPE=v2-federated:admin
+N;X-NC-SCOPE=v2-federated:admin;;;;
+ADR;TYPE=OTHER;X-NC-SCOPE=v2-local:Testing test test test;;;;;;
+EMAIL;TYPE=OTHER;X-NC-SCOPE=v2-federated:miau_lalala@gmx.net
+TEL;TYPE=OTHER;X-NC-SCOPE=v2-local:+435454454544
+CLOUD:admin@http://localhost
+END:VCARD
+VCF;
+ $originalCard = [
+ 'carddata' => $vcfWithScopes,
+ ];
+ $this->cardDavBackend->expects(self::once())
+ ->method('getCard')
+ ->with(123, 'Guests:user.vcf')
+ ->willReturn($originalCard);
+
+ $children = $this->addressBook->getChildren();
+
+ self::assertCount(1, $children);
+ }
+
+ public function testGetFilteredChildForFederation(): void {
+ $this->config->expects(self::exactly(3))
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
+ ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
+ ]);
+ $this->trustedServers->expects(self::once())
+ ->method('getServers')
+ ->willReturn([
+ [
+ 'shared_secret' => 'shared123',
+ ],
+ ]);
+ $vcfWithScopes = <<<VCF
+BEGIN:VCARD
+VERSION:3.0
+PRODID:-//Sabre//Sabre VObject 4.4.2//EN
+UID:admin
+FN;X-NC-SCOPE=v2-federated:admin
+N;X-NC-SCOPE=v2-federated:admin;;;;
+ADR;TYPE=OTHER;X-NC-SCOPE=v2-local:Testing test test test;;;;;;
+EMAIL;TYPE=OTHER;X-NC-SCOPE=v2-federated:miau_lalala@gmx.net
+TEL;TYPE=OTHER;X-NC-SCOPE=v2-local:+435454454544
+CLOUD:admin@http://localhost
+END:VCARD
+VCF;
+ $originalCard = [
+ 'carddata' => $vcfWithScopes,
+ ];
+ $this->cardDavBackend->expects(self::once())
+ ->method('getCard')
+ ->with(123, 'user.vcf')
+ ->willReturn($originalCard);
+
+ $card = $this->addressBook->getChild('user.vcf');
+
+ /** @var VCard $vCard */
+ $vCard = Reader::read($card->get());
+ foreach ($vCard->children() as $child) {
+ $scope = $child->offsetGet('X-NC-SCOPE');
+ if ($scope !== null) {
+ self::assertNotEquals(IAccountManager::SCOPE_PRIVATE, $scope->getValue());
+ self::assertNotEquals(IAccountManager::SCOPE_LOCAL, $scope->getValue());
+ }
+ }
+ }
+
+ public function testGetChildNotFound(): void {
+ $this->config->expects(self::exactly(3))
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
+ ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
+ ]);
+ $this->trustedServers->expects(self::once())
+ ->method('getServers')
+ ->willReturn([
+ [
+ 'shared_secret' => 'shared123',
+ ],
+ ]);
+ $this->expectException(NotFound::class);
+
+ $this->addressBook->getChild('LDAP:user.vcf');
+ }
+
+ public function testGetChildWithoutEnumeration(): void {
+ $this->config->expects(self::exactly(3))
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'no'],
+ ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
+ ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
+ ]);
+ $this->expectException(Forbidden::class);
+
+ $this->addressBook->getChild('LDAP:user.vcf');
+ }
+
+ public function testGetChildAsGuest(): void {
+ $this->config->expects(self::exactly(3))
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
+ ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
+ ]);
+ $user = $this->createMock(IUser::class);
+ $user->method('getBackendClassName')->willReturn('Guests');
+ $this->userSession->expects(self::once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->expectException(Forbidden::class);
+
+ $this->addressBook->getChild('LDAP:user.vcf');
+ }
+
+ public function testGetChildWithGroupEnumerationRestriction(): void {
+ $this->config->expects(self::exactly(3))
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
+ ]);
+ $user = $this->createMock(IUser::class);
+ $user->method('getBackendClassName')->willReturn('LDAP');
+ $this->userSession->expects(self::once())
+ ->method('getUser')
+ ->willReturn($user);
+ $otherUser = $this->createMock(IUser::class);
+ $user->method('getBackendClassName')->willReturn('LDAP');
+ $otherUser->method('getUID')->willReturn('other');
+ $group = $this->createMock(IGroup::class);
+ $group->expects(self::once())
+ ->method('getUsers')
+ ->willReturn([$otherUser]);
+ $this->groupManager->expects(self::once())
+ ->method('getUserGroups')
+ ->with($user)
+ ->willReturn([$group]);
+ $cardData = <<<VCF
+BEGIN:VCARD
+VERSION:3.0
+PRODID:-//Sabre//Sabre VObject 4.4.2//EN
+UID:admin
+FN;X-NC-SCOPE=v2-federated:other
+END:VCARD
+VCF;
+ $this->cardDavBackend->expects(self::once())
+ ->method('getCard')
+ ->with($this->addressBookInfo['id'], "{$otherUser->getBackendClassName()}:{$otherUser->getUID()}.vcf")
+ ->willReturn([
+ 'id' => 123,
+ 'carddata' => $cardData,
+ ]);
+
+ $this->addressBook->getChild("{$otherUser->getBackendClassName()}:{$otherUser->getUID()}.vcf");
+ }
+
+ public function testGetChildWithPhoneNumberEnumerationRestriction(): void {
+ $this->config->expects(self::exactly(3))
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
+ ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'yes'],
+ ]);
+ $user = $this->createMock(IUser::class);
+ $user->method('getBackendClassName')->willReturn('LDAP');
+ $this->userSession->expects(self::once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->expectException(Forbidden::class);
+
+ $this->addressBook->getChild('LDAP:user.vcf');
+ }
+
+ public function testGetOwnChildWithPhoneNumberEnumerationRestriction(): void {
+ $this->config->expects(self::exactly(3))
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
+ ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'yes'],
+ ]);
+ $user = $this->createMock(IUser::class);
+ $user->method('getBackendClassName')->willReturn('LDAP');
+ $user->method('getUID')->willReturn('user');
+ $this->userSession->expects(self::once())
+ ->method('getUser')
+ ->willReturn($user);
+ $cardData = <<<VCF
+BEGIN:VCARD
+VERSION:3.0
+PRODID:-//Sabre//Sabre VObject 4.4.2//EN
+UID:admin
+FN;X-NC-SCOPE=v2-federated:user
+END:VCARD
+VCF;
+ $this->cardDavBackend->expects(self::once())
+ ->method('getCard')
+ ->with($this->addressBookInfo['id'], 'LDAP:user.vcf')
+ ->willReturn([
+ 'id' => 123,
+ 'carddata' => $cardData,
+ ]);
+
+ $this->addressBook->getChild('LDAP:user.vcf');
+ }
+
+ public function testGetMultipleChildrenWithGroupEnumerationRestriction(): void {
+ $this->config
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
+ ]);
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user');
+ $user->method('getBackendClassName')->willReturn('LDAP');
+ $other1 = $this->createMock(IUser::class);
+ $other1->method('getUID')->willReturn('other1');
+ $other1->method('getBackendClassName')->willReturn('LDAP');
+ $other2 = $this->createMock(IUser::class);
+ $other2->method('getUID')->willReturn('other2');
+ $other2->method('getBackendClassName')->willReturn('LDAP');
+ $other3 = $this->createMock(IUser::class);
+ $other3->method('getUID')->willReturn('other3');
+ $other3->method('getBackendClassName')->willReturn('LDAP');
+ $this->userSession
+ ->method('getUser')
+ ->willReturn($user);
+ $group1 = $this->createMock(IGroup::class);
+ $group1
+ ->method('getUsers')
+ ->willReturn([$user, $other1]);
+ $group2 = $this->createMock(IGroup::class);
+ $group2
+ ->method('getUsers')
+ ->willReturn([$other1, $other2, $user]);
+ $this->groupManager
+ ->method('getUserGroups')
+ ->with($user)
+ ->willReturn([$group1]);
+ $this->cardDavBackend->expects(self::once())
+ ->method('getMultipleCards')
+ ->with($this->addressBookInfo['id'], [
+ SyncService::getCardUri($user),
+ SyncService::getCardUri($other1),
+ ])
+ ->willReturn([
+ [],
+ [],
+ ]);
+
+ $cards = $this->addressBook->getMultipleChildren([
+ SyncService::getCardUri($user),
+ SyncService::getCardUri($other1),
+ // SyncService::getCardUri($other2), // Omitted to test that it's not returned as stray
+ SyncService::getCardUri($other3), // No overlapping group with this one
+ ]);
+
+ self::assertCount(2, $cards);
+ }
+
+ public function testGetMultipleChildrenAsGuest(): void {
+ $this->config
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
+ ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
+ ]);
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user');
+ $user->method('getBackendClassName')->willReturn('Guests');
+ $this->userSession->expects(self::once())
+ ->method('getUser')
+ ->willReturn($user);
+
+ $cards = $this->addressBook->getMultipleChildren(['Database:user1.vcf', 'LDAP:user2.vcf']);
+
+ self::assertEmpty($cards);
+ }
+
+ public function testGetMultipleChildren(): void {
+ $this->config
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'],
+ ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'],
+ ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'],
+ ]);
+ $this->trustedServers
+ ->method('getServers')
+ ->willReturn([
+ [
+ 'shared_secret' => 'shared123',
+ ],
+ ]);
+ $cardData = <<<VCF
+BEGIN:VCARD
+VERSION:3.0
+PRODID:-//Sabre//Sabre VObject 4.4.2//EN
+UID:admin
+FN;X-NC-SCOPE=v2-federated:user
+END:VCARD
+VCF;
+ $this->cardDavBackend->expects(self::once())
+ ->method('getMultipleCards')
+ ->with($this->addressBookInfo['id'], ['Database:user1.vcf', 'LDAP:user2.vcf'])
+ ->willReturn([
+ [
+ 'id' => 123,
+ 'carddata' => $cardData,
+ ],
+ [
+ 'id' => 321,
+ 'carddata' => $cardData,
+ ],
+ ]);
+
+ $cards = $this->addressBook->getMultipleChildren(['Database:user1.vcf', 'LDAP:user2.vcf']);
+
+ self::assertCount(2, $cards);
+ }
+}
diff --git a/apps/dav/tests/unit/CardDAV/Validation/CardDavValidatePluginTest.php b/apps/dav/tests/unit/CardDAV/Validation/CardDavValidatePluginTest.php
new file mode 100644
index 00000000000..058735ba32a
--- /dev/null
+++ b/apps/dav/tests/unit/CardDAV/Validation/CardDavValidatePluginTest.php
@@ -0,0 +1,73 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\CardDAV\Validation;
+
+use OCA\DAV\CardDAV\Validation\CardDavValidatePlugin;
+use OCP\IAppConfig;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Test\TestCase;
+
+class CardDavValidatePluginTest extends TestCase {
+
+ private CardDavValidatePlugin $plugin;
+ private IAppConfig&MockObject $config;
+ private RequestInterface&MockObject $request;
+ private ResponseInterface&MockObject $response;
+
+ protected function setUp(): void {
+ parent::setUp();
+ // construct mock objects
+ $this->config = $this->createMock(IAppConfig::class);
+ $this->request = $this->createMock(RequestInterface::class);
+ $this->response = $this->createMock(ResponseInterface::class);
+ $this->plugin = new CardDavValidatePlugin(
+ $this->config,
+ );
+ }
+
+ public function testPutSizeLessThenLimit(): void {
+
+ // construct method responses
+ $this->config
+ ->method('getValueInt')
+ ->with('dav', 'card_size_limit', 5242880)
+ ->willReturn(5242880);
+ $this->request
+ ->method('getRawServerValue')
+ ->with('CONTENT_LENGTH')
+ ->willReturn('1024');
+ // test condition
+ $this->assertTrue(
+ $this->plugin->beforePut($this->request, $this->response)
+ );
+
+ }
+
+ public function testPutSizeMoreThenLimit(): void {
+
+ // construct method responses
+ $this->config
+ ->method('getValueInt')
+ ->with('dav', 'card_size_limit', 5242880)
+ ->willReturn(5242880);
+ $this->request
+ ->method('getRawServerValue')
+ ->with('CONTENT_LENGTH')
+ ->willReturn('6242880');
+ $this->expectException(Forbidden::class);
+ // test condition
+ $this->plugin->beforePut($this->request, $this->response);
+
+ }
+
+}
diff --git a/apps/dav/tests/unit/Command/DeleteCalendarTest.php b/apps/dav/tests/unit/Command/DeleteCalendarTest.php
index db0ee31f6be..2bd269de6dc 100644
--- a/apps/dav/tests/unit/Command/DeleteCalendarTest.php
+++ b/apps/dav/tests/unit/Command/DeleteCalendarTest.php
@@ -2,30 +2,14 @@
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: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-namespace OCA\DAV\Tests\Command;
+namespace OCA\DAV\Tests\unit\Command;
use OCA\DAV\CalDAV\BirthdayService;
-use OCA\DAV\CalDav\CalDavBackend;
+use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\Command\DeleteCalendar;
use OCP\IConfig;
use OCP\IL10N;
@@ -44,23 +28,12 @@ class DeleteCalendarTest extends TestCase {
public const USER = 'user';
public const NAME = 'calendar';
- /** @var CalDavBackend|MockObject */
- private $calDav;
-
- /** @var IConfig|MockObject */
- private $config;
-
- /** @var IL10N|MockObject */
- private $l10n;
-
- /** @var IUserManager|MockObject */
- private $userManager;
-
- /** @var DeleteCalendar */
- private $command;
-
- /** @var MockObject|LoggerInterface */
- private $logger;
+ private CalDavBackend&MockObject $calDav;
+ private IConfig&MockObject $config;
+ private IL10N&MockObject $l10n;
+ private IUserManager&MockObject $userManager;
+ private LoggerInterface&MockObject $logger;
+ private DeleteCalendar $command;
protected function setUp(): void {
parent::setUp();
@@ -80,7 +53,7 @@ class DeleteCalendarTest extends TestCase {
);
}
- public function testInvalidUser() {
+ public function testInvalidUser(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage(
'User <' . self::USER . '> is unknown.');
@@ -97,7 +70,7 @@ class DeleteCalendarTest extends TestCase {
]);
}
- public function testNoCalendarName() {
+ public function testNoCalendarName(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage(
'Please specify a calendar name or --birthday');
@@ -113,10 +86,10 @@ class DeleteCalendarTest extends TestCase {
]);
}
- public function testInvalidCalendar() {
+ public function testInvalidCalendar(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage(
- 'User <' . self::USER . '> has no calendar named <' . self::NAME . '>.');
+ 'User <' . self::USER . '> has no calendar named <' . self::NAME . '>.');
$this->userManager->expects($this->once())
->method('userExists')
@@ -137,12 +110,12 @@ class DeleteCalendarTest extends TestCase {
]);
}
- public function testDelete() {
+ public function testDelete(): void {
$id = 1234;
$calendar = [
'id' => $id,
'principaluri' => 'principals/users/' . self::USER,
- 'uri' => self::NAME
+ 'uri' => self::NAME,
];
$this->userManager->expects($this->once())
@@ -167,7 +140,7 @@ class DeleteCalendarTest extends TestCase {
]);
}
- public function testForceDelete() {
+ public function testForceDelete(): void {
$id = 1234;
$calendar = [
'id' => $id,
@@ -198,12 +171,13 @@ class DeleteCalendarTest extends TestCase {
]);
}
- public function testDeleteBirthday() {
+ public function testDeleteBirthday(): void {
$id = 1234;
$calendar = [
'id' => $id,
'principaluri' => 'principals/users/' . self::USER,
- 'uri' => BirthdayService::BIRTHDAY_CALENDAR_URI
+ 'uri' => BirthdayService::BIRTHDAY_CALENDAR_URI,
+ '{DAV:}displayname' => 'Test',
];
$this->userManager->expects($this->once())
@@ -228,11 +202,12 @@ class DeleteCalendarTest extends TestCase {
]);
}
- public function testBirthdayHasPrecedence() {
+ public function testBirthdayHasPrecedence(): void {
$calendar = [
'id' => 1234,
'principaluri' => 'principals/users/' . self::USER,
- 'uri' => BirthdayService::BIRTHDAY_CALENDAR_URI
+ 'uri' => BirthdayService::BIRTHDAY_CALENDAR_URI,
+ '{DAV:}displayname' => 'Test',
];
$this->userManager->expects($this->once())
->method('userExists')
diff --git a/apps/dav/tests/unit/Command/ListAddressbooksTest.php b/apps/dav/tests/unit/Command/ListAddressbooksTest.php
new file mode 100644
index 00000000000..2768ed576c3
--- /dev/null
+++ b/apps/dav/tests/unit/Command/ListAddressbooksTest.php
@@ -0,0 +1,107 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\Command;
+
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\Command\ListAddressbooks;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Symfony\Component\Console\Tester\CommandTester;
+use Test\TestCase;
+
+/**
+ * Class ListCalendarsTest
+ *
+ * @package OCA\DAV\Tests\Command
+ */
+class ListAddressbooksTest extends TestCase {
+ private IUserManager&MockObject $userManager;
+ private CardDavBackend&MockObject $cardDavBackend;
+ private ListAddressbooks $command;
+
+ public const USERNAME = 'username';
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->cardDavBackend = $this->createMock(CardDavBackend::class);
+
+ $this->command = new ListAddressbooks(
+ $this->userManager,
+ $this->cardDavBackend
+ );
+ }
+
+ public function testWithBadUser(): void {
+ $this->expectException(\InvalidArgumentException::class);
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with(self::USERNAME)
+ ->willReturn(false);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => self::USERNAME,
+ ]);
+ $this->assertStringContainsString('User <' . self::USERNAME . '> in unknown', $commandTester->getDisplay());
+ }
+
+ public function testWithCorrectUserWithNoCalendars(): void {
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with(self::USERNAME)
+ ->willReturn(true);
+
+ $this->cardDavBackend->expects($this->once())
+ ->method('getAddressBooksForUser')
+ ->with('principals/users/' . self::USERNAME)
+ ->willReturn([]);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => self::USERNAME,
+ ]);
+ $this->assertStringContainsString('User <' . self::USERNAME . "> has no addressbooks\n", $commandTester->getDisplay());
+ }
+
+ public static function dataExecute(): array {
+ return [
+ [false, '✓'],
+ [true, 'x']
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataExecute')]
+ public function testWithCorrectUser(bool $readOnly, string $output): void {
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with(self::USERNAME)
+ ->willReturn(true);
+
+ $this->cardDavBackend->expects($this->once())
+ ->method('getAddressBooksForUser')
+ ->with('principals/users/' . self::USERNAME)
+ ->willReturn([
+ [
+ '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => $readOnly,
+ 'uri' => 'test',
+ '{DAV:}displayname' => 'dp',
+ '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => 'owner-principal',
+ '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname' => 'owner-dp',
+ ]
+ ]);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => self::USERNAME,
+ ]);
+ $this->assertStringContainsString($output, $commandTester->getDisplay());
+ }
+}
diff --git a/apps/dav/tests/unit/Command/ListCalendarSharesTest.php b/apps/dav/tests/unit/Command/ListCalendarSharesTest.php
new file mode 100644
index 00000000000..e5d4251cbf9
--- /dev/null
+++ b/apps/dav/tests/unit/Command/ListCalendarSharesTest.php
@@ -0,0 +1,172 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\Command;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\Command\ListCalendarShares;
+use OCA\DAV\Connector\Sabre\Principal;
+use OCA\DAV\DAV\Sharing\SharingMapper;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Symfony\Component\Console\Tester\CommandTester;
+use Test\TestCase;
+
+class ListCalendarSharesTest extends TestCase {
+
+ private IUserManager&MockObject $userManager;
+ private Principal&MockObject $principal;
+ private CalDavBackend&MockObject $caldav;
+ private SharingMapper $sharingMapper;
+ private ListCalendarShares $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->principal = $this->createMock(Principal::class);
+ $this->caldav = $this->createMock(CalDavBackend::class);
+ $this->sharingMapper = $this->createMock(SharingMapper::class);
+
+ $this->command = new ListCalendarShares(
+ $this->userManager,
+ $this->principal,
+ $this->caldav,
+ $this->sharingMapper,
+ );
+ }
+
+ public function testUserUnknown(): void {
+ $user = 'bob';
+
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage("User $user is unknown");
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with($user)
+ ->willReturn(false);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => $user,
+ ]);
+ }
+
+ public function testPrincipalNotFound(): void {
+ $user = 'bob';
+
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage("Unable to fetch principal for user $user");
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with($user)
+ ->willReturn(true);
+
+ $this->principal->expects($this->once())
+ ->method('getPrincipalByPath')
+ ->with('principals/users/' . $user)
+ ->willReturn(null);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => $user,
+ ]);
+ }
+
+ public function testNoCalendarShares(): void {
+ $user = 'bob';
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with($user)
+ ->willReturn(true);
+
+ $this->principal->expects($this->once())
+ ->method('getPrincipalByPath')
+ ->with('principals/users/' . $user)
+ ->willReturn([
+ 'uri' => 'principals/users/' . $user,
+ ]);
+
+ $this->principal->expects($this->once())
+ ->method('getGroupMembership')
+ ->willReturn([]);
+ $this->principal->expects($this->once())
+ ->method('getCircleMembership')
+ ->willReturn([]);
+
+ $this->sharingMapper->expects($this->once())
+ ->method('getSharesByPrincipals')
+ ->willReturn([]);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => $user,
+ ]);
+
+ $this->assertStringContainsString(
+ "User $user has no calendar shares",
+ $commandTester->getDisplay()
+ );
+ }
+
+ public function testFilterByCalendarId(): void {
+ $user = 'bob';
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with($user)
+ ->willReturn(true);
+
+ $this->principal->expects($this->once())
+ ->method('getPrincipalByPath')
+ ->with('principals/users/' . $user)
+ ->willReturn([
+ 'uri' => 'principals/users/' . $user,
+ ]);
+
+ $this->principal->expects($this->once())
+ ->method('getGroupMembership')
+ ->willReturn([]);
+ $this->principal->expects($this->once())
+ ->method('getCircleMembership')
+ ->willReturn([]);
+
+ $this->sharingMapper->expects($this->once())
+ ->method('getSharesByPrincipals')
+ ->willReturn([
+ [
+ 'id' => 1000,
+ 'principaluri' => 'principals/users/bob',
+ 'type' => 'calendar',
+ 'access' => 2,
+ 'resourceid' => 10
+ ],
+ [
+ 'id' => 1001,
+ 'principaluri' => 'principals/users/bob',
+ 'type' => 'calendar',
+ 'access' => 3,
+ 'resourceid' => 11
+ ],
+ ]);
+
+ $commandTester = new CommandTester($this->command);
+ $commandTester->execute([
+ 'uid' => $user,
+ '--calendar-id' => 10,
+ ]);
+
+ $this->assertStringNotContainsString(
+ '1001',
+ $commandTester->getDisplay()
+ );
+ }
+}
diff --git a/apps/dav/tests/unit/Command/ListCalendarsTest.php b/apps/dav/tests/unit/Command/ListCalendarsTest.php
index 6f200da01bf..d398a7c772f 100644
--- a/apps/dav/tests/unit/Command/ListCalendarsTest.php
+++ b/apps/dav/tests/unit/Command/ListCalendarsTest.php
@@ -1,35 +1,17 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016 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>
- * @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\Tests\Command;
+namespace OCA\DAV\Tests\unit\Command;
use OCA\DAV\CalDAV\BirthdayService;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\Command\ListCalendars;
use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Console\Tester\CommandTester;
use Test\TestCase;
@@ -39,15 +21,9 @@ use Test\TestCase;
* @package OCA\DAV\Tests\Command
*/
class ListCalendarsTest extends TestCase {
-
- /** @var \OCP\IUserManager|\PHPUnit\Framework\MockObject\MockObject $userManager */
- private $userManager;
-
- /** @var CalDavBackend|\PHPUnit\Framework\MockObject\MockObject $l10n */
- private $calDav;
-
- /** @var ListCalendars */
- private $command;
+ private IUserManager&MockObject $userManager;
+ private CalDavBackend&MockObject $calDav;
+ private ListCalendars $command;
public const USERNAME = 'username';
@@ -63,7 +39,7 @@ class ListCalendarsTest extends TestCase {
);
}
- public function testWithBadUser() {
+ public function testWithBadUser(): void {
$this->expectException(\InvalidArgumentException::class);
$this->userManager->expects($this->once())
@@ -75,10 +51,10 @@ class ListCalendarsTest extends TestCase {
$commandTester->execute([
'uid' => self::USERNAME,
]);
- $this->assertStringContainsString("User <" . self::USERNAME . "> in unknown", $commandTester->getDisplay());
+ $this->assertStringContainsString('User <' . self::USERNAME . '> in unknown', $commandTester->getDisplay());
}
- public function testWithCorrectUserWithNoCalendars() {
+ public function testWithCorrectUserWithNoCalendars(): void {
$this->userManager->expects($this->once())
->method('userExists')
->with(self::USERNAME)
@@ -93,20 +69,18 @@ class ListCalendarsTest extends TestCase {
$commandTester->execute([
'uid' => self::USERNAME,
]);
- $this->assertStringContainsString("User <" . self::USERNAME . "> has no calendars\n", $commandTester->getDisplay());
+ $this->assertStringContainsString('User <' . self::USERNAME . "> has no calendars\n", $commandTester->getDisplay());
}
- public function dataExecute() {
+ public static function dataExecute(): array {
return [
[false, '✓'],
[true, 'x']
];
}
- /**
- * @dataProvider dataExecute
- */
- public function testWithCorrectUser(bool $readOnly, string $output) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataExecute')]
+ public function testWithCorrectUser(bool $readOnly, string $output): void {
$this->userManager->expects($this->once())
->method('userExists')
->with(self::USERNAME)
diff --git a/apps/dav/tests/unit/Command/MoveCalendarTest.php b/apps/dav/tests/unit/Command/MoveCalendarTest.php
index 5a858e140ac..e9f016961f2 100644
--- a/apps/dav/tests/unit/Command/MoveCalendarTest.php
+++ b/apps/dav/tests/unit/Command/MoveCalendarTest.php
@@ -1,30 +1,11 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016 Thomas Citharel <nextcloud@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>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have 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\Tests\Command;
+namespace OCA\DAV\Tests\unit\Command;
use InvalidArgumentException;
use OCA\DAV\CalDAV\CalDavBackend;
@@ -45,30 +26,14 @@ use Test\TestCase;
* @package OCA\DAV\Tests\Command
*/
class MoveCalendarTest extends TestCase {
-
- /** @var \OCP\IUserManager|MockObject $userManager */
- private $userManager;
-
- /** @var \OCP\IGroupManager|MockObject $groupManager */
- private $groupManager;
-
- /** @var \OCP\Share\IManager|MockObject $shareManager */
- private $shareManager;
-
- /** @var IConfig|MockObject $l10n */
- private $config;
-
- /** @var IL10N|MockObject $l10n */
- private $l10n;
-
- /** @var CalDavBackend|MockObject $l10n */
- private $calDav;
-
- /** @var MoveCalendar */
- private $command;
-
- /** @var LoggerInterface|MockObject */
- private $logger;
+ private IUserManager&MockObject $userManager;
+ private IGroupManager&MockObject $groupManager;
+ private \OCP\Share\IManager&MockObject $shareManager;
+ private IConfig&MockObject $config;
+ private IL10N&MockObject $l10n;
+ private CalDavBackend&MockObject $calDav;
+ private LoggerInterface&MockObject $logger;
+ private MoveCalendar $command;
protected function setUp(): void {
parent::setUp();
@@ -92,33 +57,23 @@ class MoveCalendarTest extends TestCase {
);
}
- public function dataExecute() {
+ public static function dataExecute(): array {
return [
[false, true],
[true, false]
];
}
- /**
- * @dataProvider dataExecute
- *
- * @param $userOriginExists
- * @param $userDestinationExists
- */
- public function testWithBadUserOrigin($userOriginExists, $userDestinationExists) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataExecute')]
+ public function testWithBadUserOrigin(bool $userOriginExists, bool $userDestinationExists): void {
$this->expectException(\InvalidArgumentException::class);
- $this->userManager->expects($this->at(0))
+ $this->userManager->expects($this->exactly($userOriginExists ? 2 : 1))
->method('userExists')
- ->with('user')
- ->willReturn($userOriginExists);
-
- if (!$userDestinationExists) {
- $this->userManager->expects($this->at(1))
- ->method('userExists')
- ->with('user2')
- ->willReturn($userDestinationExists);
- }
+ ->willReturnMap([
+ ['user', $userOriginExists],
+ ['user2', $userDestinationExists],
+ ]);
$commandTester = new CommandTester($this->command);
$commandTester->execute([
@@ -129,19 +84,16 @@ class MoveCalendarTest extends TestCase {
}
- public function testMoveWithInexistantCalendar() {
+ public function testMoveWithInexistantCalendar(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('User <user> has no calendar named <personal>. You can run occ dav:list-calendars to list calendars URIs for this user.');
- $this->userManager->expects($this->at(0))
+ $this->userManager->expects($this->exactly(2))
->method('userExists')
- ->with('user')
- ->willReturn(true);
-
- $this->userManager->expects($this->at(1))
- ->method('userExists')
- ->with('user2')
- ->willReturn(true);
+ ->willReturnMap([
+ ['user', true],
+ ['user2', true],
+ ]);
$this->calDav->expects($this->once())->method('getCalendarByUri')
->with('principals/users/user', 'personal')
@@ -156,30 +108,26 @@ class MoveCalendarTest extends TestCase {
}
- public function testMoveWithExistingDestinationCalendar() {
+ public function testMoveWithExistingDestinationCalendar(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('User <user2> already has a calendar named <personal>.');
- $this->userManager->expects($this->at(0))
- ->method('userExists')
- ->with('user')
- ->willReturn(true);
-
- $this->userManager->expects($this->at(1))
+ $this->userManager->expects($this->exactly(2))
->method('userExists')
- ->with('user2')
- ->willReturn(true);
-
- $this->calDav->expects($this->at(0))->method('getCalendarByUri')
- ->with('principals/users/user', 'personal')
- ->willReturn([
- 'id' => 1234,
+ ->willReturnMap([
+ ['user', true],
+ ['user2', true],
]);
- $this->calDav->expects($this->at(1))->method('getCalendarByUri')
- ->with('principals/users/user2', 'personal')
- ->willReturn([
- 'id' => 1234,
+ $this->calDav->expects($this->exactly(2))
+ ->method('getCalendarByUri')
+ ->willReturnMap([
+ ['principals/users/user', 'personal', [
+ 'id' => 1234,
+ ]],
+ ['principals/users/user2', 'personal', [
+ 'id' => 1234,
+ ]],
]);
$commandTester = new CommandTester($this->command);
@@ -190,26 +138,22 @@ class MoveCalendarTest extends TestCase {
]);
}
- public function testMove() {
- $this->userManager->expects($this->at(0))
+ public function testMove(): void {
+ $this->userManager->expects($this->exactly(2))
->method('userExists')
- ->with('user')
- ->willReturn(true);
-
- $this->userManager->expects($this->at(1))
- ->method('userExists')
- ->with('user2')
- ->willReturn(true);
-
- $this->calDav->expects($this->at(0))->method('getCalendarByUri')
- ->with('principals/users/user', 'personal')
- ->willReturn([
- 'id' => 1234,
+ ->willReturnMap([
+ ['user', true],
+ ['user2', true],
]);
- $this->calDav->expects($this->at(1))->method('getCalendarByUri')
- ->with('principals/users/user2', 'personal')
- ->willReturn(null);
+ $this->calDav->expects($this->exactly(2))
+ ->method('getCalendarByUri')
+ ->willReturnMap([
+ ['principals/users/user', 'personal', [
+ 'id' => 1234,
+ ]],
+ ['principals/users/user2', 'personal', null],
+ ]);
$this->calDav->expects($this->once())->method('getShares')
->with(1234)
@@ -222,40 +166,34 @@ class MoveCalendarTest extends TestCase {
'destinationuid' => 'user2',
]);
- $this->assertStringContainsString("[OK] Calendar <personal> was moved from user <user> to <user2>", $commandTester->getDisplay());
+ $this->assertStringContainsString('[OK] Calendar <personal> was moved from user <user> to <user2>', $commandTester->getDisplay());
}
- public function dataTestMoveWithDestinationNotPartOfGroup(): array {
+ public static function dataTestMoveWithDestinationNotPartOfGroup(): array {
return [
[true],
[false]
];
}
- /**
- * @dataProvider dataTestMoveWithDestinationNotPartOfGroup
- */
- public function testMoveWithDestinationNotPartOfGroup(bool $shareWithGroupMembersOnly) {
- $this->userManager->expects($this->at(0))
- ->method('userExists')
- ->with('user')
- ->willReturn(true);
-
- $this->userManager->expects($this->at(1))
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTestMoveWithDestinationNotPartOfGroup')]
+ public function testMoveWithDestinationNotPartOfGroup(bool $shareWithGroupMembersOnly): void {
+ $this->userManager->expects($this->exactly(2))
->method('userExists')
- ->with('user2')
- ->willReturn(true);
-
- $this->calDav->expects($this->at(0))->method('getCalendarByUri')
- ->with('principals/users/user', 'personal')
- ->willReturn([
- 'id' => 1234,
- 'uri' => 'personal'
+ ->willReturnMap([
+ ['user', true],
+ ['user2', true],
]);
- $this->calDav->expects($this->at(1))->method('getCalendarByUri')
- ->with('principals/users/user2', 'personal')
- ->willReturn(null);
+ $this->calDav->expects($this->exactly(2))
+ ->method('getCalendarByUri')
+ ->willReturnMap([
+ ['principals/users/user', 'personal', [
+ 'id' => 1234,
+ 'uri' => 'personal',
+ ]],
+ ['principals/users/user2', 'personal', null],
+ ]);
$this->shareManager->expects($this->once())->method('shareWithGroupMembersOnly')
->willReturn($shareWithGroupMembersOnly);
@@ -267,7 +205,7 @@ class MoveCalendarTest extends TestCase {
]);
if ($shareWithGroupMembersOnly === true) {
$this->expectException(InvalidArgumentException::class);
- $this->expectExceptionMessage("User <user2> is not part of the group <nextclouders> with whom the calendar <personal> was shared. You may use -f to move the calendar while deleting this share.");
+ $this->expectExceptionMessage('User <user2> is not part of the group <nextclouders> with whom the calendar <personal> was shared. You may use -f to move the calendar while deleting this share.');
}
$commandTester = new CommandTester($this->command);
@@ -278,27 +216,23 @@ class MoveCalendarTest extends TestCase {
]);
}
- public function testMoveWithDestinationPartOfGroup() {
- $this->userManager->expects($this->at(0))
+ public function testMoveWithDestinationPartOfGroup(): void {
+ $this->userManager->expects($this->exactly(2))
->method('userExists')
- ->with('user')
- ->willReturn(true);
-
- $this->userManager->expects($this->at(1))
- ->method('userExists')
- ->with('user2')
- ->willReturn(true);
-
- $this->calDav->expects($this->at(0))->method('getCalendarByUri')
- ->with('principals/users/user', 'personal')
- ->willReturn([
- 'id' => 1234,
- 'uri' => 'personal'
+ ->willReturnMap([
+ ['user', true],
+ ['user2', true],
]);
- $this->calDav->expects($this->at(1))->method('getCalendarByUri')
- ->with('principals/users/user2', 'personal')
- ->willReturn(null);
+ $this->calDav->expects($this->exactly(2))
+ ->method('getCalendarByUri')
+ ->willReturnMap([
+ ['principals/users/user', 'personal', [
+ 'id' => 1234,
+ 'uri' => 'personal',
+ ]],
+ ['principals/users/user2', 'personal', null],
+ ]);
$this->shareManager->expects($this->once())->method('shareWithGroupMembersOnly')
->willReturn(true);
@@ -320,31 +254,27 @@ class MoveCalendarTest extends TestCase {
'destinationuid' => 'user2',
]);
- $this->assertStringContainsString("[OK] Calendar <personal> was moved from user <user> to <user2>", $commandTester->getDisplay());
+ $this->assertStringContainsString('[OK] Calendar <personal> was moved from user <user> to <user2>', $commandTester->getDisplay());
}
- public function testMoveWithDestinationNotPartOfGroupAndForce() {
- $this->userManager->expects($this->at(0))
- ->method('userExists')
- ->with('user')
- ->willReturn(true);
-
- $this->userManager->expects($this->at(1))
+ public function testMoveWithDestinationNotPartOfGroupAndForce(): void {
+ $this->userManager->expects($this->exactly(2))
->method('userExists')
- ->with('user2')
- ->willReturn(true);
-
- $this->calDav->expects($this->at(0))->method('getCalendarByUri')
- ->with('principals/users/user', 'personal')
- ->willReturn([
- 'id' => 1234,
- 'uri' => 'personal',
- '{DAV:}displayname' => 'Personal'
+ ->willReturnMap([
+ ['user', true],
+ ['user2', true],
]);
- $this->calDav->expects($this->at(1))->method('getCalendarByUri')
- ->with('principals/users/user2', 'personal')
- ->willReturn(null);
+ $this->calDav->expects($this->exactly(2))
+ ->method('getCalendarByUri')
+ ->willReturnMap([
+ ['principals/users/user', 'personal', [
+ 'id' => 1234,
+ 'uri' => 'personal',
+ '{DAV:}displayname' => 'Personal'
+ ]],
+ ['principals/users/user2', 'personal', null],
+ ]);
$this->shareManager->expects($this->once())->method('shareWithGroupMembersOnly')
->willReturn(true);
@@ -367,54 +297,48 @@ class MoveCalendarTest extends TestCase {
'--force' => true
]);
- $this->assertStringContainsString("[OK] Calendar <personal> was moved from user <user> to <user2>", $commandTester->getDisplay());
+ $this->assertStringContainsString('[OK] Calendar <personal> was moved from user <user> to <user2>', $commandTester->getDisplay());
}
- public function dataTestMoveWithCalendarAlreadySharedToDestination(): array {
+ public static function dataTestMoveWithCalendarAlreadySharedToDestination(): array {
return [
[true],
[false]
];
}
- /**
- * @dataProvider dataTestMoveWithCalendarAlreadySharedToDestination
- */
- public function testMoveWithCalendarAlreadySharedToDestination(bool $force) {
- $this->userManager->expects($this->at(0))
- ->method('userExists')
- ->with('user')
- ->willReturn(true);
-
- $this->userManager->expects($this->at(1))
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTestMoveWithCalendarAlreadySharedToDestination')]
+ public function testMoveWithCalendarAlreadySharedToDestination(bool $force): void {
+ $this->userManager->expects($this->exactly(2))
->method('userExists')
- ->with('user2')
- ->willReturn(true);
-
- $this->calDav->expects($this->at(0))->method('getCalendarByUri')
- ->with('principals/users/user', 'personal')
- ->willReturn([
- 'id' => 1234,
- 'uri' => 'personal',
- '{DAV:}displayname' => 'Personal',
+ ->willReturnMap([
+ ['user', true],
+ ['user2', true],
]);
- $this->calDav->expects($this->at(1))->method('getCalendarByUri')
- ->with('principals/users/user2', 'personal')
- ->willReturn(null);
+ $this->calDav->expects($this->exactly(2))
+ ->method('getCalendarByUri')
+ ->willReturnMap([
+ ['principals/users/user', 'personal', [
+ 'id' => 1234,
+ 'uri' => 'personal',
+ '{DAV:}displayname' => 'Personal'
+ ]],
+ ['principals/users/user2', 'personal', null],
+ ]);
$this->calDav->expects($this->once())->method('getShares')
- ->with(1234)
- ->willReturn([
- [
- 'href' => 'principal:principals/users/user2',
- '{DAV:}displayname' => 'Personal'
- ]
- ]);
+ ->with(1234)
+ ->willReturn([
+ [
+ 'href' => 'principal:principals/users/user2',
+ '{DAV:}displayname' => 'Personal'
+ ]
+ ]);
if ($force === false) {
$this->expectException(InvalidArgumentException::class);
- $this->expectExceptionMessage("The calendar <personal> is already shared to user <user2>.You may use -f to move the calendar while deleting this share.");
+ $this->expectExceptionMessage('The calendar <personal> is already shared to user <user2>.You may use -f to move the calendar while deleting this share.');
} else {
$this->calDav->expects($this->once())->method('updateShares');
}
diff --git a/apps/dav/tests/unit/Command/RemoveInvalidSharesTest.php b/apps/dav/tests/unit/Command/RemoveInvalidSharesTest.php
index 66ac40eff83..ec56aa64eb2 100644
--- a/apps/dav/tests/unit/Command/RemoveInvalidSharesTest.php
+++ b/apps/dav/tests/unit/Command/RemoveInvalidSharesTest.php
@@ -1,30 +1,17 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018, ownCloud GmbH
- *
- * @author Morris Jobke <hey@morrisjobke.de>
- * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2018 ownCloud GmbH
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-namespace OCA\DAV\Tests\Unit\Command;
+namespace OCA\DAV\Tests\unit\Command;
use OCA\DAV\Command\RemoveInvalidShares;
use OCA\DAV\Connector\Sabre\Principal;
-use OCP\Migration\IOutput;
+use OCP\IDBConnection;
+use OCP\Server;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Test\TestCase;
@@ -38,7 +25,7 @@ use Test\TestCase;
class RemoveInvalidSharesTest extends TestCase {
protected function setUp(): void {
parent::setUp();
- $db = \OC::$server->getDatabaseConnection();
+ $db = Server::get(IDBConnection::class);
$db->insertIfNotExist('*PREFIX*dav_shares', [
'principaluri' => 'principal:unknown',
@@ -48,20 +35,18 @@ class RemoveInvalidSharesTest extends TestCase {
]);
}
- public function test() {
- $db = \OC::$server->getDatabaseConnection();
- /** @var Principal | \PHPUnit\Framework\MockObject\MockObject $principal */
+ public function test(): void {
+ $db = Server::get(IDBConnection::class);
$principal = $this->createMock(Principal::class);
- /** @var IOutput | \PHPUnit\Framework\MockObject\MockObject $output */
- $output = $this->createMock(IOutput::class);
-
$repair = new RemoveInvalidShares($db, $principal);
$this->invokePrivate($repair, 'run', [$this->createMock(InputInterface::class), $this->createMock(OutputInterface::class)]);
$query = $db->getQueryBuilder();
- $result = $query->select('*')->from('dav_shares')
- ->where($query->expr()->eq('principaluri', $query->createNamedParameter('principal:unknown')))->execute();
+ $query->select('*')
+ ->from('dav_shares')
+ ->where($query->expr()->eq('principaluri', $query->createNamedParameter('principal:unknown')));
+ $result = $query->executeQuery();
$data = $result->fetchAll();
$result->closeCursor();
$this->assertEquals(0, count($data));
diff --git a/apps/dav/tests/unit/Comments/CommentsNodeTest.php b/apps/dav/tests/unit/Comments/CommentsNodeTest.php
index f085ace9d89..9e108b4cf63 100644
--- a/apps/dav/tests/unit/Comments/CommentsNodeTest.php
+++ b/apps/dav/tests/unit/Comments/CommentsNodeTest.php
@@ -1,28 +1,10 @@
<?php
+
+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 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\Tests\unit\Comments;
@@ -30,41 +12,29 @@ use OCA\DAV\Comments\CommentNode;
use OCP\Comments\IComment;
use OCP\Comments\ICommentsManager;
use OCP\Comments\MessageTooLongException;
-use OCP\ILogger;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
use Sabre\DAV\PropPatch;
class CommentsNodeTest extends \Test\TestCase {
-
- /** @var ICommentsManager|\PHPUnit\Framework\MockObject\MockObject */
- protected $commentsManager;
-
- protected $comment;
- protected $node;
- protected $userManager;
- protected $logger;
- protected $userSession;
+ protected ICommentsManager&MockObject $commentsManager;
+ protected IComment&MockObject $comment;
+ protected IUserManager&MockObject $userManager;
+ protected LoggerInterface&MockObject $logger;
+ protected IUserSession&MockObject $userSession;
+ protected CommentNode $node;
protected function setUp(): void {
parent::setUp();
- $this->commentsManager = $this->getMockBuilder(ICommentsManager::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->comment = $this->getMockBuilder(IComment::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->userManager = $this->getMockBuilder(IUserManager::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->userSession = $this->getMockBuilder(IUserSession::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->logger = $this->getMockBuilder(ILogger::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $this->commentsManager = $this->createMock(ICommentsManager::class);
+ $this->comment = $this->createMock(IComment::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
$this->node = new CommentNode(
$this->commentsManager,
@@ -75,11 +45,8 @@ class CommentsNodeTest extends \Test\TestCase {
);
}
- public function testDelete() {
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
-
+ public function testDelete(): void {
+ $user = $this->createMock(IUser::class);
$user->expects($this->once())
->method('getUID')
->willReturn('alice');
@@ -108,13 +75,10 @@ class CommentsNodeTest extends \Test\TestCase {
}
- public function testDeleteForbidden() {
+ public function testDeleteForbidden(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
-
+ $user = $this->createMock(IUser::class);
$user->expects($this->once())
->method('getUID')
->willReturn('mallory');
@@ -140,7 +104,7 @@ class CommentsNodeTest extends \Test\TestCase {
$this->node->delete();
}
- public function testGetName() {
+ public function testGetName(): void {
$id = '19';
$this->comment->expects($this->once())
->method('getId')
@@ -150,23 +114,20 @@ class CommentsNodeTest extends \Test\TestCase {
}
- public function testSetName() {
+ public function testSetName(): void {
$this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class);
$this->node->setName('666');
}
- public function testGetLastModified() {
+ public function testGetLastModified(): void {
$this->assertSame($this->node->getLastModified(), null);
}
- public function testUpdateComment() {
+ public function testUpdateComment(): void {
$msg = 'Hello Earth';
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
-
+ $user = $this->createMock(IUser::class);
$user->expects($this->once())
->method('getUID')
->willReturn('alice');
@@ -195,16 +156,13 @@ class CommentsNodeTest extends \Test\TestCase {
}
- public function testUpdateCommentLogException() {
+ public function testUpdateCommentLogException(): void {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('buh!');
$msg = null;
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
-
+ $user = $this->createMock(IUser::class);
$user->expects($this->once())
->method('getUID')
->willReturn('alice');
@@ -216,7 +174,7 @@ class CommentsNodeTest extends \Test\TestCase {
$this->comment->expects($this->once())
->method('setMessage')
->with($msg)
- ->will($this->throwException(new \Exception('buh!')));
+ ->willThrowException(new \Exception('buh!'));
$this->comment->expects($this->any())
->method('getActorType')
@@ -230,20 +188,17 @@ class CommentsNodeTest extends \Test\TestCase {
->method('save');
$this->logger->expects($this->once())
- ->method('logException');
+ ->method('error');
$this->node->updateComment($msg);
}
- public function testUpdateCommentMessageTooLongException() {
+ public function testUpdateCommentMessageTooLongException(): void {
$this->expectException(\Sabre\DAV\Exception\BadRequest::class);
$this->expectExceptionMessage('Message exceeds allowed character limit of');
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
-
+ $user = $this->createMock(IUser::class);
$user->expects($this->once())
->method('getUID')
->willReturn('alice');
@@ -254,7 +209,7 @@ class CommentsNodeTest extends \Test\TestCase {
$this->comment->expects($this->once())
->method('setMessage')
- ->will($this->throwException(new MessageTooLongException()));
+ ->willThrowException(new MessageTooLongException());
$this->comment->expects($this->any())
->method('getActorType')
@@ -268,22 +223,19 @@ class CommentsNodeTest extends \Test\TestCase {
->method('save');
$this->logger->expects($this->once())
- ->method('logException');
+ ->method('error');
// imagine 'foo' has >1k characters. comment is mocked anyway.
$this->node->updateComment('foo');
}
- public function testUpdateForbiddenByUser() {
+ public function testUpdateForbiddenByUser(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$msg = 'HaXX0r';
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
-
+ $user = $this->createMock(IUser::class);
$user->expects($this->once())
->method('getUID')
->willReturn('mallory');
@@ -310,15 +262,12 @@ class CommentsNodeTest extends \Test\TestCase {
}
- public function testUpdateForbiddenByType() {
+ public function testUpdateForbiddenByType(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$msg = 'HaXX0r';
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
-
+ $user = $this->createMock(IUser::class);
$user->expects($this->never())
->method('getUID');
@@ -340,7 +289,7 @@ class CommentsNodeTest extends \Test\TestCase {
}
- public function testUpdateForbiddenByNotLoggedIn() {
+ public function testUpdateForbiddenByNotLoggedIn(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$msg = 'HaXX0r';
@@ -362,11 +311,8 @@ class CommentsNodeTest extends \Test\TestCase {
$this->node->updateComment($msg);
}
- public function testPropPatch() {
- $propPatch = $this->getMockBuilder(PropPatch::class)
- ->disableOriginalConstructor()
- ->getMock();
-
+ public function testPropPatch(): void {
+ $propPatch = $this->createMock(PropPatch::class);
$propPatch->expects($this->once())
->method('handle')
->with('{http://owncloud.org/ns}message');
@@ -374,7 +320,7 @@ class CommentsNodeTest extends \Test\TestCase {
$this->node->propPatch($propPatch);
}
- public function testGetProperties() {
+ public function testGetProperties(): void {
$ns = '{http://owncloud.org/ns}';
$expected = [
$ns . 'id' => '123',
@@ -405,15 +351,20 @@ class CommentsNodeTest extends \Test\TestCase {
$ns . 'referenceId' => 'ref',
$ns . 'isUnread' => null,
$ns . 'reactions' => [],
+ $ns . 'metaData' => [
+ 'last_edited_at' => 1702553770,
+ 'last_edited_by_id' => 'charly',
+ 'last_edited_by_type' => 'user',
+ ],
+ $ns . 'expireDate' => new \DateTime('2016-01-12 19:00:00'),
];
$this->commentsManager->expects($this->exactly(2))
->method('resolveDisplayName')
- ->withConsecutive(
- [$this->equalTo('user'), $this->equalTo('alice')],
- [$this->equalTo('user'), $this->equalTo('bob')]
- )
- ->willReturnOnConsecutiveCalls('Alice Al-Isson', 'Unknown user');
+ ->willReturnMap([
+ ['user', 'alice', 'Alice Al-Isson'],
+ ['user', 'bob', 'Unknown user']
+ ]);
$this->comment->expects($this->once())
->method('getId')
@@ -474,6 +425,14 @@ class CommentsNodeTest extends \Test\TestCase {
->method('getReferenceId')
->willReturn($expected[$ns . 'referenceId']);
+ $this->comment->expects($this->once())
+ ->method('getMetaData')
+ ->willReturn($expected[$ns . 'metaData']);
+
+ $this->comment->expects($this->once())
+ ->method('getExpireDate')
+ ->willReturn($expected[$ns . 'expireDate']);
+
$user = $this->getMockBuilder(IUser::class)
->disableOriginalConstructor()
->getMock();
@@ -489,14 +448,14 @@ class CommentsNodeTest extends \Test\TestCase {
$properties = $this->node->getProperties(null);
foreach ($properties as $name => $value) {
- $this->assertArrayHasKey($name, $expected);
+ $this->assertArrayHasKey($name, $expected, 'Key not found in the list of $expected');
$this->assertSame($expected[$name], $value);
unset($expected[$name]);
}
$this->assertTrue(empty($expected));
}
- public function readCommentProvider() {
+ public static function readCommentProvider(): array {
$creationDT = new \DateTime('2016-01-19 18:48:00');
$diff = new \DateInterval('PT2H');
$readDT1 = clone $creationDT;
@@ -510,11 +469,8 @@ class CommentsNodeTest extends \Test\TestCase {
];
}
- /**
- * @dataProvider readCommentProvider
- * @param $expected
- */
- public function testGetPropertiesUnreadProperty($creationDT, $readDT, $expected) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('readCommentProvider')]
+ public function testGetPropertiesUnreadProperty(\DateTime $creationDT, ?\DateTime $readDT, string $expected): void {
$this->comment->expects($this->any())
->method('getCreationDateTime')
->willReturn($creationDT);
diff --git a/apps/dav/tests/unit/Comments/CommentsPluginTest.php b/apps/dav/tests/unit/Comments/CommentsPluginTest.php
index 5d05b278e18..18d32772f7b 100644
--- a/apps/dav/tests/unit/Comments/CommentsPluginTest.php
+++ b/apps/dav/tests/unit/Comments/CommentsPluginTest.php
@@ -1,28 +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 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\Tests\unit\Comments;
@@ -33,49 +14,35 @@ use OCP\Comments\IComment;
use OCP\Comments\ICommentsManager;
use OCP\IUser;
use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\INode;
use Sabre\DAV\Tree;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class CommentsPluginTest extends \Test\TestCase {
- /** @var \Sabre\DAV\Server */
- private $server;
-
- /** @var Tree */
- private $tree;
-
- /** @var ICommentsManager */
- private $commentsManager;
-
- /** @var IUserSession */
- private $userSession;
-
- /** @var CommentsPluginImplementation */
- private $plugin;
+ private \Sabre\DAV\Server&MockObject $server;
+ private Tree&MockObject $tree;
+ private ICommentsManager&MockObject $commentsManager;
+ private IUserSession&MockObject $userSession;
+ private CommentsPluginImplementation $plugin;
protected function setUp(): void {
parent::setUp();
- $this->tree = $this->getMockBuilder(Tree::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $this->tree = $this->createMock(Tree::class);
- $this->server = $this->getMockBuilder('\Sabre\DAV\Server')
+ $this->server = $this->getMockBuilder(\Sabre\DAV\Server::class)
->setConstructorArgs([$this->tree])
- ->setMethods(['getRequestUri'])
+ ->onlyMethods(['getRequestUri'])
->getMock();
- $this->commentsManager = $this->getMockBuilder(ICommentsManager::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->userSession = $this->getMockBuilder(IUserSession::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $this->commentsManager = $this->createMock(ICommentsManager::class);
+ $this->userSession = $this->createMock(IUserSession::class);
$this->plugin = new CommentsPluginImplementation($this->commentsManager, $this->userSession);
}
- public function testCreateComment() {
+ public function testCreateComment(): void {
$commentData = [
'actorType' => 'users',
'verb' => 'comment',
@@ -170,8 +137,8 @@ class CommentsPluginTest extends \Test\TestCase {
$this->plugin->httpPost($request, $response);
}
-
- public function testCreateCommentInvalidObject() {
+
+ public function testCreateCommentInvalidObject(): void {
$this->expectException(\Sabre\DAV\Exception\NotFound::class);
$commentData = [
@@ -217,7 +184,7 @@ class CommentsPluginTest extends \Test\TestCase {
$this->tree->expects($this->any())
->method('getNodeForPath')
->with('/' . $path)
- ->will($this->throwException(new \Sabre\DAV\Exception\NotFound()));
+ ->willThrowException(new \Sabre\DAV\Exception\NotFound());
$request = $this->getMockBuilder(RequestInterface::class)
->disableOriginalConstructor()
@@ -252,8 +219,8 @@ class CommentsPluginTest extends \Test\TestCase {
$this->plugin->httpPost($request, $response);
}
-
- public function testCreateCommentInvalidActor() {
+
+ public function testCreateCommentInvalidActor(): void {
$this->expectException(\Sabre\DAV\Exception\BadRequest::class);
$commentData = [
@@ -340,8 +307,8 @@ class CommentsPluginTest extends \Test\TestCase {
$this->plugin->httpPost($request, $response);
}
-
- public function testCreateCommentUnsupportedMediaType() {
+
+ public function testCreateCommentUnsupportedMediaType(): void {
$this->expectException(\Sabre\DAV\Exception\UnsupportedMediaType::class);
$commentData = [
@@ -428,8 +395,8 @@ class CommentsPluginTest extends \Test\TestCase {
$this->plugin->httpPost($request, $response);
}
-
- public function testCreateCommentInvalidPayload() {
+
+ public function testCreateCommentInvalidPayload(): void {
$this->expectException(\Sabre\DAV\Exception\BadRequest::class);
$commentData = [
@@ -522,8 +489,8 @@ class CommentsPluginTest extends \Test\TestCase {
$this->plugin->httpPost($request, $response);
}
-
- public function testCreateCommentMessageTooLong() {
+
+ public function testCreateCommentMessageTooLong(): void {
$this->expectException(\Sabre\DAV\Exception\BadRequest::class);
$this->expectExceptionMessage('Message exceeds allowed character limit of');
@@ -616,8 +583,8 @@ class CommentsPluginTest extends \Test\TestCase {
$this->plugin->httpPost($request, $response);
}
-
- public function testOnReportInvalidNode() {
+
+ public function testOnReportInvalidNode(): void {
$this->expectException(\Sabre\DAV\Exception\ReportNotSupported::class);
$path = 'totally/unrelated/13';
@@ -639,8 +606,8 @@ class CommentsPluginTest extends \Test\TestCase {
$this->plugin->onReport(CommentsPluginImplementation::REPORT_NAME, [], '/' . $path);
}
-
- public function testOnReportInvalidReportName() {
+
+ public function testOnReportInvalidReportName(): void {
$this->expectException(\Sabre\DAV\Exception\ReportNotSupported::class);
$path = 'comments/files/42';
@@ -662,7 +629,7 @@ class CommentsPluginTest extends \Test\TestCase {
$this->plugin->onReport('{whoever}whatever', [], '/' . $path);
}
- public function testOnReportDateTimeEmpty() {
+ public function testOnReportDateTimeEmpty(): void {
$path = 'comments/files/42';
$parameters = [
@@ -717,7 +684,7 @@ class CommentsPluginTest extends \Test\TestCase {
$this->plugin->onReport(CommentsPluginImplementation::REPORT_NAME, $parameters, '/' . $path);
}
- public function testOnReport() {
+ public function testOnReport(): void {
$path = 'comments/files/42';
$parameters = [
diff --git a/apps/dav/tests/unit/Comments/EntityCollectionTest.php b/apps/dav/tests/unit/Comments/EntityCollectionTest.php
index 4466b0a6307..29ebde7d602 100644
--- a/apps/dav/tests/unit/Comments/EntityCollectionTest.php
+++ b/apps/dav/tests/unit/Comments/EntityCollectionTest.php
@@ -1,67 +1,39 @@
<?php
+
+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 Morris Jobke <hey@morrisjobke.de>
- * @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\Tests\unit\Comments;
+use OCA\DAV\Comments\CommentNode;
use OCA\DAV\Comments\EntityCollection;
use OCP\Comments\IComment;
use OCP\Comments\ICommentsManager;
-use OCP\ILogger;
+use OCP\Comments\NotFoundException;
use OCP\IUserManager;
use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
class EntityCollectionTest extends \Test\TestCase {
-
- /** @var \OCP\Comments\ICommentsManager|\PHPUnit\Framework\MockObject\MockObject */
- protected $commentsManager;
- /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */
- protected $userManager;
- /** @var ILogger|\PHPUnit\Framework\MockObject\MockObject */
- protected $logger;
- /** @var EntityCollection */
- protected $collection;
- /** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */
- protected $userSession;
+ protected ICommentsManager&MockObject $commentsManager;
+ protected IUserManager&MockObject $userManager;
+ protected LoggerInterface&MockObject $logger;
+ protected IUserSession&MockObject $userSession;
+ protected EntityCollection $collection;
protected function setUp(): void {
parent::setUp();
- $this->commentsManager = $this->getMockBuilder(ICommentsManager::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->userManager = $this->getMockBuilder(IUserManager::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->userSession = $this->getMockBuilder(IUserSession::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->logger = $this->getMockBuilder(ILogger::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->collection = new \OCA\DAV\Comments\EntityCollection(
+ $this->commentsManager = $this->createMock(ICommentsManager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+
+ $this->collection = new EntityCollection(
'19',
'files',
$this->commentsManager,
@@ -71,11 +43,11 @@ class EntityCollectionTest extends \Test\TestCase {
);
}
- public function testGetId() {
+ public function testGetId(): void {
$this->assertSame($this->collection->getId(), '19');
}
- public function testGetChild() {
+ public function testGetChild(): void {
$this->commentsManager->expects($this->once())
->method('get')
->with('55')
@@ -86,22 +58,22 @@ class EntityCollectionTest extends \Test\TestCase {
);
$node = $this->collection->getChild('55');
- $this->assertTrue($node instanceof \OCA\DAV\Comments\CommentNode);
+ $this->assertInstanceOf(CommentNode::class, $node);
}
- public function testGetChildException() {
+ public function testGetChildException(): void {
$this->expectException(\Sabre\DAV\Exception\NotFound::class);
$this->commentsManager->expects($this->once())
->method('get')
->with('55')
- ->will($this->throwException(new \OCP\Comments\NotFoundException()));
+ ->willThrowException(new NotFoundException());
$this->collection->getChild('55');
}
- public function testGetChildren() {
+ public function testGetChildren(): void {
$this->commentsManager->expects($this->once())
->method('getForObject')
->with('files', '19')
@@ -113,11 +85,11 @@ class EntityCollectionTest extends \Test\TestCase {
$result = $this->collection->getChildren();
- $this->assertSame(count($result), 1);
- $this->assertTrue($result[0] instanceof \OCA\DAV\Comments\CommentNode);
+ $this->assertCount(1, $result);
+ $this->assertInstanceOf(CommentNode::class, $result[0]);
}
- public function testFindChildren() {
+ public function testFindChildren(): void {
$dt = new \DateTime('2016-01-10 18:48:00');
$this->commentsManager->expects($this->once())
->method('getForObject')
@@ -130,19 +102,19 @@ class EntityCollectionTest extends \Test\TestCase {
$result = $this->collection->findChildren(5, 15, $dt);
- $this->assertSame(count($result), 1);
- $this->assertTrue($result[0] instanceof \OCA\DAV\Comments\CommentNode);
+ $this->assertCount(1, $result);
+ $this->assertInstanceOf(CommentNode::class, $result[0]);
}
- public function testChildExistsTrue() {
+ public function testChildExistsTrue(): void {
$this->assertTrue($this->collection->childExists('44'));
}
- public function testChildExistsFalse() {
+ public function testChildExistsFalse(): void {
$this->commentsManager->expects($this->once())
->method('get')
->with('44')
- ->will($this->throwException(new \OCP\Comments\NotFoundException()));
+ ->willThrowException(new NotFoundException());
$this->assertFalse($this->collection->childExists('44'));
}
diff --git a/apps/dav/tests/unit/Comments/EntityTypeCollectionTest.php b/apps/dav/tests/unit/Comments/EntityTypeCollectionTest.php
index f49b2c39857..e5178a3e786 100644
--- a/apps/dav/tests/unit/Comments/EntityTypeCollectionTest.php
+++ b/apps/dav/tests/unit/Comments/EntityTypeCollectionTest.php
@@ -1,105 +1,75 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @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\Tests\unit\Comments;
use OCA\DAV\Comments\EntityCollection as EntityCollectionImplemantation;
+use OCA\DAV\Comments\EntityTypeCollection;
use OCP\Comments\ICommentsManager;
-use OCP\ILogger;
use OCP\IUserManager;
use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
class EntityTypeCollectionTest extends \Test\TestCase {
-
- /** @var ICommentsManager|\PHPUnit\Framework\MockObject\MockObject */
- protected $commentsManager;
- /** @var \OCP\IUserManager|\PHPUnit\Framework\MockObject\MockObject */
- protected $userManager;
- /** @var \OCP\ILogger|\PHPUnit\Framework\MockObject\MockObject */
- protected $logger;
- /** @var \OCA\DAV\Comments\EntityTypeCollection */
- protected $collection;
- /** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */
- protected $userSession;
+ protected ICommentsManager&MockObject $commentsManager;
+ protected IUserManager&MockObject $userManager;
+ protected LoggerInterface&MockObject $logger;
+ protected IUserSession&MockObject $userSession;
+ protected EntityTypeCollection $collection;
protected $childMap = [];
protected function setUp(): void {
parent::setUp();
- $this->commentsManager = $this->getMockBuilder(ICommentsManager::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->userManager = $this->getMockBuilder(IUserManager::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->userSession = $this->getMockBuilder(IUserSession::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->logger = $this->getMockBuilder(ILogger::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $instance = $this;
-
- $this->collection = new \OCA\DAV\Comments\EntityTypeCollection(
+ $this->commentsManager = $this->createMock(ICommentsManager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+
+ $this->collection = new EntityTypeCollection(
'files',
$this->commentsManager,
$this->userManager,
$this->userSession,
$this->logger,
- function ($child) use ($instance) {
- return !empty($instance->childMap[$child]);
+ function ($child) {
+ return !empty($this->childMap[$child]);
}
);
}
- public function testChildExistsYes() {
+ public function testChildExistsYes(): void {
$this->childMap[17] = true;
$this->assertTrue($this->collection->childExists('17'));
}
- public function testChildExistsNo() {
+ public function testChildExistsNo(): void {
$this->assertFalse($this->collection->childExists('17'));
}
- public function testGetChild() {
+ public function testGetChild(): void {
$this->childMap[17] = true;
$ec = $this->collection->getChild('17');
- $this->assertTrue($ec instanceof EntityCollectionImplemantation);
+ $this->assertInstanceOf(EntityCollectionImplemantation::class, $ec);
}
- public function testGetChildException() {
+ public function testGetChildException(): void {
$this->expectException(\Sabre\DAV\Exception\NotFound::class);
$this->collection->getChild('17');
}
- public function testGetChildren() {
+ public function testGetChildren(): void {
$this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class);
$this->collection->getChildren();
diff --git a/apps/dav/tests/unit/Comments/RootCollectionTest.php b/apps/dav/tests/unit/Comments/RootCollectionTest.php
index 8537eb9ab17..9a05d996c8c 100644
--- a/apps/dav/tests/unit/Comments/RootCollectionTest.php
+++ b/apps/dav/tests/unit/Comments/RootCollectionTest.php
@@ -1,97 +1,59 @@
<?php
+
+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 Morris Jobke <hey@morrisjobke.de>
- * @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\Tests\unit\Comments;
use OC\EventDispatcher\EventDispatcher;
-use OC\EventDispatcher\SymfonyAdapter;
use OCA\DAV\Comments\EntityTypeCollection as EntityTypeCollectionImplementation;
+use OCA\DAV\Comments\RootCollection;
use OCP\Comments\CommentsEntityEvent;
use OCP\Comments\ICommentsManager;
-use OCP\ILogger;
+use OCP\EventDispatcher\IEventDispatcher;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class RootCollectionTest extends \Test\TestCase {
-
- /** @var \OCP\Comments\ICommentsManager|\PHPUnit\Framework\MockObject\MockObject */
- protected $commentsManager;
- /** @var \OCP\IUserManager|\PHPUnit\Framework\MockObject\MockObject */
- protected $userManager;
- /** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */
- protected $logger;
- /** @var \OCA\DAV\Comments\RootCollection */
- protected $collection;
- /** @var \OCP\IUserSession|\PHPUnit\Framework\MockObject\MockObject */
- protected $userSession;
- /** @var EventDispatcherInterface */
- protected $dispatcher;
- /** @var \OCP\IUser|\PHPUnit\Framework\MockObject\MockObject */
- protected $user;
+ protected ICommentsManager&MockObject $commentsManager;
+ protected IUserManager&MockObject $userManager;
+ protected LoggerInterface&MockObject $logger;
+ protected IUserSession&MockObject $userSession;
+ protected IEventDispatcher $dispatcher;
+ protected IUser&MockObject $user;
+ protected RootCollection $collection;
protected function setUp(): void {
parent::setUp();
- $this->user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->commentsManager = $this->getMockBuilder(ICommentsManager::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->userManager = $this->getMockBuilder(IUserManager::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->userSession = $this->getMockBuilder(IUserSession::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->logger = $this->getMockBuilder(LoggerInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->dispatcher = new SymfonyAdapter(
- new EventDispatcher(
- new \Symfony\Component\EventDispatcher\EventDispatcher(),
- \OC::$server,
- $this->logger
- ),
+ $this->user = $this->createMock(IUser::class);
+
+ $this->commentsManager = $this->createMock(ICommentsManager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->dispatcher = new EventDispatcher(
+ new \Symfony\Component\EventDispatcher\EventDispatcher(),
+ \OC::$server,
$this->logger
);
- $this->collection = new \OCA\DAV\Comments\RootCollection(
+ $this->collection = new RootCollection(
$this->commentsManager,
$this->userManager,
$this->userSession,
$this->dispatcher,
- $this->createMock(ILogger::class)
+ $this->logger
);
}
- protected function prepareForInitCollections() {
+ protected function prepareForInitCollections(): void {
$this->user->expects($this->any())
->method('getUID')
->willReturn('alice');
@@ -100,7 +62,7 @@ class RootCollectionTest extends \Test\TestCase {
->method('getUser')
->willReturn($this->user);
- $this->dispatcher->addListener(CommentsEntityEvent::EVENT_ENTITY, function (CommentsEntityEvent $event) {
+ $this->dispatcher->addListener(CommentsEntityEvent::class, function (CommentsEntityEvent $event): void {
$event->addEntityCollection('files', function () {
return true;
});
@@ -108,27 +70,27 @@ class RootCollectionTest extends \Test\TestCase {
}
- public function testCreateFile() {
+ public function testCreateFile(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$this->collection->createFile('foo');
}
- public function testCreateDirectory() {
+ public function testCreateDirectory(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$this->collection->createDirectory('foo');
}
- public function testGetChild() {
+ public function testGetChild(): void {
$this->prepareForInitCollections();
$etc = $this->collection->getChild('files');
- $this->assertTrue($etc instanceof EntityTypeCollectionImplementation);
+ $this->assertInstanceOf(EntityTypeCollectionImplementation::class, $etc);
}
- public function testGetChildInvalid() {
+ public function testGetChildInvalid(): void {
$this->expectException(\Sabre\DAV\Exception\NotFound::class);
$this->prepareForInitCollections();
@@ -136,64 +98,64 @@ class RootCollectionTest extends \Test\TestCase {
}
- public function testGetChildNoAuth() {
+ public function testGetChildNoAuth(): void {
$this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class);
$this->collection->getChild('files');
}
- public function testGetChildren() {
+ public function testGetChildren(): void {
$this->prepareForInitCollections();
$children = $this->collection->getChildren();
$this->assertFalse(empty($children));
foreach ($children as $child) {
- $this->assertTrue($child instanceof EntityTypeCollectionImplementation);
+ $this->assertInstanceOf(EntityTypeCollectionImplementation::class, $child);
}
}
- public function testGetChildrenNoAuth() {
+ public function testGetChildrenNoAuth(): void {
$this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class);
$this->collection->getChildren();
}
- public function testChildExistsYes() {
+ public function testChildExistsYes(): void {
$this->prepareForInitCollections();
$this->assertTrue($this->collection->childExists('files'));
}
- public function testChildExistsNo() {
+ public function testChildExistsNo(): void {
$this->prepareForInitCollections();
$this->assertFalse($this->collection->childExists('robots'));
}
- public function testChildExistsNoAuth() {
+ public function testChildExistsNoAuth(): void {
$this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class);
$this->collection->childExists('files');
}
- public function testDelete() {
+ public function testDelete(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$this->collection->delete();
}
- public function testGetName() {
+ public function testGetName(): void {
$this->assertSame('comments', $this->collection->getName());
}
- public function testSetName() {
+ public function testSetName(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$this->collection->setName('foobar');
}
- public function testGetLastModified() {
+ public function testGetLastModified(): void {
$this->assertSame(null, $this->collection->getLastModified());
}
}
diff --git a/apps/dav/tests/unit/Connector/PublicAuthTest.php b/apps/dav/tests/unit/Connector/LegacyPublicAuthTest.php
index 89068c0e6ef..8b8c775c8ec 100644
--- a/apps/dav/tests/unit/Connector/PublicAuthTest.php
+++ b/apps/dav/tests/unit/Connector/LegacyPublicAuthTest.php
@@ -1,78 +1,46 @@
<?php
+
+declare(strict_types=1);
/**
- * @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 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\Tests\unit\Connector;
-use OC\Security\Bruteforce\Throttler;
+use OCA\DAV\Connector\LegacyPublicAuth;
use OCP\IRequest;
use OCP\ISession;
+use OCP\Security\Bruteforce\IThrottler;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
use OCP\Share\IShare;
+use PHPUnit\Framework\MockObject\MockObject;
/**
- * Class PublicAuthTest
+ * Class LegacyPublicAuthTest
*
* @group DB
*
* @package OCA\DAV\Tests\unit\Connector
*/
-class PublicAuthTest extends \Test\TestCase {
-
- /** @var ISession|\PHPUnit\Framework\MockObject\MockObject */
- private $session;
- /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
- private $request;
- /** @var IManager|\PHPUnit\Framework\MockObject\MockObject */
- private $shareManager;
- /** @var \OCA\DAV\Connector\PublicAuth */
- private $auth;
- /** @var Throttler|\PHPUnit\Framework\MockObject\MockObject */
- private $throttler;
-
- /** @var string */
- private $oldUser;
+class LegacyPublicAuthTest extends \Test\TestCase {
+ private ISession&MockObject $session;
+ private IRequest&MockObject $request;
+ private IManager&MockObject $shareManager;
+ private IThrottler&MockObject $throttler;
+ private LegacyPublicAuth $auth;
+ private string|false $oldUser;
protected function setUp(): void {
parent::setUp();
- $this->session = $this->getMockBuilder(ISession::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->request = $this->getMockBuilder(IRequest::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->shareManager = $this->getMockBuilder(IManager::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->throttler = $this->getMockBuilder(Throttler::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->auth = new \OCA\DAV\Connector\PublicAuth(
+ $this->session = $this->createMock(ISession::class);
+ $this->request = $this->createMock(IRequest::class);
+ $this->shareManager = $this->createMock(IManager::class);
+ $this->throttler = $this->createMock(IThrottler::class);
+
+ $this->auth = new LegacyPublicAuth(
$this->request,
$this->shareManager,
$this->session,
@@ -88,12 +56,14 @@ class PublicAuthTest extends \Test\TestCase {
// Set old user
\OC_User::setUserId($this->oldUser);
- \OC_Util::setupFS($this->oldUser);
+ if ($this->oldUser !== false) {
+ \OC_Util::setupFS($this->oldUser);
+ }
parent::tearDown();
}
- public function testNoShare() {
+ public function testNoShare(): void {
$this->shareManager->expects($this->once())
->method('getShareByToken')
->willThrowException(new ShareNotFound());
@@ -103,10 +73,8 @@ class PublicAuthTest extends \Test\TestCase {
$this->assertFalse($result);
}
- public function testShareNoPassword() {
- $share = $this->getMockBuilder(IShare::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testShareNoPassword(): void {
+ $share = $this->createMock(IShare::class);
$share->method('getPassword')->willReturn(null);
$this->shareManager->expects($this->once())
@@ -118,10 +86,8 @@ class PublicAuthTest extends \Test\TestCase {
$this->assertTrue($result);
}
- public function testSharePasswordFancyShareType() {
- $share = $this->getMockBuilder(IShare::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testSharePasswordFancyShareType(): void {
+ $share = $this->createMock(IShare::class);
$share->method('getPassword')->willReturn('password');
$share->method('getShareType')->willReturn(42);
@@ -135,10 +101,8 @@ class PublicAuthTest extends \Test\TestCase {
}
- public function testSharePasswordRemote() {
- $share = $this->getMockBuilder(IShare::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testSharePasswordRemote(): void {
+ $share = $this->createMock(IShare::class);
$share->method('getPassword')->willReturn('password');
$share->method('getShareType')->willReturn(IShare::TYPE_REMOTE);
@@ -151,10 +115,8 @@ class PublicAuthTest extends \Test\TestCase {
$this->assertTrue($result);
}
- public function testSharePasswordLinkValidPassword() {
- $share = $this->getMockBuilder(IShare::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testSharePasswordLinkValidPassword(): void {
+ $share = $this->createMock(IShare::class);
$share->method('getPassword')->willReturn('password');
$share->method('getShareType')->willReturn(IShare::TYPE_LINK);
@@ -164,19 +126,17 @@ class PublicAuthTest extends \Test\TestCase {
$this->shareManager->expects($this->once())
->method('checkPassword')->with(
- $this->equalTo($share),
- $this->equalTo('password')
- )->willReturn(true);
+ $this->equalTo($share),
+ $this->equalTo('password')
+ )->willReturn(true);
$result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
$this->assertTrue($result);
}
- public function testSharePasswordMailValidPassword() {
- $share = $this->getMockBuilder(IShare::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testSharePasswordMailValidPassword(): void {
+ $share = $this->createMock(IShare::class);
$share->method('getPassword')->willReturn('password');
$share->method('getShareType')->willReturn(IShare::TYPE_EMAIL);
@@ -195,10 +155,8 @@ class PublicAuthTest extends \Test\TestCase {
$this->assertTrue($result);
}
- public function testSharePasswordLinkValidSession() {
- $share = $this->getMockBuilder(IShare::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testInvalidSharePasswordLinkValidSession(): void {
+ $share = $this->createMock(IShare::class);
$share->method('getPassword')->willReturn('password');
$share->method('getShareType')->willReturn(IShare::TYPE_LINK);
$share->method('getId')->willReturn('42');
@@ -221,10 +179,8 @@ class PublicAuthTest extends \Test\TestCase {
$this->assertTrue($result);
}
- public function testSharePasswordLinkInvalidSession() {
- $share = $this->getMockBuilder(IShare::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testSharePasswordLinkInvalidSession(): void {
+ $share = $this->createMock(IShare::class);
$share->method('getPassword')->willReturn('password');
$share->method('getShareType')->willReturn(IShare::TYPE_LINK);
$share->method('getId')->willReturn('42');
@@ -248,10 +204,8 @@ class PublicAuthTest extends \Test\TestCase {
}
- public function testSharePasswordMailInvalidSession() {
- $share = $this->getMockBuilder(IShare::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testSharePasswordMailInvalidSession(): void {
+ $share = $this->createMock(IShare::class);
$share->method('getPassword')->willReturn('password');
$share->method('getShareType')->willReturn(IShare::TYPE_EMAIL);
$share->method('getId')->willReturn('42');
diff --git a/apps/dav/tests/unit/Connector/Sabre/AuthTest.php b/apps/dav/tests/unit/Connector/Sabre/AuthTest.php
index 9355b34d66a..4b42a815708 100644
--- a/apps/dav/tests/unit/Connector/Sabre/AuthTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/AuthTest.php
@@ -1,40 +1,23 @@
<?php
+
+declare(strict_types=1);
/**
- * @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 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>
- * @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\Tests\unit\Connector\Sabre;
+use OC\Authentication\Exceptions\PasswordLoginForbiddenException;
use OC\Authentication\TwoFactorAuth\Manager;
-use OC\Security\Bruteforce\Throttler;
use OC\User\Session;
+use OCA\DAV\Connector\Sabre\Auth;
+use OCA\DAV\Connector\Sabre\Exception\PasswordLoginForbidden;
use OCP\IRequest;
use OCP\ISession;
use OCP\IUser;
+use OCP\Security\Bruteforce\IThrottler;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\Server;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
@@ -47,34 +30,21 @@ use Test\TestCase;
* @group DB
*/
class AuthTest extends TestCase {
- /** @var ISession */
- private $session;
- /** @var \OCA\DAV\Connector\Sabre\Auth */
- private $auth;
- /** @var Session */
- private $userSession;
- /** @var IRequest */
- private $request;
- /** @var Manager */
- private $twoFactorManager;
- /** @var Throttler */
- private $throttler;
+ private ISession&MockObject $session;
+ private Session&MockObject $userSession;
+ private IRequest&MockObject $request;
+ private Manager&MockObject $twoFactorManager;
+ private IThrottler&MockObject $throttler;
+ private Auth $auth;
protected function setUp(): void {
parent::setUp();
- $this->session = $this->getMockBuilder(ISession::class)
- ->disableOriginalConstructor()->getMock();
- $this->userSession = $this->getMockBuilder(Session::class)
- ->disableOriginalConstructor()->getMock();
- $this->request = $this->getMockBuilder(IRequest::class)
- ->disableOriginalConstructor()->getMock();
- $this->twoFactorManager = $this->getMockBuilder(Manager::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->throttler = $this->getMockBuilder(Throttler::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->auth = new \OCA\DAV\Connector\Sabre\Auth(
+ $this->session = $this->createMock(ISession::class);
+ $this->userSession = $this->createMock(Session::class);
+ $this->request = $this->createMock(IRequest::class);
+ $this->twoFactorManager = $this->createMock(Manager::class);
+ $this->throttler = $this->createMock(IThrottler::class);
+ $this->auth = new Auth(
$this->session,
$this->userSession,
$this->request,
@@ -83,41 +53,39 @@ class AuthTest extends TestCase {
);
}
- public function testIsDavAuthenticatedWithoutDavSession() {
+ public function testIsDavAuthenticatedWithoutDavSession(): void {
$this->session
->expects($this->once())
->method('get')
->with('AUTHENTICATED_TO_DAV_BACKEND')
->willReturn(null);
- $this->assertFalse($this->invokePrivate($this->auth, 'isDavAuthenticated', ['MyTestUser']));
+ $this->assertFalse(self::invokePrivate($this->auth, 'isDavAuthenticated', ['MyTestUser']));
}
- public function testIsDavAuthenticatedWithWrongDavSession() {
+ public function testIsDavAuthenticatedWithWrongDavSession(): void {
$this->session
->expects($this->exactly(2))
->method('get')
->with('AUTHENTICATED_TO_DAV_BACKEND')
->willReturn('AnotherUser');
- $this->assertFalse($this->invokePrivate($this->auth, 'isDavAuthenticated', ['MyTestUser']));
+ $this->assertFalse(self::invokePrivate($this->auth, 'isDavAuthenticated', ['MyTestUser']));
}
- public function testIsDavAuthenticatedWithCorrectDavSession() {
+ public function testIsDavAuthenticatedWithCorrectDavSession(): void {
$this->session
->expects($this->exactly(2))
->method('get')
->with('AUTHENTICATED_TO_DAV_BACKEND')
->willReturn('MyTestUser');
- $this->assertTrue($this->invokePrivate($this->auth, 'isDavAuthenticated', ['MyTestUser']));
+ $this->assertTrue(self::invokePrivate($this->auth, 'isDavAuthenticated', ['MyTestUser']));
}
- public function testValidateUserPassOfAlreadyDAVAuthenticatedUser() {
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
- $user->expects($this->exactly(2))
+ public function testValidateUserPassOfAlreadyDAVAuthenticatedUser(): void {
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->exactly(1))
->method('getUID')
->willReturn('MyTestUser');
$this->userSession
@@ -125,7 +93,7 @@ class AuthTest extends TestCase {
->method('isLoggedIn')
->willReturn(true);
$this->userSession
- ->expects($this->exactly(2))
+ ->expects($this->exactly(1))
->method('getUser')
->willReturn($user);
$this->session
@@ -137,13 +105,11 @@ class AuthTest extends TestCase {
->expects($this->once())
->method('close');
- $this->assertTrue($this->invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword']));
+ $this->assertTrue(self::invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword']));
}
- public function testValidateUserPassOfInvalidDAVAuthenticatedUser() {
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testValidateUserPassOfInvalidDAVAuthenticatedUser(): void {
+ $user = $this->createMock(IUser::class);
$user->expects($this->once())
->method('getUID')
->willReturn('MyTestUser');
@@ -164,14 +130,12 @@ class AuthTest extends TestCase {
->expects($this->once())
->method('close');
- $this->assertFalse($this->invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword']));
+ $this->assertFalse(self::invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword']));
}
- public function testValidateUserPassOfInvalidDAVAuthenticatedUserWithValidPassword() {
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
- $user->expects($this->exactly(3))
+ public function testValidateUserPassOfInvalidDAVAuthenticatedUserWithValidPassword(): void {
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->exactly(2))
->method('getUID')
->willReturn('MyTestUser');
$this->userSession
@@ -179,7 +143,7 @@ class AuthTest extends TestCase {
->method('isLoggedIn')
->willReturn(true);
$this->userSession
- ->expects($this->exactly(3))
+ ->expects($this->exactly(2))
->method('getUser')
->willReturn($user);
$this->session
@@ -200,10 +164,10 @@ class AuthTest extends TestCase {
->expects($this->once())
->method('close');
- $this->assertTrue($this->invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword']));
+ $this->assertTrue(self::invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword']));
}
- public function testValidateUserPassWithInvalidPassword() {
+ public function testValidateUserPassWithInvalidPassword(): void {
$this->userSession
->expects($this->once())
->method('isLoggedIn')
@@ -217,12 +181,12 @@ class AuthTest extends TestCase {
->expects($this->once())
->method('close');
- $this->assertFalse($this->invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword']));
+ $this->assertFalse(self::invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword']));
}
-
- public function testValidateUserPassWithPasswordLoginForbidden() {
- $this->expectException(\OCA\DAV\Connector\Sabre\Exception\PasswordLoginForbidden::class);
+
+ public function testValidateUserPassWithPasswordLoginForbidden(): void {
+ $this->expectException(PasswordLoginForbidden::class);
$this->userSession
->expects($this->once())
@@ -232,21 +196,17 @@ class AuthTest extends TestCase {
->expects($this->once())
->method('logClientIn')
->with('MyTestUser', 'MyTestPassword')
- ->will($this->throwException(new \OC\Authentication\Exceptions\PasswordLoginForbiddenException()));
+ ->willThrowException(new PasswordLoginForbiddenException());
$this->session
->expects($this->once())
->method('close');
- $this->invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword']);
+ self::invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword']);
}
- public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenForNonGet() {
- $request = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenForNonGet(): void {
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
$this->userSession
->expects($this->any())
->method('isLoggedIn')
@@ -260,9 +220,7 @@ class AuthTest extends TestCase {
->method('get')
->with('AUTHENTICATED_TO_DAV_BACKEND')
->willReturn(null);
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $user = $this->createMock(IUser::class);
$user->expects($this->any())
->method('getUID')
->willReturn('MyWrongDavUser');
@@ -283,13 +241,9 @@ class AuthTest extends TestCase {
$this->assertSame($expectedResponse, $response);
}
- public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenAndCorrectlyDavAuthenticated() {
- $request = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenAndCorrectlyDavAuthenticated(): void {
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
$this->userSession
->expects($this->any())
->method('isLoggedIn')
@@ -301,20 +255,13 @@ class AuthTest extends TestCase {
$this->request
->expects($this->any())
->method('isUserAgent')
- ->with([
- '/^Mozilla\/5\.0 \([A-Za-z ]+\) (mirall|csyncoC)\/.*$/',
- '/^Mozilla\/5\.0 \(Android\) (ownCloud|Nextcloud)\-android.*$/',
- '/^Mozilla\/5\.0 \(iOS\) (ownCloud|Nextcloud)\-iOS.*$/',
- ])
->willReturn(false);
$this->session
->expects($this->any())
->method('get')
->with('AUTHENTICATED_TO_DAV_BACKEND')
->willReturn('LoggedInUser');
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $user = $this->createMock(IUser::class);
$user->expects($this->any())
->method('getUID')
->willReturn('LoggedInUser');
@@ -329,17 +276,13 @@ class AuthTest extends TestCase {
$this->auth->check($request, $response);
}
-
- public function testAuthenticateAlreadyLoggedInWithoutTwoFactorChallengePassed() {
+
+ public function testAuthenticateAlreadyLoggedInWithoutTwoFactorChallengePassed(): void {
$this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class);
$this->expectExceptionMessage('2FA challenge not passed.');
- $request = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
$this->userSession
->expects($this->any())
->method('isLoggedIn')
@@ -351,20 +294,13 @@ class AuthTest extends TestCase {
$this->request
->expects($this->any())
->method('isUserAgent')
- ->with([
- '/^Mozilla\/5\.0 \([A-Za-z ]+\) (mirall|csyncoC)\/.*$/',
- '/^Mozilla\/5\.0 \(Android\) ownCloud\-android.*$/',
- '/^Mozilla\/5\.0 \(iOS\) (ownCloud|Nextcloud)\-iOS.*$/',
- ])
->willReturn(false);
$this->session
->expects($this->any())
->method('get')
->with('AUTHENTICATED_TO_DAV_BACKEND')
->willReturn('LoggedInUser');
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $user = $this->createMock(IUser::class);
$user->expects($this->any())
->method('getUID')
->willReturn('LoggedInUser');
@@ -383,17 +319,13 @@ class AuthTest extends TestCase {
$this->auth->check($request, $response);
}
-
- public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenAndIncorrectlyDavAuthenticated() {
+
+ public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenAndIncorrectlyDavAuthenticated(): void {
$this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class);
$this->expectExceptionMessage('CSRF check not passed.');
- $request = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
$this->userSession
->expects($this->any())
->method('isLoggedIn')
@@ -405,20 +337,13 @@ class AuthTest extends TestCase {
$this->request
->expects($this->any())
->method('isUserAgent')
- ->with([
- '/^Mozilla\/5\.0 \([A-Za-z ]+\) (mirall|csyncoC)\/.*$/',
- '/^Mozilla\/5\.0 \(Android\) (ownCloud|Nextcloud)\-android.*$/',
- '/^Mozilla\/5\.0 \(iOS\) (ownCloud|Nextcloud)\-iOS.*$/',
- ])
->willReturn(false);
$this->session
->expects($this->any())
->method('get')
->with('AUTHENTICATED_TO_DAV_BACKEND')
->willReturn('AnotherUser');
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $user = $this->createMock(IUser::class);
$user->expects($this->any())
->method('getUID')
->willReturn('LoggedInUser');
@@ -433,13 +358,9 @@ class AuthTest extends TestCase {
$this->auth->check($request, $response);
}
- public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenForNonGetAndDesktopClient() {
- $request = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenForNonGetAndDesktopClient(): void {
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
$this->userSession
->expects($this->any())
->method('isLoggedIn')
@@ -451,20 +372,13 @@ class AuthTest extends TestCase {
$this->request
->expects($this->any())
->method('isUserAgent')
- ->with([
- '/^Mozilla\/5\.0 \([A-Za-z ]+\) (mirall|csyncoC)\/.*$/',
- '/^Mozilla\/5\.0 \(Android\) (ownCloud|Nextcloud)\-android.*$/',
- '/^Mozilla\/5\.0 \(iOS\) (ownCloud|Nextcloud)\-iOS.*$/',
- ])
->willReturn(true);
$this->session
->expects($this->any())
->method('get')
->with('AUTHENTICATED_TO_DAV_BACKEND')
->willReturn(null);
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $user = $this->createMock(IUser::class);
$user->expects($this->any())
->method('getUID')
->willReturn('MyWrongDavUser');
@@ -480,13 +394,9 @@ class AuthTest extends TestCase {
$this->auth->check($request, $response);
}
- public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenForGet() {
- $request = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenForGet(): void {
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
$this->userSession
->expects($this->any())
->method('isLoggedIn')
@@ -496,9 +406,7 @@ class AuthTest extends TestCase {
->method('get')
->with('AUTHENTICATED_TO_DAV_BACKEND')
->willReturn(null);
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $user = $this->createMock(IUser::class);
$user->expects($this->any())
->method('getUID')
->willReturn('MyWrongDavUser');
@@ -515,13 +423,9 @@ class AuthTest extends TestCase {
$this->assertEquals([true, 'principals/users/MyWrongDavUser'], $response);
}
- public function testAuthenticateAlreadyLoggedInWithCsrfTokenForGet() {
- $request = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testAuthenticateAlreadyLoggedInWithCsrfTokenForGet(): void {
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
$this->userSession
->expects($this->any())
->method('isLoggedIn')
@@ -531,9 +435,7 @@ class AuthTest extends TestCase {
->method('get')
->with('AUTHENTICATED_TO_DAV_BACKEND')
->willReturn(null);
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $user = $this->createMock(IUser::class);
$user->expects($this->any())
->method('getUID')
->willReturn('MyWrongDavUser');
@@ -550,58 +452,84 @@ class AuthTest extends TestCase {
$this->assertEquals([true, 'principals/users/MyWrongDavUser'], $response);
}
- public function testAuthenticateNoBasicAuthenticateHeadersProvided() {
- $server = $this->getMockBuilder(Server::class)
- ->disableOriginalConstructor()
- ->getMock();
- $server->httpRequest = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- $server->httpResponse = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testAuthenticateNoBasicAuthenticateHeadersProvided(): void {
+ $server = $this->createMock(Server::class);
+ $server->httpRequest = $this->createMock(RequestInterface::class);
+ $server->httpResponse = $this->createMock(ResponseInterface::class);
$response = $this->auth->check($server->httpRequest, $server->httpResponse);
$this->assertEquals([false, 'No \'Authorization: Basic\' header found. Either the client didn\'t send one, or the server is misconfigured'], $response);
}
-
- public function testAuthenticateNoBasicAuthenticateHeadersProvidedWithAjax() {
+
+ public function testAuthenticateNoBasicAuthenticateHeadersProvidedWithAjax(): void {
$this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class);
$this->expectExceptionMessage('Cannot authenticate over ajax calls');
- /** @var \Sabre\HTTP\RequestInterface $httpRequest */
- $httpRequest = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- /** @var \Sabre\HTTP\ResponseInterface $httpResponse */
- $httpResponse = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ /** @var \Sabre\HTTP\RequestInterface&MockObject $httpRequest */
+ $httpRequest = $this->createMock(RequestInterface::class);
+ /** @var \Sabre\HTTP\ResponseInterface&MockObject $httpResponse */
+ $httpResponse = $this->createMock(ResponseInterface::class);
$this->userSession
->expects($this->any())
->method('isLoggedIn')
->willReturn(false);
$httpRequest
+ ->expects($this->exactly(2))
+ ->method('getHeader')
+ ->willReturnMap([
+ ['X-Requested-With', 'XMLHttpRequest'],
+ ['Authorization', null],
+ ]);
+
+ $this->auth->check($httpRequest, $httpResponse);
+ }
+
+ public function testAuthenticateWithBasicAuthenticateHeadersProvidedWithAjax(): void {
+ // No CSRF
+ $this->request
->expects($this->once())
+ ->method('passesCSRFCheck')
+ ->willReturn(false);
+
+ /** @var \Sabre\HTTP\RequestInterface&MockObject $httpRequest */
+ $httpRequest = $this->createMock(RequestInterface::class);
+ /** @var \Sabre\HTTP\ResponseInterface&MockObject $httpResponse */
+ $httpResponse = $this->createMock(ResponseInterface::class);
+ $httpRequest
+ ->expects($this->any())
->method('getHeader')
- ->with('X-Requested-With')
- ->willReturn('XMLHttpRequest');
+ ->willReturnMap([
+ ['X-Requested-With', 'XMLHttpRequest'],
+ ['Authorization', 'basic dXNlcm5hbWU6cGFzc3dvcmQ='],
+ ]);
+
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->any())
+ ->method('getUID')
+ ->willReturn('MyDavUser');
+ $this->userSession
+ ->expects($this->any())
+ ->method('isLoggedIn')
+ ->willReturn(false);
+ $this->userSession
+ ->expects($this->once())
+ ->method('logClientIn')
+ ->with('username', 'password')
+ ->willReturn(true);
+ $this->userSession
+ ->expects($this->any())
+ ->method('getUser')
+ ->willReturn($user);
+
$this->auth->check($httpRequest, $httpResponse);
}
- public function testAuthenticateNoBasicAuthenticateHeadersProvidedWithAjaxButUserIsStillLoggedIn() {
+ public function testAuthenticateNoBasicAuthenticateHeadersProvidedWithAjaxButUserIsStillLoggedIn(): void {
/** @var \Sabre\HTTP\RequestInterface $httpRequest */
- $httpRequest = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $httpRequest = $this->createMock(RequestInterface::class);
/** @var \Sabre\HTTP\ResponseInterface $httpResponse */
- $httpResponse = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- /** @var IUser */
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $httpResponse = $this->createMock(ResponseInterface::class);
+ $user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('MyTestUser');
$this->userSession
->expects($this->any())
@@ -631,65 +559,44 @@ class AuthTest extends TestCase {
);
}
- public function testAuthenticateValidCredentials() {
- $server = $this->getMockBuilder(Server::class)
- ->disableOriginalConstructor()
- ->getMock();
- $server->httpRequest = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- $server->httpRequest
- ->expects($this->at(0))
- ->method('getHeader')
- ->with('X-Requested-With')
- ->willReturn(null);
+ public function testAuthenticateValidCredentials(): void {
+ $server = $this->createMock(Server::class);
+ $server->httpRequest = $this->createMock(RequestInterface::class);
$server->httpRequest
- ->expects($this->at(1))
+ ->expects($this->once())
->method('getHeader')
->with('Authorization')
->willReturn('basic dXNlcm5hbWU6cGFzc3dvcmQ=');
- $server->httpResponse = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+
+ $server->httpResponse = $this->createMock(ResponseInterface::class);
$this->userSession
->expects($this->once())
->method('logClientIn')
->with('username', 'password')
->willReturn(true);
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
- $user->expects($this->exactly(3))
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->exactly(2))
->method('getUID')
->willReturn('MyTestUser');
$this->userSession
- ->expects($this->exactly(4))
+ ->expects($this->exactly(3))
->method('getUser')
->willReturn($user);
$response = $this->auth->check($server->httpRequest, $server->httpResponse);
$this->assertEquals([true, 'principals/users/MyTestUser'], $response);
}
- public function testAuthenticateInvalidCredentials() {
- $server = $this->getMockBuilder(Server::class)
- ->disableOriginalConstructor()
- ->getMock();
- $server->httpRequest = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- $server->httpRequest
- ->expects($this->at(0))
- ->method('getHeader')
- ->with('X-Requested-With')
- ->willReturn(null);
+ public function testAuthenticateInvalidCredentials(): void {
+ $server = $this->createMock(Server::class);
+ $server->httpRequest = $this->createMock(RequestInterface::class);
$server->httpRequest
- ->expects($this->at(1))
+ ->expects($this->exactly(2))
->method('getHeader')
- ->with('Authorization')
- ->willReturn('basic dXNlcm5hbWU6cGFzc3dvcmQ=');
- $server->httpResponse = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ ->willReturnMap([
+ ['Authorization', 'basic dXNlcm5hbWU6cGFzc3dvcmQ='],
+ ['X-Requested-With', null],
+ ]);
+ $server->httpResponse = $this->createMock(ResponseInterface::class);
$this->userSession
->expects($this->once())
->method('logClientIn')
diff --git a/apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php b/apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php
index 007b5c90295..1e6267d4cbb 100644
--- a/apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php
@@ -1,34 +1,20 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
- *
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @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\Tests\unit\Connector\Sabre;
+use OC\User\Session;
use OCA\DAV\Connector\Sabre\BearerAuth;
+use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
use OCP\IUser;
use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
use Test\TestCase;
@@ -37,42 +23,41 @@ use Test\TestCase;
* @group DB
*/
class BearerAuthTest extends TestCase {
- /** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */
- private $userSession;
- /** @var ISession|\PHPUnit\Framework\MockObject\MockObject */
- private $session;
- /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
- private $request;
- /** @var BearerAuth */
- private $bearerAuth;
+ private IUserSession&MockObject $userSession;
+ private ISession&MockObject $session;
+ private IRequest&MockObject $request;
+ private BearerAuth $bearerAuth;
+
+ private IConfig&MockObject $config;
protected function setUp(): void {
parent::setUp();
- $this->userSession = $this->createMock(\OC\User\Session::class);
+ $this->userSession = $this->createMock(Session::class);
$this->session = $this->createMock(ISession::class);
$this->request = $this->createMock(IRequest::class);
+ $this->config = $this->createMock(IConfig::class);
$this->bearerAuth = new BearerAuth(
$this->userSession,
$this->session,
- $this->request
+ $this->request,
+ $this->config,
);
}
- public function testValidateBearerTokenNotLoggedIn() {
+ public function testValidateBearerTokenNotLoggedIn(): void {
$this->assertFalse($this->bearerAuth->validateBearerToken('Token'));
}
- public function testValidateBearerToken() {
- $this->userSession
- ->expects($this->at(0))
- ->method('isLoggedIn')
- ->willReturn(false);
+ public function testValidateBearerToken(): void {
$this->userSession
- ->expects($this->at(2))
+ ->expects($this->exactly(2))
->method('isLoggedIn')
- ->willReturn(true);
+ ->willReturnOnConsecutiveCalls(
+ false,
+ true,
+ );
$user = $this->createMock(IUser::class);
$user
->expects($this->once())
@@ -86,10 +71,10 @@ class BearerAuthTest extends TestCase {
$this->assertSame('principals/users/admin', $this->bearerAuth->validateBearerToken('Token'));
}
- public function testChallenge() {
- /** @var \PHPUnit\Framework\MockObject\MockObject|RequestInterface $request */
+ public function testChallenge(): void {
+ /** @var RequestInterface&MockObject $request */
$request = $this->createMock(RequestInterface::class);
- /** @var \PHPUnit\Framework\MockObject\MockObject|ResponseInterface $response */
+ /** @var ResponseInterface&MockObject $response */
$response = $this->createMock(ResponseInterface::class);
$result = $this->bearerAuth->challenge($request, $response);
$this->assertEmpty($result);
diff --git a/apps/dav/tests/unit/Connector/Sabre/BlockLegacyClientPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/BlockLegacyClientPluginTest.php
index 4d2771132cc..366c9475b1b 100644
--- a/apps/dav/tests/unit/Connector/Sabre/BlockLegacyClientPluginTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/BlockLegacyClientPluginTest.php
@@ -1,77 +1,124 @@
<?php
+
+declare(strict_types=1);
+
/**
- * @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 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\Tests\unit\Connector\Sabre;
use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin;
+use OCA\Theming\ThemingDefaults;
use OCP\IConfig;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\HTTP\RequestInterface;
use Test\TestCase;
+enum ERROR_TYPE {
+ case MIN_ERROR;
+ case MAX_ERROR;
+ case NONE;
+}
+
/**
* Class BlockLegacyClientPluginTest
*
* @package OCA\DAV\Tests\unit\Connector\Sabre
*/
class BlockLegacyClientPluginTest extends TestCase {
- /** @var IConfig | \PHPUnit\Framework\MockObject\MockObject */
- private $config;
- /** @var BlockLegacyClientPlugin */
- private $blockLegacyClientVersionPlugin;
+
+ private IConfig&MockObject $config;
+ private ThemingDefaults&MockObject $themingDefaults;
+ private BlockLegacyClientPlugin $blockLegacyClientVersionPlugin;
protected function setUp(): void {
parent::setUp();
- $this->config = $this->getMockBuilder(IConfig::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->blockLegacyClientVersionPlugin = new BlockLegacyClientPlugin($this->config);
+ $this->config = $this->createMock(IConfig::class);
+ $this->themingDefaults = $this->createMock(ThemingDefaults::class);
+ $this->blockLegacyClientVersionPlugin = new BlockLegacyClientPlugin(
+ $this->config,
+ $this->themingDefaults,
+ );
}
- /**
- * @return array
- */
- public function oldDesktopClientProvider() {
+ public static function oldDesktopClientProvider(): array {
return [
- ['Mozilla/5.0 (1.5.0) mirall/1.5.0'],
- ['mirall/1.5.0'],
- ['mirall/1.5.4'],
- ['mirall/1.6.0'],
- ['Mozilla/5.0 (Bogus Text) mirall/1.6.9'],
+ ['Mozilla/5.0 (Windows) mirall/1.5.0', ERROR_TYPE::MIN_ERROR],
+ ['Mozilla/5.0 (Bogus Text) mirall/1.6.9', ERROR_TYPE::MIN_ERROR],
+ ['Mozilla/5.0 (Windows) mirall/2.5.0', ERROR_TYPE::MAX_ERROR],
+ ['Mozilla/5.0 (Bogus Text) mirall/2.0.1', ERROR_TYPE::MAX_ERROR],
+ ['Mozilla/5.0 (Windows) mirall/2.0.0', ERROR_TYPE::NONE],
+ ['Mozilla/5.0 (Bogus Text) mirall/2.0.0', ERROR_TYPE::NONE],
];
}
+ #[\PHPUnit\Framework\Attributes\DataProvider('oldDesktopClientProvider')]
+ public function testBeforeHandlerException(string $userAgent, ERROR_TYPE $errorType): void {
+ $this->themingDefaults
+ ->expects($this->atMost(1))
+ ->method('getSyncClientUrl')
+ ->willReturn('https://nextcloud.com/install/#install-clients');
+
+ $this->config
+ ->expects($this->exactly(2))
+ ->method('getSystemValueString')
+ ->willReturnCallback(function (string $key) {
+ if ($key === 'minimum.supported.desktop.version') {
+ return '1.7.0';
+ }
+ return '2.0.0';
+ });
+
+ if ($errorType !== ERROR_TYPE::NONE) {
+ $errorString = $errorType === ERROR_TYPE::MIN_ERROR
+ ? 'This version of the client is unsupported. Upgrade to <a href="https://nextcloud.com/install/#install-clients">version 1.7.0 or later</a>.'
+ : 'This version of the client is unsupported. Downgrade to <a href="https://nextcloud.com/install/#install-clients">version 2.0.0 or earlier</a>.';
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $this->expectExceptionMessage($errorString);
+ }
+
+ /** @var RequestInterface|MockObject $request */
+ $request = $this->createMock(RequestInterface::class);
+ $request
+ ->expects($this->once())
+ ->method('getHeader')
+ ->with('User-Agent')
+ ->willReturn($userAgent);
+
+ $this->blockLegacyClientVersionPlugin->beforeHandler($request);
+ }
+
/**
- * @dataProvider oldDesktopClientProvider
- * @param string $userAgent
+ * Ensure that there is no room for XSS attack through configured URL / version
*/
- public function testBeforeHandlerException($userAgent) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('oldDesktopClientProvider')]
+ public function testBeforeHandlerExceptionPreventXSSAttack(string $userAgent, ERROR_TYPE $errorType): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
- $this->expectExceptionMessage('Unsupported client version.');
- /** @var \Sabre\HTTP\RequestInterface | \PHPUnit\Framework\MockObject\MockObject $request */
+ $this->themingDefaults
+ ->expects($this->atMost(1))
+ ->method('getSyncClientUrl')
+ ->willReturn('https://example.com"><script>alter("hacked");</script>');
+
+ $this->config
+ ->expects($this->exactly(2))
+ ->method('getSystemValueString')
+ ->willReturnCallback(function (string $key) {
+ if ($key === 'minimum.supported.desktop.version') {
+ return '1.7.0 <script>alert("unsafe")</script>';
+ }
+ return '2.0.0 <script>alert("unsafe")</script>';
+ });
+
+ $errorString = $errorType === ERROR_TYPE::MIN_ERROR
+ ? 'This version of the client is unsupported. Upgrade to <a href="https://example.com&quot;&gt;&lt;script&gt;alter(&quot;hacked&quot;);&lt;/script&gt;">version 1.7.0 &lt;script&gt;alert(&quot;unsafe&quot;)&lt;/script&gt; or later</a>.'
+ : 'This version of the client is unsupported. Downgrade to <a href="https://example.com&quot;&gt;&lt;script&gt;alter(&quot;hacked&quot;);&lt;/script&gt;">version 2.0.0 &lt;script&gt;alert(&quot;unsafe&quot;)&lt;/script&gt; or earlier</a>.';
+ $this->expectExceptionMessage($errorString);
+
+ /** @var RequestInterface|MockObject $request */
$request = $this->createMock('\Sabre\HTTP\RequestInterface');
$request
->expects($this->once())
@@ -79,35 +126,24 @@ class BlockLegacyClientPluginTest extends TestCase {
->with('User-Agent')
->willReturn($userAgent);
- $this->config
- ->expects($this->once())
- ->method('getSystemValue')
- ->with('minimum.supported.desktop.version', '2.0.0')
- ->willReturn('1.7.0');
-
$this->blockLegacyClientVersionPlugin->beforeHandler($request);
}
- /**
- * @return array
- */
- public function newAndAlternateDesktopClientProvider() {
+ public static function newAndAlternateDesktopClientProvider(): array {
return [
- ['Mozilla/5.0 (1.7.0) mirall/1.7.0'],
- ['mirall/1.8.3'],
- ['mirall/1.7.2'],
- ['mirall/1.7.0'],
+ ['Mozilla/5.0 (Windows) mirall/1.7.0'],
['Mozilla/5.0 (Bogus Text) mirall/1.9.3'],
+ ['Mozilla/5.0 (Not Our Client But Old Version) LegacySync/1.1.0'],
+ ['Mozilla/5.0 (Windows) mirall/4.7.0'],
+ ['Mozilla/5.0 (Bogus Text) mirall/3.9.3'],
+ ['Mozilla/5.0 (Not Our Client But Old Version) LegacySync/45.0.0'],
];
}
- /**
- * @dataProvider newAndAlternateDesktopClientProvider
- * @param string $userAgent
- */
- public function testBeforeHandlerSuccess($userAgent) {
- /** @var \Sabre\HTTP\RequestInterface | \PHPUnit\Framework\MockObject\MockObject $request */
- $request = $this->createMock('\Sabre\HTTP\RequestInterface');
+ #[\PHPUnit\Framework\Attributes\DataProvider('newAndAlternateDesktopClientProvider')]
+ public function testBeforeHandlerSuccess(string $userAgent): void {
+ /** @var RequestInterface|MockObject $request */
+ $request = $this->createMock(RequestInterface::class);
$request
->expects($this->once())
->method('getHeader')
@@ -115,22 +151,27 @@ class BlockLegacyClientPluginTest extends TestCase {
->willReturn($userAgent);
$this->config
- ->expects($this->once())
- ->method('getSystemValue')
- ->with('minimum.supported.desktop.version', '2.0.0')
- ->willReturn('1.7.0');
+ ->expects($this->exactly(2))
+ ->method('getSystemValueString')
+ ->willReturnCallback(function (string $key) {
+ if ($key === 'minimum.supported.desktop.version') {
+ return '1.7.0';
+ }
+ return '10.0.0';
+ });
$this->blockLegacyClientVersionPlugin->beforeHandler($request);
}
- public function testBeforeHandlerNoUserAgent() {
- /** @var \Sabre\HTTP\RequestInterface | \PHPUnit\Framework\MockObject\MockObject $request */
- $request = $this->createMock('\Sabre\HTTP\RequestInterface');
+ public function testBeforeHandlerNoUserAgent(): void {
+ /** @var RequestInterface|MockObject $request */
+ $request = $this->createMock(RequestInterface::class);
$request
->expects($this->once())
->method('getHeader')
->with('User-Agent')
->willReturn(null);
+
$this->blockLegacyClientVersionPlugin->beforeHandler($request);
}
}
diff --git a/apps/dav/tests/unit/Connector/Sabre/CommentsPropertiesPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/CommentsPropertiesPluginTest.php
index ea49cef5d0f..a934d6401c2 100644
--- a/apps/dav/tests/unit/Connector/Sabre/CommentsPropertiesPluginTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/CommentsPropertiesPluginTest.php
@@ -1,87 +1,51 @@
<?php
+
+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 Morris Jobke <hey@morrisjobke.de>
- * @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\Tests\unit\Connector\Sabre;
use OCA\DAV\Connector\Sabre\CommentPropertiesPlugin as CommentPropertiesPluginImplementation;
+use OCA\DAV\Connector\Sabre\Directory;
use OCA\DAV\Connector\Sabre\File;
use OCP\Comments\ICommentsManager;
use OCP\IUser;
use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\PropFind;
+use Sabre\DAV\Server;
class CommentsPropertiesPluginTest extends \Test\TestCase {
-
- /** @var CommentPropertiesPluginImplementation */
- protected $plugin;
- protected $commentsManager;
- protected $userSession;
- protected $server;
+ protected CommentPropertiesPluginImplementation $plugin;
+ protected ICommentsManager&MockObject $commentsManager;
+ protected IUserSession&MockObject $userSession;
+ protected Server&MockObject $server;
protected function setUp(): void {
parent::setUp();
- $this->commentsManager = $this->getMockBuilder(ICommentsManager::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->userSession = $this->getMockBuilder(IUserSession::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->server = $this->getMockBuilder('\Sabre\DAV\Server')
- ->disableOriginalConstructor()
- ->getMock();
+ $this->commentsManager = $this->createMock(ICommentsManager::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->server = $this->createMock(Server::class);
$this->plugin = new CommentPropertiesPluginImplementation($this->commentsManager, $this->userSession);
$this->plugin->initialize($this->server);
}
- public function nodeProvider() {
- $mocks = [];
- foreach (['\OCA\DAV\Connector\Sabre\File', '\OCA\DAV\Connector\Sabre\Directory', '\Sabre\DAV\INode'] as $class) {
- $mocks[] = $this->getMockBuilder($class)
- ->disableOriginalConstructor()
- ->getMock();
- }
-
+ public static function nodeProvider(): array {
return [
- [$mocks[0], true],
- [$mocks[1], true],
- [$mocks[2], false]
+ [File::class, true],
+ [Directory::class, true],
+ [\Sabre\DAV\INode::class, false]
];
}
- /**
- * @dataProvider nodeProvider
- * @param $node
- * @param $expectedSuccessful
- */
- public function testHandleGetProperties($node, $expectedSuccessful) {
- $propFind = $this->getMockBuilder(PropFind::class)
- ->disableOriginalConstructor()
- ->getMock();
+ #[\PHPUnit\Framework\Attributes\DataProvider('nodeProvider')]
+ public function testHandleGetProperties(string $class, bool $expectedSuccessful): void {
+ $propFind = $this->createMock(PropFind::class);
if ($expectedSuccessful) {
$propFind->expects($this->exactly(3))
@@ -91,10 +55,11 @@ class CommentsPropertiesPluginTest extends \Test\TestCase {
->method('handle');
}
+ $node = $this->createMock($class);
$this->plugin->handleGetProperties($propFind, $node);
}
- public function baseUriProvider() {
+ public static function baseUriProvider(): array {
return [
['owncloud/remote.php/webdav/', '4567', 'owncloud/remote.php/dav/comments/files/4567'],
['owncloud/remote.php/files/', '4567', 'owncloud/remote.php/dav/comments/files/4567'],
@@ -102,16 +67,9 @@ class CommentsPropertiesPluginTest extends \Test\TestCase {
];
}
- /**
- * @dataProvider baseUriProvider
- * @param $baseUri
- * @param $fid
- * @param $expectedHref
- */
- public function testGetCommentsLink($baseUri, $fid, $expectedHref) {
- $node = $this->getMockBuilder(File::class)
- ->disableOriginalConstructor()
- ->getMock();
+ #[\PHPUnit\Framework\Attributes\DataProvider('baseUriProvider')]
+ public function testGetCommentsLink(string $baseUri, string $fid, ?string $expectedHref): void {
+ $node = $this->createMock(File::class);
$node->expects($this->any())
->method('getId')
->willReturn($fid);
@@ -124,29 +82,23 @@ class CommentsPropertiesPluginTest extends \Test\TestCase {
$this->assertSame($expectedHref, $href);
}
- public function userProvider() {
+ public static function userProvider(): array {
return [
- [
- $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock()
- ],
+ [IUser::class],
[null]
];
}
- /**
- * @dataProvider userProvider
- * @param $user
- */
- public function testGetUnreadCount($user) {
- $node = $this->getMockBuilder(File::class)
- ->disableOriginalConstructor()
- ->getMock();
+ #[\PHPUnit\Framework\Attributes\DataProvider('userProvider')]
+ public function testGetUnreadCount(?string $user): void {
+ $node = $this->createMock(File::class);
$node->expects($this->any())
->method('getId')
->willReturn('4567');
+ if ($user !== null) {
+ $user = $this->createMock($user);
+ }
$this->userSession->expects($this->once())
->method('getUser')
->willReturn($user);
diff --git a/apps/dav/tests/unit/Connector/Sabre/CopyEtagHeaderPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/CopyEtagHeaderPluginTest.php
index 858e5c8199b..7067cf335ed 100644
--- a/apps/dav/tests/unit/Connector/Sabre/CopyEtagHeaderPluginTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/CopyEtagHeaderPluginTest.php
@@ -1,31 +1,10 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
- * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.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>
- * @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\Tests\unit\Connector\Sabre;
@@ -36,19 +15,9 @@ use Sabre\DAV\Server;
use Sabre\DAV\Tree;
use Test\TestCase;
-/**
- * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
- * This file is licensed under the Affero General Public License version 3 or
- * later.
- * See the COPYING-README file.
- */
class CopyEtagHeaderPluginTest extends TestCase {
-
- /** @var CopyEtagHeaderPlugin */
- private $plugin;
-
- /** @var Server */
- private $server;
+ private CopyEtagHeaderPlugin $plugin;
+ private Server $server;
protected function setUp(): void {
parent::setUp();
@@ -57,7 +26,7 @@ class CopyEtagHeaderPluginTest extends TestCase {
$this->plugin->initialize($this->server);
}
- public function testCopyEtag() {
+ public function testCopyEtag(): void {
$request = new \Sabre\Http\Request('GET', 'dummy.file');
$response = new \Sabre\Http\Response();
$response->setHeader('Etag', 'abcd');
@@ -67,7 +36,7 @@ class CopyEtagHeaderPluginTest extends TestCase {
$this->assertEquals('abcd', $response->getHeader('OC-Etag'));
}
- public function testNoopWhenEmpty() {
+ public function testNoopWhenEmpty(): void {
$request = new \Sabre\Http\Request('GET', 'dummy.file');
$response = new \Sabre\Http\Response();
@@ -89,16 +58,12 @@ class CopyEtagHeaderPluginTest extends TestCase {
// Nothing to assert, we are just testing if the exception is handled
}
- public function testAfterMove() {
- $node = $this->getMockBuilder(File::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testAfterMove(): void {
+ $node = $this->createMock(File::class);
$node->expects($this->once())
->method('getETag')
->willReturn('123456');
- $tree = $this->getMockBuilder(Tree::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $tree = $this->createMock(Tree::class);
$tree->expects($this->once())
->method('getNodeForPath')
->with('test.txt')
diff --git a/apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php b/apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php
index 48658f3ffa3..d4021a66299 100644
--- a/apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php
@@ -1,44 +1,21 @@
<?php
-/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
- * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.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 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/>
- *
- */
-namespace OCA\DAV\Tests\unit\Connector\Sabre;
+declare(strict_types=1);
/**
- * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
- * This file is licensed under the Affero General Public License version 3 or
- * later.
- * See the COPYING-README file.
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
+use OCA\DAV\CalDAV\DefaultCalendarValidator;
use OCA\DAV\Connector\Sabre\Directory;
use OCA\DAV\Connector\Sabre\File;
+use OCA\DAV\DAV\CustomPropertiesBackend;
+use OCP\IDBConnection;
use OCP\IUser;
+use OCP\Server;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\Tree;
/**
@@ -49,55 +26,41 @@ use Sabre\DAV\Tree;
* @package OCA\DAV\Tests\unit\Connector\Sabre
*/
class CustomPropertiesBackendTest extends \Test\TestCase {
-
- /**
- * @var \Sabre\DAV\Server
- */
- private $server;
-
- /**
- * @var \Sabre\DAV\Tree
- */
- private $tree;
-
- /**
- * @var \OCA\DAV\DAV\CustomPropertiesBackend
- */
- private $plugin;
-
- /**
- * @var \OCP\IUser
- */
- private $user;
+ private \Sabre\DAV\Server $server;
+ private \Sabre\DAV\Tree&MockObject $tree;
+ private IUser&MockObject $user;
+ private DefaultCalendarValidator&MockObject $defaultCalendarValidator;
+ private CustomPropertiesBackend $plugin;
protected function setUp(): void {
parent::setUp();
+
$this->server = new \Sabre\DAV\Server();
- $this->tree = $this->getMockBuilder(Tree::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $this->tree = $this->createMock(Tree::class);
- $userId = $this->getUniqueID('testcustompropertiesuser');
+ $userId = self::getUniqueID('testcustompropertiesuser');
- $this->user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $this->user = $this->createMock(IUser::class);
$this->user->expects($this->any())
->method('getUID')
->willReturn($userId);
- $this->plugin = new \OCA\DAV\DAV\CustomPropertiesBackend(
+ $this->defaultCalendarValidator = $this->createMock(DefaultCalendarValidator::class);
+
+ $this->plugin = new CustomPropertiesBackend(
+ $this->server,
$this->tree,
- \OC::$server->getDatabaseConnection(),
- $this->user
+ Server::get(IDBConnection::class),
+ $this->user,
+ $this->defaultCalendarValidator,
);
}
protected function tearDown(): void {
- $connection = \OC::$server->getDatabaseConnection();
+ $connection = Server::get(IDBConnection::class);
$deleteStatement = $connection->prepare(
- 'DELETE FROM `*PREFIX*properties`' .
- ' WHERE `userid` = ?'
+ 'DELETE FROM `*PREFIX*properties`'
+ . ' WHERE `userid` = ?'
);
$deleteStatement->execute(
[
@@ -105,12 +68,12 @@ class CustomPropertiesBackendTest extends \Test\TestCase {
]
);
$deleteStatement->closeCursor();
+
+ parent::tearDown();
}
- private function createTestNode($class) {
- $node = $this->getMockBuilder($class)
- ->disableOriginalConstructor()
- ->getMock();
+ private function createTestNode(string $class) {
+ $node = $this->createMock($class);
$node->expects($this->any())
->method('getId')
->willReturn(123);
@@ -122,7 +85,7 @@ class CustomPropertiesBackendTest extends \Test\TestCase {
return $node;
}
- private function applyDefaultProps($path = '/dummypath') {
+ private function applyDefaultProps($path = '/dummypath'): void {
// properties to set
$propPatch = new \Sabre\DAV\PropPatch([
'customprop' => 'value1',
@@ -146,7 +109,7 @@ class CustomPropertiesBackendTest extends \Test\TestCase {
/**
* Test that propFind on a missing file soft fails
*/
- public function testPropFindMissingFileSoftFail() {
+ public function testPropFindMissingFileSoftFail(): void {
$propFind = new \Sabre\DAV\PropFind(
'/dummypath',
[
@@ -174,7 +137,7 @@ class CustomPropertiesBackendTest extends \Test\TestCase {
/**
* Test setting/getting properties
*/
- public function testSetGetPropertiesForFile() {
+ public function testSetGetPropertiesForFile(): void {
$this->applyDefaultProps();
$propFind = new \Sabre\DAV\PropFind(
@@ -200,7 +163,7 @@ class CustomPropertiesBackendTest extends \Test\TestCase {
/**
* Test getting properties from directory
*/
- public function testGetPropertiesForDirectory() {
+ public function testGetPropertiesForDirectory(): void {
$this->applyDefaultProps('/dummypath');
$this->applyDefaultProps('/dummypath/test.txt');
@@ -247,7 +210,7 @@ class CustomPropertiesBackendTest extends \Test\TestCase {
/**
* Test delete property
*/
- public function testDeleteProperty() {
+ public function testDeleteProperty(): void {
$this->applyDefaultProps();
$propPatch = new \Sabre\DAV\PropPatch([
diff --git a/apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php b/apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php
index e8297c2ac66..421ee1bdc12 100644
--- a/apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php
@@ -1,49 +1,36 @@
<?php
+
+declare(strict_types=1);
/**
- * @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 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\Tests\Unit\Connector\Sabre;
+namespace OCA\DAV\Tests\unit\Connector\Sabre;
use OC\Files\FileInfo;
+use OC\Files\Filesystem;
use OC\Files\Node\Node;
use OC\Files\Storage\Wrapper\Quota;
+use OC\Files\View;
use OCA\DAV\Connector\Sabre\Directory;
+use OCA\DAV\Connector\Sabre\Exception\Forbidden;
+use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
+use OCA\Files_Sharing\External\Storage;
+use OCP\Constants;
use OCP\Files\ForbiddenException;
+use OCP\Files\InvalidPathException;
use OCP\Files\Mount\IMountPoint;
-
-class TestViewDirectory extends \OC\Files\View {
- private $updatables;
- private $deletables;
- private $canRename;
-
- public function __construct($updatables, $deletables, $canRename = true) {
- $this->updatables = $updatables;
- $this->deletables = $deletables;
- $this->canRename = $canRename;
+use OCP\Files\StorageNotAvailableException;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\Traits\UserTrait;
+
+class TestViewDirectory extends View {
+ public function __construct(
+ private $updatables,
+ private $deletables,
+ private $canRename = true,
+ ) {
}
public function isUpdatable($path) {
@@ -58,11 +45,11 @@ class TestViewDirectory extends \OC\Files\View {
return $this->deletables[$path];
}
- public function rename($path1, $path2) {
+ public function rename($source, $target, array $options = []) {
return $this->canRename;
}
- public function getRelativePath($path) {
+ public function getRelativePath($path): ?string {
return $path;
}
}
@@ -72,26 +59,29 @@ class TestViewDirectory extends \OC\Files\View {
* @group DB
*/
class DirectoryTest extends \Test\TestCase {
+ use UserTrait;
- /** @var \OC\Files\View | \PHPUnit\Framework\MockObject\MockObject */
- private $view;
- /** @var \OC\Files\FileInfo | \PHPUnit\Framework\MockObject\MockObject */
- private $info;
+ private View&MockObject $view;
+ private FileInfo&MockObject $info;
protected function setUp(): void {
parent::setUp();
- $this->view = $this->createMock('OC\Files\View');
- $this->info = $this->createMock('OC\Files\FileInfo');
+ $this->view = $this->createMock(View::class);
+ $this->info = $this->createMock(FileInfo::class);
$this->info->method('isReadable')
->willReturn(true);
$this->info->method('getType')
->willReturn(Node::TYPE_FOLDER);
$this->info->method('getName')
- ->willReturn("folder");
+ ->willReturn('folder');
+ $this->info->method('getPath')
+ ->willReturn('/admin/files/folder');
+ $this->info->method('getPermissions')
+ ->willReturn(Constants::PERMISSION_READ);
}
- private function getDir($path = '/') {
+ private function getDir(string $path = '/'): Directory {
$this->view->expects($this->once())
->method('getRelativePath')
->willReturn($path);
@@ -104,7 +94,7 @@ class DirectoryTest extends \Test\TestCase {
}
- public function testDeleteRootFolderFails() {
+ public function testDeleteRootFolderFails(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$this->info->expects($this->any())
@@ -117,8 +107,8 @@ class DirectoryTest extends \Test\TestCase {
}
- public function testDeleteForbidden() {
- $this->expectException(\OCA\DAV\Connector\Sabre\Exception\Forbidden::class);
+ public function testDeleteForbidden(): void {
+ $this->expectException(Forbidden::class);
// deletion allowed
$this->info->expects($this->once())
@@ -136,7 +126,7 @@ class DirectoryTest extends \Test\TestCase {
}
- public function testDeleteFolderWhenAllowed() {
+ public function testDeleteFolderWhenAllowed(): void {
// deletion allowed
$this->info->expects($this->once())
->method('isDeletable')
@@ -153,7 +143,7 @@ class DirectoryTest extends \Test\TestCase {
}
- public function testDeleteFolderFailsWhenNotAllowed() {
+ public function testDeleteFolderFailsWhenNotAllowed(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$this->info->expects($this->once())
@@ -165,7 +155,7 @@ class DirectoryTest extends \Test\TestCase {
}
- public function testDeleteFolderThrowsWhenDeletionFailed() {
+ public function testDeleteFolderThrowsWhenDeletionFailed(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
// deletion allowed
@@ -183,13 +173,9 @@ class DirectoryTest extends \Test\TestCase {
$dir->delete();
}
- public function testGetChildren() {
- $info1 = $this->getMockBuilder(FileInfo::class)
- ->disableOriginalConstructor()
- ->getMock();
- $info2 = $this->getMockBuilder(FileInfo::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testGetChildren(): void {
+ $info1 = $this->createMock(FileInfo::class);
+ $info2 = $this->createMock(FileInfo::class);
$info1->method('getName')
->willReturn('first');
$info1->method('getPath')
@@ -205,17 +191,26 @@ class DirectoryTest extends \Test\TestCase {
$this->view->expects($this->once())
->method('getDirectoryContent')
- ->with('')
->willReturn([$info1, $info2]);
$this->view->expects($this->any())
->method('getRelativePath')
- ->willReturn('');
+ ->willReturnCallback(function ($path) {
+ return str_replace('/admin/files/', '', $path);
+ });
+
+ $this->view->expects($this->any())
+ ->method('getAbsolutePath')
+ ->willReturnCallback(function ($path) {
+ return Filesystem::normalizePath('/admin/files' . $path);
+ });
+
+ $this->overwriteService(View::class, $this->view);
$dir = new Directory($this->view, $this->info);
$nodes = $dir->getChildren();
- $this->assertEquals(2, count($nodes));
+ $this->assertCount(2, $nodes);
// calling a second time just returns the cached values,
// does not call getDirectoryContents again
@@ -223,7 +218,7 @@ class DirectoryTest extends \Test\TestCase {
}
- public function testGetChildrenNoPermission() {
+ public function testGetChildrenNoPermission(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$info = $this->createMock(FileInfo::class);
@@ -236,7 +231,7 @@ class DirectoryTest extends \Test\TestCase {
}
- public function testGetChildNoPermission() {
+ public function testGetChildNoPermission(): void {
$this->expectException(\Sabre\DAV\Exception\NotFound::class);
$this->info->expects($this->any())
@@ -248,24 +243,24 @@ class DirectoryTest extends \Test\TestCase {
}
- public function testGetChildThrowStorageNotAvailableException() {
+ public function testGetChildThrowStorageNotAvailableException(): void {
$this->expectException(\Sabre\DAV\Exception\ServiceUnavailable::class);
$this->view->expects($this->once())
->method('getFileInfo')
- ->willThrowException(new \OCP\Files\StorageNotAvailableException());
+ ->willThrowException(new StorageNotAvailableException());
$dir = new Directory($this->view, $this->info);
$dir->getChild('.');
}
- public function testGetChildThrowInvalidPath() {
- $this->expectException(\OCA\DAV\Connector\Sabre\Exception\InvalidPath::class);
+ public function testGetChildThrowInvalidPath(): void {
+ $this->expectException(InvalidPath::class);
$this->view->expects($this->once())
->method('verifyPath')
- ->willThrowException(new \OCP\Files\InvalidPathException());
+ ->willThrowException(new InvalidPathException());
$this->view->expects($this->never())
->method('getFileInfo');
@@ -273,21 +268,26 @@ class DirectoryTest extends \Test\TestCase {
$dir->getChild('.');
}
- public function testGetQuotaInfoUnlimited() {
+ public function testGetQuotaInfoUnlimited(): void {
+ $this->createUser('user', 'password');
+ self::loginAsUser('user');
$mountPoint = $this->createMock(IMountPoint::class);
- $storage = $this->getMockBuilder(Quota::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $storage = $this->createMock(Quota::class);
$mountPoint->method('getStorage')
->willReturn($storage);
$storage->expects($this->any())
->method('instanceOfStorage')
->willReturnMap([
- '\OCA\Files_Sharing\SharedStorage' => false,
- '\OC\Files\Storage\Wrapper\Quota' => false,
+ ['\OCA\Files_Sharing\SharedStorage', false],
+ ['\OC\Files\Storage\Wrapper\Quota', false],
+ [Storage::class, false],
]);
+ $storage->expects($this->once())
+ ->method('getOwner')
+ ->willReturn('user');
+
$storage->expects($this->never())
->method('getQuota');
@@ -295,6 +295,10 @@ class DirectoryTest extends \Test\TestCase {
->method('free_space')
->willReturn(800);
+ $this->info->expects($this->any())
+ ->method('getPath')
+ ->willReturn('/admin/files/foo');
+
$this->info->expects($this->once())
->method('getSize')
->willReturn(200);
@@ -303,6 +307,14 @@ class DirectoryTest extends \Test\TestCase {
->method('getMountPoint')
->willReturn($mountPoint);
+ $this->view->expects($this->any())
+ ->method('getRelativePath')
+ ->willReturn('/foo');
+
+ $this->info->expects($this->once())
+ ->method('getInternalPath')
+ ->willReturn('/foo');
+
$mountPoint->method('getMountPoint')
->willReturn('/user/files/mymountpoint');
@@ -310,11 +322,11 @@ class DirectoryTest extends \Test\TestCase {
$this->assertEquals([200, -3], $dir->getQuotaInfo()); //200 used, unlimited
}
- public function testGetQuotaInfoSpecific() {
+ public function testGetQuotaInfoSpecific(): void {
+ $this->createUser('user', 'password');
+ self::loginAsUser('user');
$mountPoint = $this->createMock(IMountPoint::class);
- $storage = $this->getMockBuilder(Quota::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $storage = $this->createMock(Quota::class);
$mountPoint->method('getStorage')
->willReturn($storage);
@@ -323,9 +335,14 @@ class DirectoryTest extends \Test\TestCase {
->willReturnMap([
['\OCA\Files_Sharing\SharedStorage', false],
['\OC\Files\Storage\Wrapper\Quota', true],
+ [Storage::class, false],
]);
$storage->expects($this->once())
+ ->method('getOwner')
+ ->willReturn('user');
+
+ $storage->expects($this->once())
->method('getQuota')
->willReturn(1000);
@@ -341,46 +358,48 @@ class DirectoryTest extends \Test\TestCase {
->method('getMountPoint')
->willReturn($mountPoint);
+ $this->info->expects($this->once())
+ ->method('getInternalPath')
+ ->willReturn('/foo');
+
$mountPoint->method('getMountPoint')
->willReturn('/user/files/mymountpoint');
+ $this->view->expects($this->any())
+ ->method('getRelativePath')
+ ->willReturn('/foo');
+
$dir = new Directory($this->view, $this->info);
$this->assertEquals([200, 800], $dir->getQuotaInfo()); //200 used, 800 free
}
- /**
- * @dataProvider moveFailedProvider
- */
- public function testMoveFailed($source, $destination, $updatables, $deletables) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('moveFailedProvider')]
+ public function testMoveFailed(string $source, string $destination, array $updatables, array $deletables): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$this->moveTest($source, $destination, $updatables, $deletables);
}
- /**
- * @dataProvider moveSuccessProvider
- */
- public function testMoveSuccess($source, $destination, $updatables, $deletables) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('moveSuccessProvider')]
+ public function testMoveSuccess(string $source, string $destination, array $updatables, array $deletables): void {
$this->moveTest($source, $destination, $updatables, $deletables);
$this->addToAssertionCount(1);
}
- /**
- * @dataProvider moveFailedInvalidCharsProvider
- */
- public function testMoveFailedInvalidChars($source, $destination, $updatables, $deletables) {
- $this->expectException(\OCA\DAV\Connector\Sabre\Exception\InvalidPath::class);
+ #[\PHPUnit\Framework\Attributes\DataProvider('moveFailedInvalidCharsProvider')]
+ public function testMoveFailedInvalidChars(string $source, string $destination, array $updatables, array $deletables): void {
+ $this->expectException(InvalidPath::class);
$this->moveTest($source, $destination, $updatables, $deletables);
}
- public function moveFailedInvalidCharsProvider() {
+ public static function moveFailedInvalidCharsProvider(): array {
return [
- ['a/b', 'a/*', ['a' => true, 'a/b' => true, 'a/c*' => false], []],
+ ['a/valid', "a/i\nvalid", ['a' => true, 'a/valid' => true, 'a/c*' => false], []],
];
}
- public function moveFailedProvider() {
+ public static function moveFailedProvider(): array {
return [
['a/b', 'a/c', ['a' => false, 'a/b' => false, 'a/c' => false], []],
['a/b', 'b/b', ['a' => false, 'a/b' => false, 'b' => false, 'b/b' => false], []],
@@ -391,7 +410,7 @@ class DirectoryTest extends \Test\TestCase {
];
}
- public function moveSuccessProvider() {
+ public static function moveSuccessProvider(): array {
return [
['a/b', 'b/b', ['a' => true, 'a/b' => true, 'b' => true, 'b/b' => false], ['a/b' => true]],
// older files with special chars can still be renamed to valid names
@@ -399,12 +418,7 @@ class DirectoryTest extends \Test\TestCase {
];
}
- /**
- * @param $source
- * @param $destination
- * @param $updatables
- */
- private function moveTest($source, $destination, $updatables, $deletables) {
+ private function moveTest(string $source, string $destination, array $updatables, array $deletables): void {
$view = new TestViewDirectory($updatables, $deletables);
$sourceInfo = new FileInfo($source, null, null, [
@@ -416,7 +430,7 @@ class DirectoryTest extends \Test\TestCase {
$sourceNode = new Directory($view, $sourceInfo);
$targetNode = $this->getMockBuilder(Directory::class)
- ->setMethods(['childExists'])
+ ->onlyMethods(['childExists'])
->setConstructorArgs([$view, $targetInfo])
->getMock();
$targetNode->expects($this->any())->method('childExists')
@@ -426,7 +440,7 @@ class DirectoryTest extends \Test\TestCase {
}
- public function testFailingMove() {
+ public function testFailingMove(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$this->expectExceptionMessage('Could not copy directory b, target exists');
@@ -442,7 +456,7 @@ class DirectoryTest extends \Test\TestCase {
$sourceNode = new Directory($view, $sourceInfo);
$targetNode = $this->getMockBuilder(Directory::class)
- ->setMethods(['childExists'])
+ ->onlyMethods(['childExists'])
->setConstructorArgs([$view, $targetInfo])
->getMock();
$targetNode->expects($this->once())->method('childExists')
diff --git a/apps/dav/tests/unit/Connector/Sabre/DummyGetResponsePluginTest.php b/apps/dav/tests/unit/Connector/Sabre/DummyGetResponsePluginTest.php
index c4b7ed15f2b..2d688d64600 100644
--- a/apps/dav/tests/unit/Connector/Sabre/DummyGetResponsePluginTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/DummyGetResponsePluginTest.php
@@ -1,27 +1,10 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @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 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\Tests\unit\Connector\Sabre;
@@ -37,8 +20,7 @@ use Test\TestCase;
* @package OCA\DAV\Tests\unit\Connector\Sabre
*/
class DummyGetResponsePluginTest extends TestCase {
- /** @var DummyGetResponsePlugin */
- private $dummyGetResponsePlugin;
+ private DummyGetResponsePlugin $dummyGetResponsePlugin;
protected function setUp(): void {
parent::setUp();
@@ -46,11 +28,8 @@ class DummyGetResponsePluginTest extends TestCase {
$this->dummyGetResponsePlugin = new DummyGetResponsePlugin();
}
- public function testInitialize() {
- /** @var Server $server */
- $server = $this->getMockBuilder(Server::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testInitialize(): void {
+ $server = $this->createMock(Server::class);
$server
->expects($this->once())
->method('on')
@@ -60,15 +39,11 @@ class DummyGetResponsePluginTest extends TestCase {
}
- public function testHttpGet() {
+ public function testHttpGet(): void {
/** @var \Sabre\HTTP\RequestInterface $request */
- $request = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $request = $this->createMock(RequestInterface::class);
/** @var \Sabre\HTTP\ResponseInterface $response */
- $response = $server = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $response = $this->createMock(ResponseInterface::class);
$response
->expects($this->once())
->method('setBody');
diff --git a/apps/dav/tests/unit/Connector/Sabre/Exception/ForbiddenTest.php b/apps/dav/tests/unit/Connector/Sabre/Exception/ForbiddenTest.php
index 4d316bf870a..2f9e0ae9196 100644
--- a/apps/dav/tests/unit/Connector/Sabre/Exception/ForbiddenTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/Exception/ForbiddenTest.php
@@ -1,41 +1,28 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @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\Tests\unit\Connector\Sabre\Exception;
use OCA\DAV\Connector\Sabre\Exception\Forbidden;
+use Sabre\DAV\Server;
class ForbiddenTest extends \Test\TestCase {
- public function testSerialization() {
+ public function testSerialization(): void {
// create xml doc
- $DOM = new \DOMDocument('1.0','utf-8');
+ $DOM = new \DOMDocument('1.0', 'utf-8');
$DOM->formatOutput = true;
- $error = $DOM->createElementNS('DAV:','d:error');
+ $error = $DOM->createElementNS('DAV:', 'd:error');
$error->setAttribute('xmlns:s', \Sabre\DAV\Server::NS_SABREDAV);
$DOM->appendChild($error);
// serialize the exception
- $message = "1234567890";
+ $message = '1234567890';
$retry = false;
$expectedXml = <<<EOD
<?xml version="1.0" encoding="utf-8"?>
@@ -47,9 +34,7 @@ class ForbiddenTest extends \Test\TestCase {
EOD;
$ex = new Forbidden($message, $retry);
- $server = $this->getMockBuilder('Sabre\DAV\Server')
- ->disableOriginalConstructor()
- ->getMock();
+ $server = $this->createMock(Server::class);
$ex->serialize($server, $error);
// assert
diff --git a/apps/dav/tests/unit/Connector/Sabre/Exception/InvalidPathTest.php b/apps/dav/tests/unit/Connector/Sabre/Exception/InvalidPathTest.php
index 3c68d780ff3..6f62bef86a3 100644
--- a/apps/dav/tests/unit/Connector/Sabre/Exception/InvalidPathTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/Exception/InvalidPathTest.php
@@ -1,42 +1,28 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @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\Tests\unit\Connector\Sabre\Exception;
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
+use Sabre\DAV\Server;
class InvalidPathTest extends \Test\TestCase {
- public function testSerialization() {
+ public function testSerialization(): void {
// create xml doc
- $DOM = new \DOMDocument('1.0','utf-8');
+ $DOM = new \DOMDocument('1.0', 'utf-8');
$DOM->formatOutput = true;
- $error = $DOM->createElementNS('DAV:','d:error');
+ $error = $DOM->createElementNS('DAV:', 'd:error');
$error->setAttribute('xmlns:s', \Sabre\DAV\Server::NS_SABREDAV);
$DOM->appendChild($error);
// serialize the exception
- $message = "1234567890";
+ $message = '1234567890';
$retry = false;
$expectedXml = <<<EOD
<?xml version="1.0" encoding="utf-8"?>
@@ -48,9 +34,7 @@ class InvalidPathTest extends \Test\TestCase {
EOD;
$ex = new InvalidPath($message, $retry);
- $server = $this->getMockBuilder('Sabre\DAV\Server')
- ->disableOriginalConstructor()
- ->getMock();
+ $server = $this->createMock(Server::class);
$ex->serialize($server, $error);
// assert
diff --git a/apps/dav/tests/unit/Connector/Sabre/ExceptionLoggerPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/ExceptionLoggerPluginTest.php
index 83f8c416577..416ac8a75c9 100644
--- a/apps/dav/tests/unit/Connector/Sabre/ExceptionLoggerPluginTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/ExceptionLoggerPluginTest.php
@@ -1,63 +1,29 @@
<?php
+
+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 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>
- *
- * @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\Tests\unit\Connector\Sabre;
-use OC\Log;
use OC\SystemConfig;
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
-use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin as PluginToTest;
+use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin;
+use OCA\DAV\Exception\ServerMaintenanceMode;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\NotFound;
-use Sabre\DAV\Exception\ServiceUnavailable;
use Sabre\DAV\Server;
use Test\TestCase;
-class TestLogger extends Log {
- public $message;
- public $level;
-
- public function writeLog(string $app, $entry, int $level) {
- $this->level = $level;
- $this->message = $entry;
- }
-}
-
class ExceptionLoggerPluginTest extends TestCase {
+ private Server $server;
+ private ExceptionLoggerPlugin $plugin;
+ private LoggerInterface&MockObject $logger;
- /** @var Server */
- private $server;
-
- /** @var PluginToTest */
- private $plugin;
-
- /** @var TestLogger | \PHPUnit\Framework\MockObject\MockObject */
- private $logger;
-
- private function init() {
+ private function init(): void {
$config = $this->createMock(SystemConfig::class);
$config->expects($this->any())
->method('getValue')
@@ -71,29 +37,30 @@ class ExceptionLoggerPluginTest extends TestCase {
});
$this->server = new Server();
- $this->logger = new TestLogger(new Log\File(\OC::$SERVERROOT.'/data/nextcloud.log', '', $config), $config);
- $this->plugin = new PluginToTest('unit-test', $this->logger);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->plugin = new ExceptionLoggerPlugin('unit-test', $this->logger);
$this->plugin->initialize($this->server);
}
- /**
- * @dataProvider providesExceptions
- */
- public function testLogging($expectedLogLevel, $expectedMessage, $exception) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesExceptions')]
+ public function testLogging(string $expectedLogLevel, \Throwable $e): void {
$this->init();
- $this->plugin->logException($exception);
- $this->assertEquals($expectedLogLevel, $this->logger->level);
- $this->assertEquals(get_class($exception), $this->logger->message['Exception']);
- $this->assertEquals($expectedMessage, $this->logger->message['Message']);
+ $this->logger->expects($this->once())
+ ->method($expectedLogLevel)
+ ->with($e->getMessage(), ['app' => 'unit-test','exception' => $e]);
+
+ $this->plugin->logException($e);
}
- public function providesExceptions() {
+ public static function providesExceptions(): array {
return [
- [0, '', new NotFound()],
- [0, 'System in maintenance mode.', new ServiceUnavailable('System in maintenance mode.')],
- [4, 'Upgrade needed', new ServiceUnavailable('Upgrade needed')],
- [4, 'This path leads to nowhere', new InvalidPath('This path leads to nowhere')]
+ ['debug', new NotFound()],
+ ['debug', new ServerMaintenanceMode('System is in maintenance mode.')],
+ // Faking a translation
+ ['debug', new ServerMaintenanceMode('Syst3m 1s 1n m41nt3n4nc3 m0d3.')],
+ ['debug', new ServerMaintenanceMode('Upgrade needed')],
+ ['critical', new InvalidPath('This path leads to nowhere')]
];
}
}
diff --git a/apps/dav/tests/unit/Connector/Sabre/FakeLockerPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/FakeLockerPluginTest.php
index 578576e3f07..366932137f4 100644
--- a/apps/dav/tests/unit/Connector/Sabre/FakeLockerPluginTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/FakeLockerPluginTest.php
@@ -1,29 +1,10 @@
<?php
+
+declare(strict_types=1);
/**
- * @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 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\Tests\unit\Connector\Sabre;
@@ -42,40 +23,33 @@ use Test\TestCase;
* @package OCA\DAV\Tests\unit\Connector\Sabre
*/
class FakeLockerPluginTest extends TestCase {
- /** @var FakeLockerPlugin */
- private $fakeLockerPlugin;
+ private FakeLockerPlugin $fakeLockerPlugin;
protected function setUp(): void {
parent::setUp();
$this->fakeLockerPlugin = new FakeLockerPlugin();
}
- public function testInitialize() {
+ public function testInitialize(): void {
/** @var Server $server */
- $server = $this->getMockBuilder(Server::class)
- ->disableOriginalConstructor()
- ->getMock();
- $server
- ->expects($this->at(0))
- ->method('on')
- ->with('method:LOCK', [$this->fakeLockerPlugin, 'fakeLockProvider'], 1);
- $server
- ->expects($this->at(1))
- ->method('on')
- ->with('method:UNLOCK', [$this->fakeLockerPlugin, 'fakeUnlockProvider'], 1);
- $server
- ->expects($this->at(2))
- ->method('on')
- ->with('propFind', [$this->fakeLockerPlugin, 'propFind']);
- $server
- ->expects($this->at(3))
+ $server = $this->createMock(Server::class);
+ $calls = [
+ ['method:LOCK', [$this->fakeLockerPlugin, 'fakeLockProvider'], 1],
+ ['method:UNLOCK', [$this->fakeLockerPlugin, 'fakeUnlockProvider'], 1],
+ ['propFind', [$this->fakeLockerPlugin, 'propFind'], 100],
+ ['validateTokens', [$this->fakeLockerPlugin, 'validateTokens'], 100],
+ ];
+ $server->expects($this->exactly(count($calls)))
->method('on')
- ->with('validateTokens', [$this->fakeLockerPlugin, 'validateTokens']);
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
$this->fakeLockerPlugin->initialize($server);
}
- public function testGetHTTPMethods() {
+ public function testGetHTTPMethods(): void {
$expected = [
'LOCK',
'UNLOCK',
@@ -83,32 +57,32 @@ class FakeLockerPluginTest extends TestCase {
$this->assertSame($expected, $this->fakeLockerPlugin->getHTTPMethods('Test'));
}
- public function testGetFeatures() {
+ public function testGetFeatures(): void {
$expected = [
2,
];
$this->assertSame($expected, $this->fakeLockerPlugin->getFeatures());
}
- public function testPropFind() {
- $propFind = $this->getMockBuilder(PropFind::class)
- ->disableOriginalConstructor()
- ->getMock();
- $node = $this->getMockBuilder(INode::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testPropFind(): void {
+ $propFind = $this->createMock(PropFind::class);
+ $node = $this->createMock(INode::class);
- $propFind->expects($this->at(0))
- ->method('handle')
- ->with('{DAV:}supportedlock');
- $propFind->expects($this->at(1))
+ $calls = [
+ '{DAV:}supportedlock',
+ '{DAV:}lockdiscovery',
+ ];
+ $propFind->expects($this->exactly(count($calls)))
->method('handle')
- ->with('{DAV:}lockdiscovery');
+ ->willReturnCallback(function ($propertyName) use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, $propertyName);
+ });
$this->fakeLockerPlugin->propFind($propFind, $node);
}
- public function tokenDataProvider() {
+ public static function tokenDataProvider(): array {
return [
[
[
@@ -145,23 +119,15 @@ class FakeLockerPluginTest extends TestCase {
];
}
- /**
- * @dataProvider tokenDataProvider
- * @param array $input
- * @param array $expected
- */
- public function testValidateTokens(array $input, array $expected) {
- $request = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ #[\PHPUnit\Framework\Attributes\DataProvider('tokenDataProvider')]
+ public function testValidateTokens(array $input, array $expected): void {
+ $request = $this->createMock(RequestInterface::class);
$this->fakeLockerPlugin->validateTokens($request, $input);
$this->assertSame($expected, $input);
}
- public function testFakeLockProvider() {
- $request = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testFakeLockProvider(): void {
+ $request = $this->createMock(RequestInterface::class);
$response = new Response();
$server = $this->getMockBuilder(Server::class)
->getMock();
@@ -178,20 +144,16 @@ class FakeLockerPluginTest extends TestCase {
$this->assertXmlStringEqualsXmlString($expectedXml, $response->getBody());
}
- public function testFakeUnlockProvider() {
- $request = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testFakeUnlockProvider(): void {
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
$response->expects($this->once())
- ->method('setStatus')
- ->with('204');
+ ->method('setStatus')
+ ->with('204');
$response->expects($this->once())
- ->method('setHeader')
- ->with('Content-Length', '0');
+ ->method('setHeader')
+ ->with('Content-Length', '0');
$this->assertSame(false, $this->fakeLockerPlugin->fakeUnlockProvider($request, $response));
}
diff --git a/apps/dav/tests/unit/Connector/Sabre/FileTest.php b/apps/dav/tests/unit/Connector/Sabre/FileTest.php
index 9870a62845c..60c8382e131 100644
--- a/apps/dav/tests/unit/Connector/Sabre/FileTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/FileTest.php
@@ -1,31 +1,10 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Calviño Sánchez <danxuliu@gmail.com>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Joas Schilling <coding@schilljs.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 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\Tests\unit\Connector\Sabre;
@@ -35,14 +14,29 @@ use OC\Files\Storage\Local;
use OC\Files\Storage\Temporary;
use OC\Files\Storage\Wrapper\PermissionsMask;
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 OCA\DAV\Connector\Sabre\File;
use OCP\Constants;
+use OCP\Encryption\Exceptions\GenericEncryptionException;
+use OCP\Files\EntityTooLargeException;
use OCP\Files\FileInfo;
use OCP\Files\ForbiddenException;
-use OCP\Files\Storage;
+use OCP\Files\InvalidContentException;
+use OCP\Files\InvalidPathException;
+use OCP\Files\LockNotAcquiredException;
+use OCP\Files\NotPermittedException;
+use OCP\Files\Storage\IStorage;
+use OCP\Files\StorageNotAvailableException;
use OCP\IConfig;
use OCP\IRequestId;
+use OCP\ITempManager;
+use OCP\IUserManager;
use OCP\Lock\ILockingProvider;
+use OCP\Lock\LockedException;
+use OCP\Server;
+use OCP\Util;
use PHPUnit\Framework\MockObject\MockObject;
use Test\HookHelper;
use Test\TestCase;
@@ -60,58 +54,39 @@ class FileTest extends TestCase {
use MountProviderTrait;
use UserTrait;
- /**
- * @var string
- */
- private $user;
-
- /** @var IConfig|MockObject */
- protected $config;
-
- /** @var IRequestId|MockObject */
- protected $requestId;
+ private string $user;
+ protected IConfig&MockObject $config;
+ protected IRequestId&MockObject $requestId;
protected function setUp(): void {
parent::setUp();
- unset($_SERVER['HTTP_OC_CHUNKED']);
- unset($_SERVER['CONTENT_LENGTH']);
- unset($_SERVER['REQUEST_METHOD']);
\OC_Hook::clear();
$this->user = 'test_user';
$this->createUser($this->user, 'pass');
- $this->loginAsUser($this->user);
+ self::loginAsUser($this->user);
$this->config = $this->createMock(IConfig::class);
$this->requestId = $this->createMock(IRequestId::class);
}
protected function tearDown(): void {
- $userManager = \OC::$server->getUserManager();
+ $userManager = Server::get(IUserManager::class);
$userManager->get($this->user)->delete();
- unset($_SERVER['HTTP_OC_CHUNKED']);
parent::tearDown();
}
- /**
- * @return MockObject|Storage
- */
- private function getMockStorage() {
- $storage = $this->getMockBuilder(Storage::class)
- ->disableOriginalConstructor()
- ->getMock();
+ private function getMockStorage(): MockObject&IStorage {
+ $storage = $this->createMock(IStorage::class);
$storage->method('getId')
->willReturn('home::someuser');
return $storage;
}
- /**
- * @param string $string
- */
- private function getStream($string) {
+ private function getStream(string $string) {
$stream = fopen('php://temp', 'r+');
fwrite($stream, $string);
fseek($stream, 0);
@@ -119,7 +94,7 @@ class FileTest extends TestCase {
}
- public function fopenFailuresProvider() {
+ public static function fopenFailuresProvider(): array {
return [
[
// return false
@@ -128,39 +103,39 @@ class FileTest extends TestCase {
false
],
[
- new \OCP\Files\NotPermittedException(),
+ new NotPermittedException(),
'Sabre\DAV\Exception\Forbidden'
],
[
- new \OCP\Files\EntityTooLargeException(),
+ new EntityTooLargeException(),
'OCA\DAV\Connector\Sabre\Exception\EntityTooLarge'
],
[
- new \OCP\Files\InvalidContentException(),
+ new InvalidContentException(),
'OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType'
],
[
- new \OCP\Files\InvalidPathException(),
+ new InvalidPathException(),
'Sabre\DAV\Exception\Forbidden'
],
[
- new \OCP\Files\ForbiddenException('', true),
+ new ForbiddenException('', true),
'OCA\DAV\Connector\Sabre\Exception\Forbidden'
],
[
- new \OCP\Files\LockNotAcquiredException('/test.txt', 1),
+ new LockNotAcquiredException('/test.txt', 1),
'OCA\DAV\Connector\Sabre\Exception\FileLocked'
],
[
- new \OCP\Lock\LockedException('/test.txt'),
+ new LockedException('/test.txt'),
'OCA\DAV\Connector\Sabre\Exception\FileLocked'
],
[
- new \OCP\Encryption\Exceptions\GenericEncryptionException(),
+ new GenericEncryptionException(),
'Sabre\DAV\Exception\ServiceUnavailable'
],
[
- new \OCP\Files\StorageNotAvailableException(),
+ new StorageNotAvailableException(),
'Sabre\DAV\Exception\ServiceUnavailable'
],
[
@@ -175,19 +150,17 @@ class FileTest extends TestCase {
];
}
- /**
- * @dataProvider fopenFailuresProvider
- */
- public function testSimplePutFails($thrownException, $expectedException, $checkPreviousClass = true) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('fopenFailuresProvider')]
+ public function testSimplePutFails(?\Throwable $thrownException, string $expectedException, bool $checkPreviousClass = true): void {
// setup
$storage = $this->getMockBuilder(Local::class)
- ->setMethods(['writeStream'])
- ->setConstructorArgs([['datadir' => \OC::$server->getTempManager()->getTemporaryFolder()]])
+ ->onlyMethods(['writeStream'])
+ ->setConstructorArgs([['datadir' => Server::get(ITempManager::class)->getTemporaryFolder()]])
->getMock();
- \OC\Files\Filesystem::mount($storage, [], $this->user . '/');
- /** @var View | MockObject $view */
+ Filesystem::mount($storage, [], $this->user . '/');
+ /** @var View&MockObject $view */
$view = $this->getMockBuilder(View::class)
- ->setMethods(['getRelativePath', 'resolvePath'])
+ ->onlyMethods(['getRelativePath', 'resolvePath'])
->getMock();
$view->expects($this->atLeastOnce())
->method('resolvePath')
@@ -200,7 +173,7 @@ class FileTest extends TestCase {
if ($thrownException !== null) {
$storage->expects($this->once())
->method('writeStream')
- ->will($this->throwException($thrownException));
+ ->willThrowException($thrownException);
} else {
$storage->expects($this->once())
->method('writeStream')
@@ -212,11 +185,11 @@ class FileTest extends TestCase {
->willReturnArgument(0);
$info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
- 'permissions' => \OCP\Constants::PERMISSION_ALL,
+ 'permissions' => Constants::PERMISSION_ALL,
'type' => FileInfo::TYPE_FOLDER,
], null);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+ $file = new File($view, $info);
// action
$caughtException = null;
@@ -235,93 +208,18 @@ class FileTest extends TestCase {
}
/**
- * Test putting a file using chunking
- *
- * @dataProvider fopenFailuresProvider
- */
- public function testChunkedPutFails($thrownException, $expectedException, $checkPreviousClass = false) {
- // setup
- $storage = $this->getMockBuilder(Local::class)
- ->setMethods(['fopen'])
- ->setConstructorArgs([['datadir' => \OC::$server->getTempManager()->getTemporaryFolder()]])
- ->getMock();
- \OC\Files\Filesystem::mount($storage, [], $this->user . '/');
- $view = $this->getMockBuilder(View::class)
- ->setMethods(['getRelativePath', 'resolvePath'])
- ->getMock();
- $view->expects($this->atLeastOnce())
- ->method('resolvePath')
- ->willReturnCallback(
- function ($path) use ($storage) {
- return [$storage, $path];
- }
- );
-
- if ($thrownException !== null) {
- $storage->expects($this->once())
- ->method('fopen')
- ->will($this->throwException($thrownException));
- } else {
- $storage->expects($this->once())
- ->method('fopen')
- ->willReturn(false);
- }
-
- $view->expects($this->any())
- ->method('getRelativePath')
- ->willReturnArgument(0);
-
- $_SERVER['HTTP_OC_CHUNKED'] = true;
-
- $info = new \OC\Files\FileInfo('/test.txt-chunking-12345-2-0', $this->getMockStorage(), null, [
- 'permissions' => \OCP\Constants::PERMISSION_ALL,
- 'type' => FileInfo::TYPE_FOLDER,
- ], null);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
-
- // put first chunk
- $file->acquireLock(ILockingProvider::LOCK_SHARED);
- $this->assertNull($file->put('test data one'));
- $file->releaseLock(ILockingProvider::LOCK_SHARED);
-
- $info = new \OC\Files\FileInfo('/test.txt-chunking-12345-2-1', $this->getMockStorage(), null, [
- 'permissions' => \OCP\Constants::PERMISSION_ALL,
- 'type' => FileInfo::TYPE_FOLDER,
- ], null);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
-
- // action
- $caughtException = null;
- try {
- // last chunk
- $file->acquireLock(ILockingProvider::LOCK_SHARED);
- $file->put('test data two');
- $file->releaseLock(ILockingProvider::LOCK_SHARED);
- } catch (\Exception $e) {
- $caughtException = $e;
- }
-
- $this->assertInstanceOf($expectedException, $caughtException);
- if ($checkPreviousClass) {
- $this->assertInstanceOf(get_class($thrownException), $caughtException->getPrevious());
- }
-
- $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
- }
-
- /**
* Simulate putting a file to the given path.
*
* @param string $path path to put the file into
- * @param string $viewRoot root to use for the view
+ * @param ?string $viewRoot root to use for the view
* @param null|Request $request the HTTP request
*
- * @return null|string of the PUT operaiton which is usually the etag
+ * @return null|string of the PUT operation which is usually the etag
*/
- private function doPut($path, $viewRoot = null, Request $request = null) {
- $view = \OC\Files\Filesystem::getView();
+ private function doPut(string $path, ?string $viewRoot = null, ?Request $request = null) {
+ $view = Filesystem::getView();
if (!is_null($viewRoot)) {
- $view = new \OC\Files\View($viewRoot);
+ $view = new View($viewRoot);
} else {
$viewRoot = '/' . $this->user . '/files';
}
@@ -331,16 +229,16 @@ class FileTest extends TestCase {
$this->getMockStorage(),
null,
[
- 'permissions' => \OCP\Constants::PERMISSION_ALL,
+ 'permissions' => Constants::PERMISSION_ALL,
'type' => FileInfo::TYPE_FOLDER,
],
null
);
- /** @var \OCA\DAV\Connector\Sabre\File | MockObject $file */
- $file = $this->getMockBuilder(\OCA\DAV\Connector\Sabre\File::class)
+ /** @var File&MockObject $file */
+ $file = $this->getMockBuilder(File::class)
->setConstructorArgs([$view, $info, null, $request])
- ->setMethods(['header'])
+ ->onlyMethods(['header'])
->getMock();
// beforeMethod locks
@@ -357,71 +255,71 @@ class FileTest extends TestCase {
/**
* Test putting a single file
*/
- public function testPutSingleFile() {
+ public function testPutSingleFile(): void {
$this->assertNotEmpty($this->doPut('/foo.txt'));
}
- public function legalMtimeProvider() {
+ public static function legalMtimeProvider(): array {
return [
- "string" => [
- 'HTTP_X_OC_MTIME' => "string",
- 'expected result' => null
+ 'string' => [
+ 'requestMtime' => 'string',
+ 'resultMtime' => null
],
- "castable string (int)" => [
- 'HTTP_X_OC_MTIME' => "987654321",
- 'expected result' => 987654321
+ 'castable string (int)' => [
+ 'requestMtime' => '987654321',
+ 'resultMtime' => 987654321
],
- "castable string (float)" => [
- 'HTTP_X_OC_MTIME' => "123456789.56",
- 'expected result' => 123456789
+ 'castable string (float)' => [
+ 'requestMtime' => '123456789.56',
+ 'resultMtime' => 123456789
],
- "float" => [
- 'HTTP_X_OC_MTIME' => 123456789.56,
- 'expected result' => 123456789
+ 'float' => [
+ 'requestMtime' => 123456789.56,
+ 'resultMtime' => 123456789
],
- "zero" => [
- 'HTTP_X_OC_MTIME' => 0,
- 'expected result' => null
+ 'zero' => [
+ 'requestMtime' => 0,
+ 'resultMtime' => null
],
- "zero string" => [
- 'HTTP_X_OC_MTIME' => "0",
- 'expected result' => null
+ 'zero string' => [
+ 'requestMtime' => '0',
+ 'resultMtime' => null
],
- "negative zero string" => [
- 'HTTP_X_OC_MTIME' => "-0",
- 'expected result' => null
+ 'negative zero string' => [
+ 'requestMtime' => '-0',
+ 'resultMtime' => null
],
- "string starting with number following by char" => [
- 'HTTP_X_OC_MTIME' => "2345asdf",
- 'expected result' => null
+ 'string starting with number following by char' => [
+ 'requestMtime' => '2345asdf',
+ 'resultMtime' => null
],
- "string castable hex int" => [
- 'HTTP_X_OC_MTIME' => "0x45adf",
- 'expected result' => null
+ 'string castable hex int' => [
+ 'requestMtime' => '0x45adf',
+ 'resultMtime' => null
],
- "string that looks like invalid hex int" => [
- 'HTTP_X_OC_MTIME' => "0x123g",
- 'expected result' => null
+ 'string that looks like invalid hex int' => [
+ 'requestMtime' => '0x123g',
+ 'resultMtime' => null
],
- "negative int" => [
- 'HTTP_X_OC_MTIME' => -34,
- 'expected result' => null
+ 'negative int' => [
+ 'requestMtime' => -34,
+ 'resultMtime' => null
],
- "negative float" => [
- 'HTTP_X_OC_MTIME' => -34.43,
- 'expected result' => null
+ 'negative float' => [
+ 'requestMtime' => -34.43,
+ 'resultMtime' => null
],
];
}
/**
* Test putting a file with string Mtime
- * @dataProvider legalMtimeProvider
*/
- public function testPutSingleFileLegalMtime($requestMtime, $resultMtime) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('legalMtimeProvider')]
+ public function testPutSingleFileLegalMtime(mixed $requestMtime, ?int $resultMtime): void {
$request = new Request([
'server' => [
- 'HTTP_X_OC_MTIME' => $requestMtime,
+ 'HTTP_X_OC_MTIME' => (string)$requestMtime,
]
], $this->requestId, $this->config, null);
$file = 'foo.txt';
@@ -438,44 +336,9 @@ class FileTest extends TestCase {
}
/**
- * Test putting a file with string Mtime using chunking
- * @dataProvider legalMtimeProvider
- */
- public function testChunkedPutLegalMtime($requestMtime, $resultMtime) {
- $request = new Request([
- 'server' => [
- 'HTTP_X_OC_MTIME' => $requestMtime,
- ]
- ], $this->requestId, $this->config, null);
-
- $_SERVER['HTTP_OC_CHUNKED'] = true;
- $file = 'foo.txt';
-
- if ($resultMtime === null) {
- $this->expectException(\Sabre\DAV\Exception::class);
- }
-
- $this->doPut($file.'-chunking-12345-2-0', null, $request);
- $this->doPut($file.'-chunking-12345-2-1', null, $request);
-
- if ($resultMtime !== null) {
- $this->assertEquals($resultMtime, $this->getFileInfos($file)['mtime']);
- }
- }
-
- /**
- * Test putting a file using chunking
- */
- public function testChunkedPut() {
- $_SERVER['HTTP_OC_CHUNKED'] = true;
- $this->assertNull($this->doPut('/test.txt-chunking-12345-2-0'));
- $this->assertNotEmpty($this->doPut('/test.txt-chunking-12345-2-1'));
- }
-
- /**
* Test that putting a file triggers create hooks
*/
- public function testPutSingleFileTriggersHooks() {
+ public function testPutSingleFileTriggersHooks(): void {
HookHelper::setUpHooks();
$this->assertNotEmpty($this->doPut('/foo.txt'));
@@ -506,8 +369,8 @@ class FileTest extends TestCase {
/**
* Test that putting a file triggers update hooks
*/
- public function testPutOverwriteFileTriggersHooks() {
- $view = \OC\Files\Filesystem::getView();
+ public function testPutOverwriteFileTriggersHooks(): void {
+ $view = Filesystem::getView();
$view->file_put_contents('/foo.txt', 'some content that will be replaced');
HookHelper::setUpHooks();
@@ -542,8 +405,8 @@ class FileTest extends TestCase {
* if the passed view was chrooted (can happen with public webdav
* where the root is the share root)
*/
- public function testPutSingleFileTriggersHooksDifferentRoot() {
- $view = \OC\Files\Filesystem::getView();
+ public function testPutSingleFileTriggersHooksDifferentRoot(): void {
+ $view = Filesystem::getView();
$view->mkdir('noderoot');
HookHelper::setUpHooks();
@@ -574,76 +437,7 @@ class FileTest extends TestCase {
);
}
- /**
- * Test that putting a file with chunks triggers create hooks
- */
- public function testPutChunkedFileTriggersHooks() {
- HookHelper::setUpHooks();
-
- $_SERVER['HTTP_OC_CHUNKED'] = true;
- $this->assertNull($this->doPut('/foo.txt-chunking-12345-2-0'));
- $this->assertNotEmpty($this->doPut('/foo.txt-chunking-12345-2-1'));
-
- $this->assertCount(4, HookHelper::$hookCalls);
- $this->assertHookCall(
- HookHelper::$hookCalls[0],
- Filesystem::signal_create,
- '/foo.txt'
- );
- $this->assertHookCall(
- HookHelper::$hookCalls[1],
- Filesystem::signal_write,
- '/foo.txt'
- );
- $this->assertHookCall(
- HookHelper::$hookCalls[2],
- Filesystem::signal_post_create,
- '/foo.txt'
- );
- $this->assertHookCall(
- HookHelper::$hookCalls[3],
- Filesystem::signal_post_write,
- '/foo.txt'
- );
- }
-
- /**
- * Test that putting a chunked file triggers update hooks
- */
- public function testPutOverwriteChunkedFileTriggersHooks() {
- $view = \OC\Files\Filesystem::getView();
- $view->file_put_contents('/foo.txt', 'some content that will be replaced');
-
- HookHelper::setUpHooks();
-
- $_SERVER['HTTP_OC_CHUNKED'] = true;
- $this->assertNull($this->doPut('/foo.txt-chunking-12345-2-0'));
- $this->assertNotEmpty($this->doPut('/foo.txt-chunking-12345-2-1'));
-
- $this->assertCount(4, HookHelper::$hookCalls);
- $this->assertHookCall(
- HookHelper::$hookCalls[0],
- Filesystem::signal_update,
- '/foo.txt'
- );
- $this->assertHookCall(
- HookHelper::$hookCalls[1],
- Filesystem::signal_write,
- '/foo.txt'
- );
- $this->assertHookCall(
- HookHelper::$hookCalls[2],
- Filesystem::signal_post_update,
- '/foo.txt'
- );
- $this->assertHookCall(
- HookHelper::$hookCalls[3],
- Filesystem::signal_post_write,
- '/foo.txt'
- );
- }
-
- public static function cancellingHook($params) {
+ public static function cancellingHook($params): void {
self::$hookCalls[] = [
'signal' => Filesystem::signal_post_create,
'params' => $params
@@ -653,8 +447,8 @@ class FileTest extends TestCase {
/**
* Test put file with cancelled hook
*/
- public function testPutSingleFileCancelPreHook() {
- \OCP\Util::connectHook(
+ public function testPutSingleFileCancelPreHook(): void {
+ Util::connectHook(
Filesystem::CLASSNAME,
Filesystem::signal_create,
'\Test\HookHelper',
@@ -676,10 +470,11 @@ class FileTest extends TestCase {
/**
* Test exception when the uploaded size did not match
*/
- public function testSimplePutFailsSizeCheck() {
+ public function testSimplePutFailsSizeCheck(): void {
// setup
+ /** @var View&MockObject */
$view = $this->getMockBuilder(View::class)
- ->setMethods(['rename', 'getRelativePath', 'filesize'])
+ ->onlyMethods(['rename', 'getRelativePath', 'filesize'])
->getMock();
$view->expects($this->any())
->method('rename')
@@ -693,15 +488,19 @@ class FileTest extends TestCase {
->method('filesize')
->willReturn(123456);
- $_SERVER['CONTENT_LENGTH'] = 123456;
- $_SERVER['REQUEST_METHOD'] = 'PUT';
+ $request = new Request([
+ 'server' => [
+ 'CONTENT_LENGTH' => '123456',
+ ],
+ 'method' => 'PUT',
+ ], $this->requestId, $this->config, null);
$info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
- 'permissions' => \OCP\Constants::PERMISSION_ALL,
+ 'permissions' => Constants::PERMISSION_ALL,
'type' => FileInfo::TYPE_FOLDER,
], null);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+ $file = new File($view, $info, null, $request);
// action
$thrown = false;
@@ -724,18 +523,18 @@ class FileTest extends TestCase {
/**
* Test exception during final rename in simple upload mode
*/
- public function testSimplePutFailsMoveFromStorage() {
- $view = new \OC\Files\View('/' . $this->user . '/files');
+ public function testSimplePutFailsMoveFromStorage(): void {
+ $view = new View('/' . $this->user . '/files');
// simulate situation where the target file is locked
$view->lockFile('/test.txt', ILockingProvider::LOCK_EXCLUSIVE);
$info = new \OC\Files\FileInfo('/' . $this->user . '/files/test.txt', $this->getMockStorage(), null, [
- 'permissions' => \OCP\Constants::PERMISSION_ALL,
+ 'permissions' => Constants::PERMISSION_ALL,
'type' => FileInfo::TYPE_FOLDER,
], null);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+ $file = new File($view, $info);
// action
$thrown = false;
@@ -747,47 +546,7 @@ class FileTest extends TestCase {
// afterMethod unlocks
$view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
- } catch (\OCA\DAV\Connector\Sabre\Exception\FileLocked $e) {
- $thrown = true;
- }
-
- $this->assertTrue($thrown);
- $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
- }
-
- /**
- * Test exception during final rename in chunk upload mode
- */
- public function testChunkedPutFailsFinalRename() {
- $view = new \OC\Files\View('/' . $this->user . '/files');
-
- // simulate situation where the target file is locked
- $view->lockFile('/test.txt', ILockingProvider::LOCK_EXCLUSIVE);
-
- $_SERVER['HTTP_OC_CHUNKED'] = true;
-
- $info = new \OC\Files\FileInfo('/' . $this->user . '/files/test.txt-chunking-12345-2-0', $this->getMockStorage(), null, [
- 'permissions' => \OCP\Constants::PERMISSION_ALL,
- 'type' => FileInfo::TYPE_FOLDER,
- ], null);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
- $file->acquireLock(ILockingProvider::LOCK_SHARED);
- $this->assertNull($file->put('test data one'));
- $file->releaseLock(ILockingProvider::LOCK_SHARED);
-
- $info = new \OC\Files\FileInfo('/' . $this->user . '/files/test.txt-chunking-12345-2-1', $this->getMockStorage(), null, [
- 'permissions' => \OCP\Constants::PERMISSION_ALL,
- 'type' => FileInfo::TYPE_FOLDER,
- ], null);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
-
- // action
- $thrown = false;
- try {
- $file->acquireLock(ILockingProvider::LOCK_SHARED);
- $file->put($this->getStream('test data'));
- $file->releaseLock(ILockingProvider::LOCK_SHARED);
- } catch (\OCA\DAV\Connector\Sabre\Exception\FileLocked $e) {
+ } catch (FileLocked $e) {
$thrown = true;
}
@@ -798,20 +557,21 @@ class FileTest extends TestCase {
/**
* Test put file with invalid chars
*/
- public function testSimplePutInvalidChars() {
+ public function testSimplePutInvalidChars(): void {
// setup
+ /** @var View&MockObject */
$view = $this->getMockBuilder(View::class)
- ->setMethods(['getRelativePath'])
+ ->onlyMethods(['getRelativePath'])
->getMock();
$view->expects($this->any())
->method('getRelativePath')
->willReturnArgument(0);
- $info = new \OC\Files\FileInfo('/*', $this->getMockStorage(), null, [
- 'permissions' => \OCP\Constants::PERMISSION_ALL,
+ $info = new \OC\Files\FileInfo("/i\nvalid", $this->getMockStorage(), null, [
+ 'permissions' => Constants::PERMISSION_ALL,
'type' => FileInfo::TYPE_FOLDER,
], null);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+ $file = new File($view, $info);
// action
$thrown = false;
@@ -823,7 +583,7 @@ class FileTest extends TestCase {
// afterMethod unlocks
$view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
- } catch (\OCA\DAV\Connector\Sabre\Exception\InvalidPath $e) {
+ } catch (InvalidPath $e) {
$thrown = true;
}
@@ -835,31 +595,34 @@ class FileTest extends TestCase {
* Test setting name with setName() with invalid chars
*
*/
- public function testSetNameInvalidChars() {
- $this->expectException(\OCA\DAV\Connector\Sabre\Exception\InvalidPath::class);
+ public function testSetNameInvalidChars(): void {
+ $this->expectException(InvalidPath::class);
// setup
+ /** @var View&MockObject */
$view = $this->getMockBuilder(View::class)
- ->setMethods(['getRelativePath'])
+ ->onlyMethods(['getRelativePath'])
->getMock();
$view->expects($this->any())
->method('getRelativePath')
->willReturnArgument(0);
- $info = new \OC\Files\FileInfo('/*', $this->getMockStorage(), null, [
- 'permissions' => \OCP\Constants::PERMISSION_ALL,
+ $info = new \OC\Files\FileInfo('/valid', $this->getMockStorage(), null, [
+ 'permissions' => Constants::PERMISSION_ALL,
'type' => FileInfo::TYPE_FOLDER,
], null);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
- $file->setName('/super*star.txt');
+ $file = new File($view, $info);
+
+ $file->setName("/i\nvalid");
}
- public function testUploadAbort() {
+ public function testUploadAbort(): void {
// setup
+ /** @var View&MockObject */
$view = $this->getMockBuilder(View::class)
- ->setMethods(['rename', 'getRelativePath', 'filesize'])
+ ->onlyMethods(['rename', 'getRelativePath', 'filesize'])
->getMock();
$view->expects($this->any())
->method('rename')
@@ -872,15 +635,19 @@ class FileTest extends TestCase {
->method('filesize')
->willReturn(123456);
- $_SERVER['CONTENT_LENGTH'] = 12345;
- $_SERVER['REQUEST_METHOD'] = 'PUT';
+ $request = new Request([
+ 'server' => [
+ 'CONTENT_LENGTH' => '123456',
+ ],
+ 'method' => 'PUT',
+ ], $this->requestId, $this->config, null);
$info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
- 'permissions' => \OCP\Constants::PERMISSION_ALL,
+ 'permissions' => Constants::PERMISSION_ALL,
'type' => FileInfo::TYPE_FOLDER,
], null);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+ $file = new File($view, $info, null, $request);
// action
$thrown = false;
@@ -901,8 +668,9 @@ class FileTest extends TestCase {
}
- public function testDeleteWhenAllowed() {
+ public function testDeleteWhenAllowed(): void {
// setup
+ /** @var View&MockObject */
$view = $this->getMockBuilder(View::class)
->getMock();
@@ -911,21 +679,22 @@ class FileTest extends TestCase {
->willReturn(true);
$info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
- 'permissions' => \OCP\Constants::PERMISSION_ALL,
+ 'permissions' => Constants::PERMISSION_ALL,
'type' => FileInfo::TYPE_FOLDER,
], null);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+ $file = new File($view, $info);
// action
$file->delete();
}
- public function testDeleteThrowsWhenDeletionNotAllowed() {
+ public function testDeleteThrowsWhenDeletionNotAllowed(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
// setup
+ /** @var View&MockObject */
$view = $this->getMockBuilder(View::class)
->getMock();
@@ -934,17 +703,18 @@ class FileTest extends TestCase {
'type' => FileInfo::TYPE_FOLDER,
], null);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+ $file = new File($view, $info);
// action
$file->delete();
}
- public function testDeleteThrowsWhenDeletionFailed() {
+ public function testDeleteThrowsWhenDeletionFailed(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
// setup
+ /** @var View&MockObject */
$view = $this->getMockBuilder(View::class)
->getMock();
@@ -954,21 +724,22 @@ class FileTest extends TestCase {
->willReturn(false);
$info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
- 'permissions' => \OCP\Constants::PERMISSION_ALL,
+ 'permissions' => Constants::PERMISSION_ALL,
'type' => FileInfo::TYPE_FOLDER,
], null);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+ $file = new File($view, $info);
// action
$file->delete();
}
- public function testDeleteThrowsWhenDeletionThrows() {
- $this->expectException(\OCA\DAV\Connector\Sabre\Exception\Forbidden::class);
+ public function testDeleteThrowsWhenDeletionThrows(): void {
+ $this->expectException(Forbidden::class);
// setup
+ /** @var View&MockObject */
$view = $this->getMockBuilder(View::class)
->getMock();
@@ -978,11 +749,11 @@ class FileTest extends TestCase {
->willThrowException(new ForbiddenException('', true));
$info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
- 'permissions' => \OCP\Constants::PERMISSION_ALL,
+ 'permissions' => Constants::PERMISSION_ALL,
'type' => FileInfo::TYPE_FOLDER,
], null);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+ $file = new File($view, $info);
// action
$file->delete();
@@ -1007,8 +778,8 @@ class FileTest extends TestCase {
/**
* Test whether locks are set before and after the operation
*/
- public function testPutLocking() {
- $view = new \OC\Files\View('/' . $this->user . '/files/');
+ public function testPutLocking(): void {
+ $view = new View('/' . $this->user . '/files/');
$path = 'test-locking.txt';
$info = new \OC\Files\FileInfo(
@@ -1016,27 +787,27 @@ class FileTest extends TestCase {
$this->getMockStorage(),
null,
[
- 'permissions' => \OCP\Constants::PERMISSION_ALL,
+ 'permissions' => Constants::PERMISSION_ALL,
'type' => FileInfo::TYPE_FOLDER,
],
null
);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+ $file = new File($view, $info);
$this->assertFalse(
- $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED),
+ $this->isFileLocked($view, $path, ILockingProvider::LOCK_SHARED),
'File unlocked before put'
);
$this->assertFalse(
- $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE),
+ $this->isFileLocked($view, $path, ILockingProvider::LOCK_EXCLUSIVE),
'File unlocked before put'
);
$wasLockedPre = false;
$wasLockedPost = false;
$eventHandler = $this->getMockBuilder(\stdclass::class)
- ->setMethods(['writeCallback', 'postWriteCallback'])
+ ->addMethods(['writeCallback', 'postWriteCallback'])
->getMock();
// both pre and post hooks might need access to the file,
@@ -1044,27 +815,27 @@ class FileTest extends TestCase {
$eventHandler->expects($this->once())
->method('writeCallback')
->willReturnCallback(
- function () use ($view, $path, &$wasLockedPre) {
- $wasLockedPre = $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED);
- $wasLockedPre = $wasLockedPre && !$this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE);
+ function () use ($view, $path, &$wasLockedPre): void {
+ $wasLockedPre = $this->isFileLocked($view, $path, ILockingProvider::LOCK_SHARED);
+ $wasLockedPre = $wasLockedPre && !$this->isFileLocked($view, $path, ILockingProvider::LOCK_EXCLUSIVE);
}
);
$eventHandler->expects($this->once())
->method('postWriteCallback')
->willReturnCallback(
- function () use ($view, $path, &$wasLockedPost) {
- $wasLockedPost = $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED);
- $wasLockedPost = $wasLockedPost && !$this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE);
+ function () use ($view, $path, &$wasLockedPost): void {
+ $wasLockedPost = $this->isFileLocked($view, $path, ILockingProvider::LOCK_SHARED);
+ $wasLockedPost = $wasLockedPost && !$this->isFileLocked($view, $path, ILockingProvider::LOCK_EXCLUSIVE);
}
);
- \OCP\Util::connectHook(
+ Util::connectHook(
Filesystem::CLASSNAME,
Filesystem::signal_write,
$eventHandler,
'writeCallback'
);
- \OCP\Util::connectHook(
+ Util::connectHook(
Filesystem::CLASSNAME,
Filesystem::signal_post_write,
$eventHandler,
@@ -1083,11 +854,11 @@ class FileTest extends TestCase {
$this->assertTrue($wasLockedPost, 'File was locked during post-hooks');
$this->assertFalse(
- $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED),
+ $this->isFileLocked($view, $path, ILockingProvider::LOCK_SHARED),
'File unlocked after put'
);
$this->assertFalse(
- $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE),
+ $this->isFileLocked($view, $path, ILockingProvider::LOCK_EXCLUSIVE),
'File unlocked after put'
);
}
@@ -1100,9 +871,9 @@ class FileTest extends TestCase {
*
* @return array list of part files
*/
- private function listPartFiles(\OC\Files\View $userView = null, $path = '') {
+ private function listPartFiles(?View $userView = null, $path = '') {
if ($userView === null) {
- $userView = \OC\Files\Filesystem::getView();
+ $userView = Filesystem::getView();
}
$files = [];
[$storage, $internalPath] = $userView->resolvePath($path);
@@ -1110,7 +881,7 @@ class FileTest extends TestCase {
$realPath = $storage->getSourcePath($internalPath);
$dh = opendir($realPath);
while (($file = readdir($dh)) !== false) {
- if (substr($file, strlen($file) - 5, 5) === '.part') {
+ if (str_ends_with($file, '.part')) {
$files[] = $file;
}
}
@@ -1126,81 +897,84 @@ class FileTest extends TestCase {
* @param View $userView
* @return array
*/
- private function getFileInfos($path = '', View $userView = null) {
+ private function getFileInfos($path = '', ?View $userView = null) {
if ($userView === null) {
$userView = Filesystem::getView();
}
return [
- "filesize" => $userView->filesize($path),
- "mtime" => $userView->filemtime($path),
- "filetype" => $userView->filetype($path),
- "mimetype" => $userView->getMimeType($path)
+ 'filesize' => $userView->filesize($path),
+ 'mtime' => $userView->filemtime($path),
+ 'filetype' => $userView->filetype($path),
+ 'mimetype' => $userView->getMimeType($path)
];
}
- public function testGetFopenFails() {
+ public function testGetFopenFails(): void {
$this->expectException(\Sabre\DAV\Exception\ServiceUnavailable::class);
+ /** @var View&MockObject */
$view = $this->getMockBuilder(View::class)
- ->setMethods(['fopen'])
+ ->onlyMethods(['fopen'])
->getMock();
$view->expects($this->atLeastOnce())
->method('fopen')
->willReturn(false);
$info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
- 'permissions' => \OCP\Constants::PERMISSION_ALL,
- 'type' => FileInfo::TYPE_FOLDER,
+ 'permissions' => Constants::PERMISSION_ALL,
+ 'type' => FileInfo::TYPE_FILE,
], null);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+ $file = new File($view, $info);
$file->get();
}
- public function testGetFopenThrows() {
- $this->expectException(\OCA\DAV\Connector\Sabre\Exception\Forbidden::class);
+ public function testGetFopenThrows(): void {
+ $this->expectException(Forbidden::class);
+ /** @var View&MockObject */
$view = $this->getMockBuilder(View::class)
- ->setMethods(['fopen'])
+ ->onlyMethods(['fopen'])
->getMock();
$view->expects($this->atLeastOnce())
->method('fopen')
->willThrowException(new ForbiddenException('', true));
$info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
- 'permissions' => \OCP\Constants::PERMISSION_ALL,
- 'type' => FileInfo::TYPE_FOLDER,
+ 'permissions' => Constants::PERMISSION_ALL,
+ 'type' => FileInfo::TYPE_FILE,
], null);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+ $file = new File($view, $info);
$file->get();
}
- public function testGetThrowsIfNoPermission() {
+ public function testGetThrowsIfNoPermission(): void {
$this->expectException(\Sabre\DAV\Exception\NotFound::class);
+ /** @var View&MockObject */
$view = $this->getMockBuilder(View::class)
- ->setMethods(['fopen'])
+ ->onlyMethods(['fopen'])
->getMock();
$view->expects($this->never())
->method('fopen');
$info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
- 'permissions' => \OCP\Constants::PERMISSION_CREATE, // no read perm
+ 'permissions' => Constants::PERMISSION_CREATE, // no read perm
'type' => FileInfo::TYPE_FOLDER,
], null);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+ $file = new File($view, $info);
$file->get();
}
- public function testSimplePutNoCreatePermissions() {
+ public function testSimplePutNoCreatePermissions(): void {
$this->logout();
$storage = new Temporary([]);
@@ -1231,8 +1005,8 @@ class FileTest extends TestCase {
$this->assertEquals('new content', $view->file_get_contents('root/file.txt'));
}
- public function testPutLockExpired() {
- $view = new \OC\Files\View('/' . $this->user . '/files/');
+ public function testPutLockExpired(): void {
+ $view = new View('/' . $this->user . '/files/');
$path = 'test-locking.txt';
$info = new \OC\Files\FileInfo(
@@ -1240,13 +1014,13 @@ class FileTest extends TestCase {
$this->getMockStorage(),
null,
[
- 'permissions' => \OCP\Constants::PERMISSION_ALL,
+ 'permissions' => Constants::PERMISSION_ALL,
'type' => FileInfo::TYPE_FOLDER,
],
null
);
- $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+ $file = new File($view, $info);
// don't lock before the PUT to simulate an expired shared lock
$this->assertNotEmpty($file->put($this->getStream('test data')));
diff --git a/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php
index 777a730ffd1..4df3accfda9 100644
--- a/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php
@@ -1,42 +1,25 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
- * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.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 Stefan Weil <sw@weilnetz.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\Tests\unit\Connector\Sabre;
+use OC\Accounts\Account;
+use OC\Accounts\AccountProperty;
use OC\User\User;
use OCA\DAV\Connector\Sabre\Directory;
+use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
use OCA\DAV\Connector\Sabre\File;
use OCA\DAV\Connector\Sabre\FilesPlugin;
use OCA\DAV\Connector\Sabre\Node;
+use OCP\Accounts\IAccountManager;
use OCP\Files\FileInfo;
+use OCP\Files\IFilenameValidator;
+use OCP\Files\InvalidPathException;
use OCP\Files\StorageNotAvailableException;
use OCP\IConfig;
use OCP\IPreview;
@@ -53,57 +36,19 @@ use Sabre\Xml\Service;
use Test\TestCase;
/**
- * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
- * This file is licensed under the Affero General Public License version 3 or
- * later.
- * See the COPYING-README file.
+ * @group DB
*/
class FilesPluginTest extends TestCase {
- public const GETETAG_PROPERTYNAME = FilesPlugin::GETETAG_PROPERTYNAME;
- public const FILEID_PROPERTYNAME = FilesPlugin::FILEID_PROPERTYNAME;
- public const INTERNAL_FILEID_PROPERTYNAME = FilesPlugin::INTERNAL_FILEID_PROPERTYNAME;
- public const SIZE_PROPERTYNAME = FilesPlugin::SIZE_PROPERTYNAME;
- public const PERMISSIONS_PROPERTYNAME = FilesPlugin::PERMISSIONS_PROPERTYNAME;
- public const LASTMODIFIED_PROPERTYNAME = FilesPlugin::LASTMODIFIED_PROPERTYNAME;
- public const CREATIONDATE_PROPERTYNAME = FilesPlugin::CREATIONDATE_PROPERTYNAME;
- public const DOWNLOADURL_PROPERTYNAME = FilesPlugin::DOWNLOADURL_PROPERTYNAME;
- public const OWNER_ID_PROPERTYNAME = FilesPlugin::OWNER_ID_PROPERTYNAME;
- public const OWNER_DISPLAY_NAME_PROPERTYNAME = FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME;
- public const DATA_FINGERPRINT_PROPERTYNAME = FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME;
- public const HAS_PREVIEW_PROPERTYNAME = FilesPlugin::HAS_PREVIEW_PROPERTYNAME;
-
- /**
- * @var \Sabre\DAV\Server | \PHPUnit\Framework\MockObject\MockObject
- */
- private $server;
-
- /**
- * @var \Sabre\DAV\Tree | \PHPUnit\Framework\MockObject\MockObject
- */
- private $tree;
-
- /**
- * @var FilesPlugin
- */
- private $plugin;
-
- /**
- * @var \OCP\IConfig | \PHPUnit\Framework\MockObject\MockObject
- */
- private $config;
-
- /**
- * @var \OCP\IRequest | \PHPUnit\Framework\MockObject\MockObject
- */
- private $request;
- /**
- * @var \OCP\IPreview | \PHPUnit\Framework\MockObject\MockObject
- */
- private $previewManager;
-
- /** @var IUserSession|MockObject */
- private $userSession;
+ private Tree&MockObject $tree;
+ private Server&MockObject $server;
+ private IConfig&MockObject $config;
+ private IRequest&MockObject $request;
+ private IPreview&MockObject $previewManager;
+ private IUserSession&MockObject $userSession;
+ private IFilenameValidator&MockObject $filenameValidator;
+ private IAccountManager&MockObject $accountManager;
+ private FilesPlugin $plugin;
protected function setUp(): void {
parent::setUp();
@@ -116,32 +61,28 @@ class FilesPluginTest extends TestCase {
$this->request = $this->createMock(IRequest::class);
$this->previewManager = $this->createMock(IPreview::class);
$this->userSession = $this->createMock(IUserSession::class);
+ $this->filenameValidator = $this->createMock(IFilenameValidator::class);
+ $this->accountManager = $this->createMock(IAccountManager::class);
$this->plugin = new FilesPlugin(
$this->tree,
$this->config,
$this->request,
$this->previewManager,
- $this->userSession
+ $this->userSession,
+ $this->filenameValidator,
+ $this->accountManager,
);
- $response = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $response = $this->createMock(ResponseInterface::class);
$this->server->httpResponse = $response;
$this->server->xml = new Service();
$this->plugin->initialize($this->server);
}
- /**
- * @param string $class
- * @return \PHPUnit\Framework\MockObject\MockObject
- */
- private function createTestNode($class, $path = '/dummypath') {
- $node = $this->getMockBuilder($class)
- ->disableOriginalConstructor()
- ->getMock();
+ private function createTestNode(string $class, string $path = '/dummypath'): MockObject {
+ $node = $this->createMock($class);
$node->expects($this->any())
->method('getId')
@@ -180,29 +121,28 @@ class FilesPluginTest extends TestCase {
return $node;
}
- public function testGetPropertiesForFile() {
- /** @var \OCA\DAV\Connector\Sabre\File | \PHPUnit\Framework\MockObject\MockObject $node */
- $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
+ public function testGetPropertiesForFile(): void {
+ /** @var File&MockObject $node */
+ $node = $this->createTestNode(File::class);
$propFind = new PropFind(
'/dummyPath',
[
- self::GETETAG_PROPERTYNAME,
- self::FILEID_PROPERTYNAME,
- self::INTERNAL_FILEID_PROPERTYNAME,
- self::SIZE_PROPERTYNAME,
- self::PERMISSIONS_PROPERTYNAME,
- self::DOWNLOADURL_PROPERTYNAME,
- self::OWNER_ID_PROPERTYNAME,
- self::OWNER_DISPLAY_NAME_PROPERTYNAME,
- self::DATA_FINGERPRINT_PROPERTYNAME,
- self::CREATIONDATE_PROPERTYNAME,
+ FilesPlugin::GETETAG_PROPERTYNAME,
+ FilesPlugin::FILEID_PROPERTYNAME,
+ FilesPlugin::INTERNAL_FILEID_PROPERTYNAME,
+ FilesPlugin::SIZE_PROPERTYNAME,
+ FilesPlugin::PERMISSIONS_PROPERTYNAME,
+ FilesPlugin::DOWNLOADURL_PROPERTYNAME,
+ FilesPlugin::OWNER_ID_PROPERTYNAME,
+ FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME,
+ FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME,
+ FilesPlugin::CREATIONDATE_PROPERTYNAME,
],
0
);
- $user = $this->getMockBuilder(User::class)
- ->disableOriginalConstructor()->getMock();
+ $user = $this->createMock(User::class);
$user
->expects($this->once())
->method('getUID')
@@ -212,6 +152,12 @@ class FilesPluginTest extends TestCase {
->method('getDisplayName')
->willReturn('M. Foo');
+ $owner = $this->createMock(Account::class);
+ $this->accountManager->expects($this->once())
+ ->method('getAccount')
+ ->with($user)
+ ->willReturn($owner);
+
$node->expects($this->once())
->method('getDirectDownload')
->willReturn(['url' => 'http://example.com/']);
@@ -219,70 +165,170 @@ class FilesPluginTest extends TestCase {
->method('getOwner')
->willReturn($user);
+ $displayNameProp = $this->createMock(AccountProperty::class);
+ $owner
+ ->expects($this->once())
+ ->method('getProperty')
+ ->with(IAccountManager::PROPERTY_DISPLAYNAME)
+ ->willReturn($displayNameProp);
+ $displayNameProp
+ ->expects($this->once())
+ ->method('getScope')
+ ->willReturn(IAccountManager::SCOPE_PUBLISHED);
+
+ $this->plugin->handleGetProperties(
+ $propFind,
+ $node
+ );
+
+ $this->assertEquals('"abc"', $propFind->get(FilesPlugin::GETETAG_PROPERTYNAME));
+ $this->assertEquals('00000123instanceid', $propFind->get(FilesPlugin::FILEID_PROPERTYNAME));
+ $this->assertEquals('123', $propFind->get(FilesPlugin::INTERNAL_FILEID_PROPERTYNAME));
+ $this->assertEquals('1973-11-29T21:33:09+00:00', $propFind->get(FilesPlugin::CREATIONDATE_PROPERTYNAME));
+ $this->assertEquals(0, $propFind->get(FilesPlugin::SIZE_PROPERTYNAME));
+ $this->assertEquals('DWCKMSR', $propFind->get(FilesPlugin::PERMISSIONS_PROPERTYNAME));
+ $this->assertEquals('http://example.com/', $propFind->get(FilesPlugin::DOWNLOADURL_PROPERTYNAME));
+ $this->assertEquals('foo', $propFind->get(FilesPlugin::OWNER_ID_PROPERTYNAME));
+ $this->assertEquals('M. Foo', $propFind->get(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME));
+ $this->assertEquals('my_fingerprint', $propFind->get(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME));
+ $this->assertEquals([], $propFind->get404Properties());
+ }
+
+ public function testGetDisplayNamePropertyWhenNotPublished(): void {
+ $node = $this->createTestNode(File::class);
+ $propFind = new PropFind(
+ '/dummyPath',
+ [
+ FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME,
+ ],
+ 0
+ );
+
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn(null);
+
+ $user = $this->createMock(User::class);
+
+ $user->expects($this->never())
+ ->method('getDisplayName');
+
+ $owner = $this->createMock(Account::class);
+ $this->accountManager->expects($this->once())
+ ->method('getAccount')
+ ->with($user)
+ ->willReturn($owner);
+
+ $node->expects($this->once())
+ ->method('getOwner')
+ ->willReturn($user);
+
+ $displayNameProp = $this->createMock(AccountProperty::class);
+ $owner
+ ->expects($this->once())
+ ->method('getProperty')
+ ->with(IAccountManager::PROPERTY_DISPLAYNAME)
+ ->willReturn($displayNameProp);
+ $displayNameProp
+ ->expects($this->once())
+ ->method('getScope')
+ ->willReturn(IAccountManager::SCOPE_PRIVATE);
+
+ $this->plugin->handleGetProperties(
+ $propFind,
+ $node
+ );
+
+ $this->assertEquals(null, $propFind->get(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME));
+ }
+
+ public function testGetDisplayNamePropertyWhenNotPublishedButLoggedIn(): void {
+ $node = $this->createTestNode(File::class);
+
+ $propFind = new PropFind(
+ '/dummyPath',
+ [
+ FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME,
+ ],
+ 0
+ );
+
+ $user = $this->createMock(User::class);
+
+ $node->expects($this->once())
+ ->method('getOwner')
+ ->willReturn($user);
+
+ $loggedInUser = $this->createMock(User::class);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($loggedInUser);
+
+ $user
+ ->expects($this->once())
+ ->method('getDisplayName')
+ ->willReturn('M. Foo');
+
+ $this->accountManager->expects($this->never())
+ ->method('getAccount');
+
$this->plugin->handleGetProperties(
$propFind,
$node
);
- $this->assertEquals('"abc"', $propFind->get(self::GETETAG_PROPERTYNAME));
- $this->assertEquals('00000123instanceid', $propFind->get(self::FILEID_PROPERTYNAME));
- $this->assertEquals('123', $propFind->get(self::INTERNAL_FILEID_PROPERTYNAME));
- $this->assertEquals('1973-11-29T21:33:09+00:00', $propFind->get(self::CREATIONDATE_PROPERTYNAME));
- $this->assertEquals(null, $propFind->get(self::SIZE_PROPERTYNAME));
- $this->assertEquals('DWCKMSR', $propFind->get(self::PERMISSIONS_PROPERTYNAME));
- $this->assertEquals('http://example.com/', $propFind->get(self::DOWNLOADURL_PROPERTYNAME));
- $this->assertEquals('foo', $propFind->get(self::OWNER_ID_PROPERTYNAME));
- $this->assertEquals('M. Foo', $propFind->get(self::OWNER_DISPLAY_NAME_PROPERTYNAME));
- $this->assertEquals('my_fingerprint', $propFind->get(self::DATA_FINGERPRINT_PROPERTYNAME));
- $this->assertEquals([self::SIZE_PROPERTYNAME], $propFind->get404Properties());
+ $this->assertEquals('M. Foo', $propFind->get(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME));
}
- public function testGetPropertiesStorageNotAvailable() {
- /** @var \OCA\DAV\Connector\Sabre\File | \PHPUnit\Framework\MockObject\MockObject $node */
- $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
+ public function testGetPropertiesStorageNotAvailable(): void {
+ /** @var File&MockObject $node */
+ $node = $this->createTestNode(File::class);
$propFind = new PropFind(
'/dummyPath',
[
- self::DOWNLOADURL_PROPERTYNAME,
+ FilesPlugin::DOWNLOADURL_PROPERTYNAME,
],
0
);
$node->expects($this->once())
->method('getDirectDownload')
- ->will($this->throwException(new StorageNotAvailableException()));
+ ->willThrowException(new StorageNotAvailableException());
$this->plugin->handleGetProperties(
$propFind,
$node
);
- $this->assertEquals(null, $propFind->get(self::DOWNLOADURL_PROPERTYNAME));
+ $this->assertEquals(null, $propFind->get(FilesPlugin::DOWNLOADURL_PROPERTYNAME));
}
- public function testGetPublicPermissions() {
+ public function testGetPublicPermissions(): void {
+ /** @var IRequest&MockObject */
+ $request = $this->createMock(IRequest::class);
$this->plugin = new FilesPlugin(
$this->tree,
$this->config,
- $this->getMockBuilder(IRequest::class)
- ->disableOriginalConstructor()
- ->getMock(),
+ $request,
$this->previewManager,
$this->userSession,
- true);
+ $this->filenameValidator,
+ $this->accountManager,
+ true,
+ );
$this->plugin->initialize($this->server);
$propFind = new PropFind(
'/dummyPath',
[
- self::PERMISSIONS_PROPERTYNAME,
+ FilesPlugin::PERMISSIONS_PROPERTYNAME,
],
0
);
- /** @var \OCA\DAV\Connector\Sabre\File | \PHPUnit\Framework\MockObject\MockObject $node */
- $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
+ /** @var File&MockObject $node */
+ $node = $this->createTestNode(File::class);
$node->expects($this->any())
->method('getDavPermissions')
->willReturn('DWCKMSR');
@@ -292,22 +338,22 @@ class FilesPluginTest extends TestCase {
$node
);
- $this->assertEquals('DWCKR', $propFind->get(self::PERMISSIONS_PROPERTYNAME));
+ $this->assertEquals('DWCKR', $propFind->get(FilesPlugin::PERMISSIONS_PROPERTYNAME));
}
- public function testGetPropertiesForDirectory() {
- /** @var \OCA\DAV\Connector\Sabre\Directory | \PHPUnit\Framework\MockObject\MockObject $node */
- $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\Directory');
+ public function testGetPropertiesForDirectory(): void {
+ /** @var Directory&MockObject $node */
+ $node = $this->createTestNode(Directory::class);
$propFind = new PropFind(
'/dummyPath',
[
- self::GETETAG_PROPERTYNAME,
- self::FILEID_PROPERTYNAME,
- self::SIZE_PROPERTYNAME,
- self::PERMISSIONS_PROPERTYNAME,
- self::DOWNLOADURL_PROPERTYNAME,
- self::DATA_FINGERPRINT_PROPERTYNAME,
+ FilesPlugin::GETETAG_PROPERTYNAME,
+ FilesPlugin::FILEID_PROPERTYNAME,
+ FilesPlugin::SIZE_PROPERTYNAME,
+ FilesPlugin::PERMISSIONS_PROPERTYNAME,
+ FilesPlugin::DOWNLOADURL_PROPERTYNAME,
+ FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME,
],
0
);
@@ -321,20 +367,18 @@ class FilesPluginTest extends TestCase {
$node
);
- $this->assertEquals('"abc"', $propFind->get(self::GETETAG_PROPERTYNAME));
- $this->assertEquals('00000123instanceid', $propFind->get(self::FILEID_PROPERTYNAME));
- $this->assertEquals(1025, $propFind->get(self::SIZE_PROPERTYNAME));
- $this->assertEquals('DWCKMSR', $propFind->get(self::PERMISSIONS_PROPERTYNAME));
- $this->assertEquals(null, $propFind->get(self::DOWNLOADURL_PROPERTYNAME));
- $this->assertEquals('my_fingerprint', $propFind->get(self::DATA_FINGERPRINT_PROPERTYNAME));
- $this->assertEquals([self::DOWNLOADURL_PROPERTYNAME], $propFind->get404Properties());
+ $this->assertEquals('"abc"', $propFind->get(FilesPlugin::GETETAG_PROPERTYNAME));
+ $this->assertEquals('00000123instanceid', $propFind->get(FilesPlugin::FILEID_PROPERTYNAME));
+ $this->assertEquals(1025, $propFind->get(FilesPlugin::SIZE_PROPERTYNAME));
+ $this->assertEquals('DWCKMSR', $propFind->get(FilesPlugin::PERMISSIONS_PROPERTYNAME));
+ $this->assertEquals(null, $propFind->get(FilesPlugin::DOWNLOADURL_PROPERTYNAME));
+ $this->assertEquals('my_fingerprint', $propFind->get(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME));
+ $this->assertEquals([FilesPlugin::DOWNLOADURL_PROPERTYNAME], $propFind->get404Properties());
}
- public function testGetPropertiesForRootDirectory() {
- /** @var \OCA\DAV\Connector\Sabre\Directory|\PHPUnit\Framework\MockObject\MockObject $node */
- $node = $this->getMockBuilder(Directory::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testGetPropertiesForRootDirectory(): void {
+ /** @var Directory&MockObject $node */
+ $node = $this->createMock(Directory::class);
$node->expects($this->any())->method('getPath')->willReturn('/');
$fileInfo = $this->createMock(FileInfo::class);
@@ -349,7 +393,7 @@ class FilesPluginTest extends TestCase {
$propFind = new PropFind(
'/',
[
- self::DATA_FINGERPRINT_PROPERTYNAME,
+ FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME,
],
0
);
@@ -359,18 +403,15 @@ class FilesPluginTest extends TestCase {
$node
);
- $this->assertEquals('my_fingerprint', $propFind->get(self::DATA_FINGERPRINT_PROPERTYNAME));
+ $this->assertEquals('my_fingerprint', $propFind->get(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME));
}
- public function testGetPropertiesWhenNoPermission() {
+ public function testGetPropertiesWhenNoPermission(): void {
// No read permissions can be caused by files access control.
// But we still want to load the directory list, so this is okay for us.
// $this->expectException(\Sabre\DAV\Exception\NotFound::class);
-
- /** @var \OCA\DAV\Connector\Sabre\Directory|\PHPUnit\Framework\MockObject\MockObject $node */
- $node = $this->getMockBuilder(Directory::class)
- ->disableOriginalConstructor()
- ->getMock();
+ /** @var Directory&MockObject $node */
+ $node = $this->createMock(Directory::class);
$node->expects($this->any())->method('getPath')->willReturn('/');
$fileInfo = $this->createMock(FileInfo::class);
@@ -385,7 +426,7 @@ class FilesPluginTest extends TestCase {
$propFind = new PropFind(
'/test',
[
- self::DATA_FINGERPRINT_PROPERTYNAME,
+ FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME,
],
0
);
@@ -398,8 +439,8 @@ class FilesPluginTest extends TestCase {
$this->addToAssertionCount(1);
}
- public function testUpdateProps() {
- $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
+ public function testUpdateProps(): void {
+ $node = $this->createTestNode(File::class);
$testDate = 'Fri, 13 Feb 2015 00:01:02 GMT';
$testCreationDate = '2007-08-31T16:47+00:00';
@@ -419,11 +460,12 @@ class FilesPluginTest extends TestCase {
// properties to set
$propPatch = new PropPatch([
- self::GETETAG_PROPERTYNAME => 'newetag',
- self::LASTMODIFIED_PROPERTYNAME => $testDate,
- self::CREATIONDATE_PROPERTYNAME => $testCreationDate,
+ FilesPlugin::GETETAG_PROPERTYNAME => 'newetag',
+ FilesPlugin::LASTMODIFIED_PROPERTYNAME => $testDate,
+ FilesPlugin::CREATIONDATE_PROPERTYNAME => $testCreationDate,
]);
+
$this->plugin->handleUpdateProperties(
'/dummypath',
$propPatch
@@ -434,19 +476,19 @@ class FilesPluginTest extends TestCase {
$this->assertEmpty($propPatch->getRemainingMutations());
$result = $propPatch->getResult();
- $this->assertEquals(200, $result[self::LASTMODIFIED_PROPERTYNAME]);
- $this->assertEquals(200, $result[self::GETETAG_PROPERTYNAME]);
- $this->assertEquals(200, $result[self::CREATIONDATE_PROPERTYNAME]);
+ $this->assertEquals(200, $result[FilesPlugin::LASTMODIFIED_PROPERTYNAME]);
+ $this->assertEquals(200, $result[FilesPlugin::GETETAG_PROPERTYNAME]);
+ $this->assertEquals(200, $result[FilesPlugin::CREATIONDATE_PROPERTYNAME]);
}
- public function testUpdatePropsForbidden() {
+ public function testUpdatePropsForbidden(): void {
$propPatch = new PropPatch([
- self::OWNER_ID_PROPERTYNAME => 'user2',
- self::OWNER_DISPLAY_NAME_PROPERTYNAME => 'User Two',
- self::FILEID_PROPERTYNAME => 12345,
- self::PERMISSIONS_PROPERTYNAME => 'C',
- self::SIZE_PROPERTYNAME => 123,
- self::DOWNLOADURL_PROPERTYNAME => 'http://example.com/',
+ FilesPlugin::OWNER_ID_PROPERTYNAME => 'user2',
+ FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME => 'User Two',
+ FilesPlugin::FILEID_PROPERTYNAME => 12345,
+ FilesPlugin::PERMISSIONS_PROPERTYNAME => 'C',
+ FilesPlugin::SIZE_PROPERTYNAME => 123,
+ FilesPlugin::DOWNLOADURL_PROPERTYNAME => 'http://example.com/',
]);
$this->plugin->handleUpdateProperties(
@@ -459,16 +501,16 @@ class FilesPluginTest extends TestCase {
$this->assertEmpty($propPatch->getRemainingMutations());
$result = $propPatch->getResult();
- $this->assertEquals(403, $result[self::OWNER_ID_PROPERTYNAME]);
- $this->assertEquals(403, $result[self::OWNER_DISPLAY_NAME_PROPERTYNAME]);
- $this->assertEquals(403, $result[self::FILEID_PROPERTYNAME]);
- $this->assertEquals(403, $result[self::PERMISSIONS_PROPERTYNAME]);
- $this->assertEquals(403, $result[self::SIZE_PROPERTYNAME]);
- $this->assertEquals(403, $result[self::DOWNLOADURL_PROPERTYNAME]);
+ $this->assertEquals(403, $result[FilesPlugin::OWNER_ID_PROPERTYNAME]);
+ $this->assertEquals(403, $result[FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME]);
+ $this->assertEquals(403, $result[FilesPlugin::FILEID_PROPERTYNAME]);
+ $this->assertEquals(403, $result[FilesPlugin::PERMISSIONS_PROPERTYNAME]);
+ $this->assertEquals(403, $result[FilesPlugin::SIZE_PROPERTYNAME]);
+ $this->assertEquals(403, $result[FilesPlugin::DOWNLOADURL_PROPERTYNAME]);
}
/**
- * Testcase from https://github.com/owncloud/core/issues/5251
+ * Test case from https://github.com/owncloud/core/issues/5251
*
* |-FolderA
* |-text.txt
@@ -478,70 +520,126 @@ class FilesPluginTest extends TestCase {
* Thus moving /FolderA/test.txt to /test.txt should fail already on that check
*
*/
- public function testMoveSrcNotDeletable() {
+ public function testMoveSrcNotDeletable(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$this->expectExceptionMessage('FolderA/test.txt cannot be deleted');
- $fileInfoFolderATestTXT = $this->getMockBuilder(FileInfo::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $fileInfoFolderATestTXT = $this->createMock(FileInfo::class);
$fileInfoFolderATestTXT->expects($this->once())
->method('isDeletable')
->willReturn(false);
- $node = $this->getMockBuilder(Node::class)
- ->disableOriginalConstructor()
- ->getMock();
- $node->expects($this->once())
+ $node = $this->createMock(Node::class);
+ $node->expects($this->atLeastOnce())
->method('getFileInfo')
->willReturn($fileInfoFolderATestTXT);
- $this->tree->expects($this->once())->method('getNodeForPath')
+ $this->tree->expects($this->atLeastOnce())
+ ->method('getNodeForPath')
->willReturn($node);
$this->plugin->checkMove('FolderA/test.txt', 'test.txt');
}
- public function testMoveSrcDeletable() {
- $fileInfoFolderATestTXT = $this->getMockBuilder(FileInfo::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testMoveSrcDeletable(): void {
+ $fileInfoFolderATestTXT = $this->createMock(FileInfo::class);
$fileInfoFolderATestTXT->expects($this->once())
->method('isDeletable')
->willReturn(true);
- $node = $this->getMockBuilder(Node::class)
- ->disableOriginalConstructor()
- ->getMock();
- $node->expects($this->once())
+ $node = $this->createMock(Node::class);
+ $node->expects($this->atLeastOnce())
->method('getFileInfo')
->willReturn($fileInfoFolderATestTXT);
- $this->tree->expects($this->once())->method('getNodeForPath')
+ $this->tree->expects($this->atLeastOnce())
+ ->method('getNodeForPath')
->willReturn($node);
$this->plugin->checkMove('FolderA/test.txt', 'test.txt');
}
-
- public function testMoveSrcNotExist() {
+ public function testMoveSrcNotExist(): void {
$this->expectException(\Sabre\DAV\Exception\NotFound::class);
$this->expectExceptionMessage('FolderA/test.txt does not exist');
- $node = $this->getMockBuilder(Node::class)
- ->disableOriginalConstructor()
- ->getMock();
- $node->expects($this->once())
+ $node = $this->createMock(Node::class);
+ $node->expects($this->atLeastOnce())
->method('getFileInfo')
->willReturn(null);
- $this->tree->expects($this->once())->method('getNodeForPath')
+ $this->tree->expects($this->atLeastOnce())
+ ->method('getNodeForPath')
->willReturn($node);
$this->plugin->checkMove('FolderA/test.txt', 'test.txt');
}
- public function downloadHeadersProvider() {
+ public function testMoveDestinationInvalid(): void {
+ $this->expectException(InvalidPath::class);
+ $this->expectExceptionMessage('Mocked exception');
+
+ $fileInfoFolderATestTXT = $this->createMock(FileInfo::class);
+ $fileInfoFolderATestTXT->expects(self::any())
+ ->method('isDeletable')
+ ->willReturn(true);
+
+ $node = $this->createMock(Node::class);
+ $node->expects($this->atLeastOnce())
+ ->method('getFileInfo')
+ ->willReturn($fileInfoFolderATestTXT);
+
+ $this->tree->expects($this->atLeastOnce())
+ ->method('getNodeForPath')
+ ->willReturn($node);
+
+ $this->filenameValidator->expects(self::once())
+ ->method('validateFilename')
+ ->with('invalid\\path.txt')
+ ->willThrowException(new InvalidPathException('Mocked exception'));
+
+ $this->plugin->checkMove('FolderA/test.txt', 'invalid\\path.txt');
+ }
+
+ public function testCopySrcNotExist(): void {
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+ $this->expectExceptionMessage('FolderA/test.txt does not exist');
+
+ $node = $this->createMock(Node::class);
+ $node->expects($this->atLeastOnce())
+ ->method('getFileInfo')
+ ->willReturn(null);
+
+ $this->tree->expects($this->atLeastOnce())
+ ->method('getNodeForPath')
+ ->willReturn($node);
+
+ $this->plugin->checkCopy('FolderA/test.txt', 'test.txt');
+ }
+
+ public function testCopyDestinationInvalid(): void {
+ $this->expectException(InvalidPath::class);
+ $this->expectExceptionMessage('Mocked exception');
+
+ $fileInfoFolderATestTXT = $this->createMock(FileInfo::class);
+ $node = $this->createMock(Node::class);
+ $node->expects($this->atLeastOnce())
+ ->method('getFileInfo')
+ ->willReturn($fileInfoFolderATestTXT);
+
+ $this->tree->expects($this->atLeastOnce())
+ ->method('getNodeForPath')
+ ->willReturn($node);
+
+ $this->filenameValidator->expects(self::once())
+ ->method('validateFilename')
+ ->with('invalid\\path.txt')
+ ->willThrowException(new InvalidPathException('Mocked exception'));
+
+ $this->plugin->checkCopy('FolderA/test.txt', 'invalid\\path.txt');
+ }
+
+ public static function downloadHeadersProvider(): array {
return [
[
false,
@@ -554,25 +652,17 @@ class FilesPluginTest extends TestCase {
];
}
- /**
- * @dataProvider downloadHeadersProvider
- */
- public function testDownloadHeaders($isClumsyAgent, $contentDispositionHeader) {
- $request = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ #[\PHPUnit\Framework\Attributes\DataProvider('downloadHeadersProvider')]
+ public function testDownloadHeaders(bool $isClumsyAgent, string $contentDispositionHeader): void {
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
$request
->expects($this->once())
->method('getPath')
->willReturn('test/somefile.xml');
- $node = $this->getMockBuilder(File::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $node = $this->createMock(File::class);
$node
->expects($this->once())
->method('getName')
@@ -589,25 +679,29 @@ class FilesPluginTest extends TestCase {
->method('isUserAgent')
->willReturn($isClumsyAgent);
+ $calls = [
+ ['Content-Disposition', $contentDispositionHeader],
+ ['X-Accel-Buffering', 'no'],
+ ];
$response
- ->expects($this->exactly(2))
+ ->expects($this->exactly(count($calls)))
->method('addHeader')
- ->withConsecutive(
- ['Content-Disposition', $contentDispositionHeader],
- ['X-Accel-Buffering', 'no']
- );
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertSame($expected, func_get_args());
+ });
$this->plugin->httpGet($request, $response);
}
- public function testHasPreview() {
- /** @var \OCA\DAV\Connector\Sabre\Directory | \PHPUnit\Framework\MockObject\MockObject $node */
- $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\Directory');
+ public function testHasPreview(): void {
+ /** @var Directory&MockObject $node */
+ $node = $this->createTestNode(Directory::class);
$propFind = new PropFind(
'/dummyPath',
[
- self::HAS_PREVIEW_PROPERTYNAME
+ FilesPlugin::HAS_PREVIEW_PROPERTYNAME
],
0
);
@@ -621,6 +715,6 @@ class FilesPluginTest extends TestCase {
$node
);
- $this->assertEquals("false", $propFind->get(self::HAS_PREVIEW_PROPERTYNAME));
+ $this->assertEquals('false', $propFind->get(FilesPlugin::HAS_PREVIEW_PROPERTYNAME));
}
}
diff --git a/apps/dav/tests/unit/Connector/Sabre/FilesReportPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/FilesReportPluginTest.php
index f73434b33b6..176949f999c 100644
--- a/apps/dav/tests/unit/Connector/Sabre/FilesReportPluginTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/FilesReportPluginTest.php
@@ -1,38 +1,23 @@
<?php
+
+declare(strict_types=1);
/**
- * @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 Robin Appelman <robin@icewind.nl>
- * @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\Tests\unit\Connector\Sabre;
use OC\Files\View;
use OCA\DAV\Connector\Sabre\Directory;
+use OCA\DAV\Connector\Sabre\FilesPlugin;
use OCA\DAV\Connector\Sabre\FilesReportPlugin as FilesReportPluginImplementation;
+use OCP\Accounts\IAccountManager;
use OCP\App\IAppManager;
use OCP\Files\File;
use OCP\Files\FileInfo;
use OCP\Files\Folder;
+use OCP\Files\IFilenameValidator;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IPreview;
@@ -44,95 +29,59 @@ use OCP\IUserSession;
use OCP\SystemTag\ISystemTag;
use OCP\SystemTag\ISystemTagManager;
use OCP\SystemTag\ISystemTagObjectMapper;
+use OCP\SystemTag\TagNotFoundException;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\INode;
+use Sabre\DAV\Server;
use Sabre\DAV\Tree;
use Sabre\HTTP\ResponseInterface;
class FilesReportPluginTest extends \Test\TestCase {
- /** @var \Sabre\DAV\Server|\PHPUnit\Framework\MockObject\MockObject */
- private $server;
-
- /** @var \Sabre\DAV\Tree|\PHPUnit\Framework\MockObject\MockObject */
- private $tree;
-
- /** @var ISystemTagObjectMapper|\PHPUnit\Framework\MockObject\MockObject */
- private $tagMapper;
-
- /** @var ISystemTagManager|\PHPUnit\Framework\MockObject\MockObject */
- private $tagManager;
-
- /** @var ITags|\PHPUnit\Framework\MockObject\MockObject */
- private $privateTags;
-
- /** @var \OCP\IUserSession */
- private $userSession;
-
- /** @var FilesReportPluginImplementation */
- private $plugin;
- /** @var View|\PHPUnit\Framework\MockObject\MockObject **/
- private $view;
-
- /** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject **/
- private $groupManager;
-
- /** @var Folder|\PHPUnit\Framework\MockObject\MockObject **/
- private $userFolder;
-
- /** @var IPreview|\PHPUnit\Framework\MockObject\MockObject * */
- private $previewManager;
-
- /** @var IAppManager|\PHPUnit\Framework\MockObject\MockObject * */
- private $appManager;
+ private \Sabre\DAV\Server&MockObject $server;
+ private Tree&MockObject $tree;
+ private ISystemTagObjectMapper&MockObject $tagMapper;
+ private ISystemTagManager&MockObject $tagManager;
+ private ITags&MockObject $privateTags;
+ private ITagManager&MockObject $privateTagManager;
+ private IUserSession&MockObject $userSession;
+ private FilesReportPluginImplementation $plugin;
+ private View&MockObject $view;
+ private IGroupManager&MockObject $groupManager;
+ private Folder&MockObject $userFolder;
+ private IPreview&MockObject $previewManager;
+ private IAppManager&MockObject $appManager;
protected function setUp(): void {
parent::setUp();
- $this->tree = $this->getMockBuilder(Tree::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->view = $this->getMockBuilder(View::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $this->tree = $this->createMock(Tree::class);
+ $this->view = $this->createMock(View::class);
- $this->server = $this->getMockBuilder('\Sabre\DAV\Server')
+ $this->server = $this->getMockBuilder(Server::class)
->setConstructorArgs([$this->tree])
- ->setMethods(['getRequestUri', 'getBaseUri'])
+ ->onlyMethods(['getRequestUri', 'getBaseUri'])
->getMock();
$this->server->expects($this->any())
->method('getBaseUri')
->willReturn('http://example.com/owncloud/remote.php/dav');
- $this->groupManager = $this->getMockBuilder(IGroupManager::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->userFolder = $this->getMockBuilder(Folder::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->previewManager = $this->getMockBuilder(IPreview::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->appManager = $this->getMockBuilder(IAppManager::class)
- ->disableOriginalConstructor()
- ->getMock();
-
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->userFolder = $this->createMock(Folder::class);
+ $this->previewManager = $this->createMock(IPreview::class);
+ $this->appManager = $this->createMock(IAppManager::class);
$this->tagManager = $this->createMock(ISystemTagManager::class);
$this->tagMapper = $this->createMock(ISystemTagObjectMapper::class);
$this->userSession = $this->createMock(IUserSession::class);
$this->privateTags = $this->createMock(ITags::class);
- $privateTagManager = $this->createMock(ITagManager::class);
- $privateTagManager->expects($this->any())
+ $this->privateTagManager = $this->createMock(ITagManager::class);
+ $this->privateTagManager->expects($this->any())
->method('load')
->with('files')
->willReturn($this->privateTags);
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $user = $this->createMock(IUser::class);
$user->expects($this->any())
->method('getUID')
->willReturn('testuser');
@@ -145,7 +94,7 @@ class FilesReportPluginTest extends \Test\TestCase {
$this->view,
$this->tagManager,
$this->tagMapper,
- $privateTagManager,
+ $this->privateTagManager,
$this->userSession,
$this->groupManager,
$this->userFolder,
@@ -153,17 +102,13 @@ class FilesReportPluginTest extends \Test\TestCase {
);
}
- public function testOnReportInvalidNode() {
+ public function testOnReportInvalidNode(): void {
$path = 'totally/unrelated/13';
$this->tree->expects($this->any())
->method('getNodeForPath')
->with('/' . $path)
- ->willReturn(
- $this->getMockBuilder(INode::class)
- ->disableOriginalConstructor()
- ->getMock()
- );
+ ->willReturn($this->createMock(INode::class));
$this->server->expects($this->any())
->method('getRequestUri')
@@ -173,7 +118,7 @@ class FilesReportPluginTest extends \Test\TestCase {
$this->assertNull($this->plugin->onReport(FilesReportPluginImplementation::REPORT_NAME, [], '/' . $path));
}
- public function testOnReportInvalidReportName() {
+ public function testOnReportInvalidReportName(): void {
$path = 'test';
$this->tree->expects($this->any())
@@ -193,7 +138,7 @@ class FilesReportPluginTest extends \Test\TestCase {
$this->assertNull($this->plugin->onReport('{whoever}whatever', [], '/' . $path));
}
- public function testOnReport() {
+ public function testOnReport(): void {
$path = 'test';
$parameters = [
@@ -217,25 +162,12 @@ class FilesReportPluginTest extends \Test\TestCase {
->method('isAdmin')
->willReturn(true);
- $this->tagMapper->expects($this->at(0))
- ->method('getObjectIdsForTags')
- ->with('123', 'files')
- ->willReturn(['111', '222']);
- $this->tagMapper->expects($this->at(1))
- ->method('getObjectIdsForTags')
- ->with('456', 'files')
- ->willReturn(['111', '222', '333']);
-
- $reportTargetNode = $this->getMockBuilder(Directory::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $reportTargetNode = $this->createMock(Directory::class);
$reportTargetNode->expects($this->any())
->method('getPath')
->willReturn('');
- $response = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $response = $this->createMock(ResponseInterface::class);
$response->expects($this->once())
->method('setHeader')
@@ -253,21 +185,41 @@ class FilesReportPluginTest extends \Test\TestCase {
->with('/' . $path)
->willReturn($reportTargetNode);
- $filesNode1 = $this->getMockBuilder(Folder::class)
- ->disableOriginalConstructor()
- ->getMock();
- $filesNode2 = $this->getMockBuilder(File::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $filesNode1 = $this->createMock(File::class);
+ $filesNode1->expects($this->any())
+ ->method('getSize')
+ ->willReturn(12);
+ $filesNode2 = $this->createMock(Folder::class);
+ $filesNode2->expects($this->any())
+ ->method('getSize')
+ ->willReturn(10);
+
+ $tag123 = $this->createMock(ISystemTag::class);
+ $tag123->expects($this->any())
+ ->method('getName')
+ ->willReturn('OneTwoThree');
+ $tag123->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+ $tag456 = $this->createMock(ISystemTag::class);
+ $tag456->expects($this->any())
+ ->method('getName')
+ ->willReturn('FourFiveSix');
+ $tag456->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
- $this->userFolder->expects($this->at(0))
- ->method('getById')
- ->with('111')
- ->willReturn([$filesNode1]);
- $this->userFolder->expects($this->at(1))
- ->method('getById')
- ->with('222')
- ->willReturn([$filesNode2]);
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['123', '456'])
+ ->willReturn([$tag123, $tag456]);
+
+ $this->userFolder->expects($this->exactly(2))
+ ->method('searchBySystemTag')
+ ->willReturnMap([
+ ['OneTwoThree', 'testuser', 0, 0, [$filesNode1]],
+ ['FourFiveSix', 'testuser', 0, 0, [$filesNode2]],
+ ]);
$this->server->expects($this->any())
->method('getRequestUri')
@@ -278,110 +230,88 @@ class FilesReportPluginTest extends \Test\TestCase {
$this->assertFalse($this->plugin->onReport(FilesReportPluginImplementation::REPORT_NAME, $parameters, '/' . $path));
}
- public function testFindNodesByFileIdsRoot() {
- $filesNode1 = $this->getMockBuilder(Folder::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testFindNodesByFileIdsRoot(): void {
+ $filesNode1 = $this->createMock(Folder::class);
$filesNode1->expects($this->once())
->method('getName')
->willReturn('first node');
- $filesNode2 = $this->getMockBuilder(File::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $filesNode2 = $this->createMock(File::class);
$filesNode2->expects($this->once())
->method('getName')
->willReturn('second node');
- $reportTargetNode = $this->getMockBuilder(Directory::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $reportTargetNode = $this->createMock(Directory::class);
$reportTargetNode->expects($this->any())
->method('getPath')
->willReturn('/');
- $this->userFolder->expects($this->at(0))
- ->method('getById')
- ->with('111')
- ->willReturn([$filesNode1]);
- $this->userFolder->expects($this->at(1))
- ->method('getById')
- ->with('222')
- ->willReturn([$filesNode2]);
+ $this->userFolder->expects($this->exactly(2))
+ ->method('getFirstNodeById')
+ ->willReturnMap([
+ [111, $filesNode1],
+ [222, $filesNode2],
+ ]);
- /** @var \OCA\DAV\Connector\Sabre\Directory|\PHPUnit\Framework\MockObject\MockObject $reportTargetNode */
+ /** @var Directory&MockObject $reportTargetNode */
$result = $this->plugin->findNodesByFileIds($reportTargetNode, ['111', '222']);
$this->assertCount(2, $result);
- $this->assertInstanceOf('\OCA\DAV\Connector\Sabre\Directory', $result[0]);
+ $this->assertInstanceOf(Directory::class, $result[0]);
$this->assertEquals('first node', $result[0]->getName());
- $this->assertInstanceOf('\OCA\DAV\Connector\Sabre\File', $result[1]);
+ $this->assertInstanceOf(\OCA\DAV\Connector\Sabre\File::class, $result[1]);
$this->assertEquals('second node', $result[1]->getName());
}
- public function testFindNodesByFileIdsSubDir() {
- $filesNode1 = $this->getMockBuilder(Folder::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testFindNodesByFileIdsSubDir(): void {
+ $filesNode1 = $this->createMock(Folder::class);
$filesNode1->expects($this->once())
->method('getName')
->willReturn('first node');
- $filesNode2 = $this->getMockBuilder(File::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $filesNode2 = $this->createMock(File::class);
$filesNode2->expects($this->once())
->method('getName')
->willReturn('second node');
- $reportTargetNode = $this->getMockBuilder(Directory::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $reportTargetNode = $this->createMock(Directory::class);
$reportTargetNode->expects($this->any())
->method('getPath')
->willReturn('/sub1/sub2');
- $subNode = $this->getMockBuilder(Folder::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $subNode = $this->createMock(Folder::class);
- $this->userFolder->expects($this->at(0))
+ $this->userFolder->expects($this->once())
->method('get')
->with('/sub1/sub2')
->willReturn($subNode);
- $subNode->expects($this->at(0))
- ->method('getById')
- ->with('111')
- ->willReturn([$filesNode1]);
- $subNode->expects($this->at(1))
- ->method('getById')
- ->with('222')
- ->willReturn([$filesNode2]);
+ $subNode->expects($this->exactly(2))
+ ->method('getFirstNodeById')
+ ->willReturnMap([
+ [111, $filesNode1],
+ [222, $filesNode2],
+ ]);
- /** @var \OCA\DAV\Connector\Sabre\Directory|\PHPUnit\Framework\MockObject\MockObject $reportTargetNode */
+ /** @var Directory&MockObject $reportTargetNode */
$result = $this->plugin->findNodesByFileIds($reportTargetNode, ['111', '222']);
$this->assertCount(2, $result);
- $this->assertInstanceOf('\OCA\DAV\Connector\Sabre\Directory', $result[0]);
+ $this->assertInstanceOf(Directory::class, $result[0]);
$this->assertEquals('first node', $result[0]->getName());
- $this->assertInstanceOf('\OCA\DAV\Connector\Sabre\File', $result[1]);
+ $this->assertInstanceOf(\OCA\DAV\Connector\Sabre\File::class, $result[1]);
$this->assertEquals('second node', $result[1]->getName());
}
- public function testPrepareResponses() {
+ public function testPrepareResponses(): void {
$requestedProps = ['{DAV:}getcontentlength', '{http://owncloud.org/ns}fileid', '{DAV:}resourcetype'];
$fileInfo = $this->createMock(FileInfo::class);
$fileInfo->method('isReadable')->willReturn(true);
- $node1 = $this->getMockBuilder(Directory::class)
- ->disableOriginalConstructor()
- ->getMock();
- $node2 = $this->getMockBuilder(\OCA\DAV\Connector\Sabre\File::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $node1 = $this->createMock(Directory::class);
+ $node2 = $this->createMock(\OCA\DAV\Connector\Sabre\File::class);
$node1->expects($this->once())
->method('getInternalFileId')
@@ -401,17 +331,19 @@ class FilesReportPluginTest extends \Test\TestCase {
->willReturn('/sub/node2');
$node2->method('getFileInfo')->willReturn($fileInfo);
- $config = $this->getMockBuilder(IConfig::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $config = $this->createMock(IConfig::class);
+ $validator = $this->createMock(IFilenameValidator::class);
+ $accountManager = $this->createMock(IAccountManager::class);
$this->server->addPlugin(
- new \OCA\DAV\Connector\Sabre\FilesPlugin(
+ new FilesPlugin(
$this->tree,
$config,
$this->createMock(IRequest::class),
$this->previewManager,
- $this->createMock(IUserSession::class)
+ $this->createMock(IUserSession::class),
+ $validator,
+ $accountManager,
)
);
$this->plugin->initialize($this->server);
@@ -419,9 +351,6 @@ class FilesReportPluginTest extends \Test\TestCase {
$this->assertCount(2, $responses);
- $this->assertEquals(200, $responses[0]->getHttpStatus());
- $this->assertEquals(200, $responses[1]->getHttpStatus());
-
$this->assertEquals('http://example.com/owncloud/remote.php/dav/files/username/node1', $responses[0]->getHref());
$this->assertEquals('http://example.com/owncloud/remote.php/dav/files/username/sub/node2', $responses[1]->getHref());
@@ -439,41 +368,97 @@ class FilesReportPluginTest extends \Test\TestCase {
$this->assertCount(0, $props2[200]['{DAV:}resourcetype']->getValue());
}
- public function testProcessFilterRulesSingle() {
+ public function testProcessFilterRulesSingle(): void {
$this->groupManager->expects($this->any())
->method('isAdmin')
->willReturn(true);
- $this->tagMapper->expects($this->exactly(1))
- ->method('getObjectIdsForTags')
- ->withConsecutive(
- ['123', 'files']
- )
- ->willReturnMap([
- ['123', 'files', 0, '', ['111', '222']],
- ]);
-
$rules = [
['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'],
];
- $this->assertEquals(['111', '222'], $this->invokePrivate($this->plugin, 'processFilterRules', [$rules]));
+ $filesNode1 = $this->createMock(File::class);
+ $filesNode1->expects($this->any())
+ ->method('getSize')
+ ->willReturn(12);
+ $filesNode2 = $this->createMock(Folder::class);
+ $filesNode2->expects($this->any())
+ ->method('getSize')
+ ->willReturn(10);
+
+ $tag123 = $this->createMock(ISystemTag::class);
+ $tag123->expects($this->any())
+ ->method('getName')
+ ->willReturn('OneTwoThree');
+ $tag123->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['123'])
+ ->willReturn([$tag123]);
+
+ $this->userFolder->expects($this->once())
+ ->method('searchBySystemTag')
+ ->with('OneTwoThree')
+ ->willReturn([$filesNode1, $filesNode2]);
+
+ $this->assertEquals([$filesNode1, $filesNode2], self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, 0, 0]));
}
- public function testProcessFilterRulesAndCondition() {
+ public function testProcessFilterRulesAndCondition(): void {
$this->groupManager->expects($this->any())
->method('isAdmin')
->willReturn(true);
- $this->tagMapper->expects($this->exactly(2))
- ->method('getObjectIdsForTags')
- ->withConsecutive(
- ['123', 'files'],
- ['456', 'files']
- )
+ $filesNode1 = $this->createMock(File::class);
+ $filesNode1->expects($this->any())
+ ->method('getSize')
+ ->willReturn(12);
+ $filesNode1->expects($this->any())
+ ->method('getId')
+ ->willReturn(111);
+ $filesNode2 = $this->createMock(Folder::class);
+ $filesNode2->expects($this->any())
+ ->method('getSize')
+ ->willReturn(10);
+ $filesNode2->expects($this->any())
+ ->method('getId')
+ ->willReturn(222);
+ $filesNode3 = $this->createMock(File::class);
+ $filesNode3->expects($this->any())
+ ->method('getSize')
+ ->willReturn(14);
+ $filesNode3->expects($this->any())
+ ->method('getId')
+ ->willReturn(333);
+
+ $tag123 = $this->createMock(ISystemTag::class);
+ $tag123->expects($this->any())
+ ->method('getName')
+ ->willReturn('OneTwoThree');
+ $tag123->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+ $tag456 = $this->createMock(ISystemTag::class);
+ $tag456->expects($this->any())
+ ->method('getName')
+ ->willReturn('FourFiveSix');
+ $tag456->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['123', '456'])
+ ->willReturn([$tag123, $tag456]);
+
+ $this->userFolder->expects($this->exactly(2))
+ ->method('searchBySystemTag')
->willReturnMap([
- ['123', 'files', 0, '', ['111', '222']],
- ['456', 'files', 0, '', ['222', '333']],
+ ['OneTwoThree', 'testuser', 0, 0, [$filesNode1, $filesNode2]],
+ ['FourFiveSix', 'testuser', 0, 0, [$filesNode2, $filesNode3]],
]);
$rules = [
@@ -481,23 +466,54 @@ class FilesReportPluginTest extends \Test\TestCase {
['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'],
];
- $this->assertEquals(['222'], array_values($this->invokePrivate($this->plugin, 'processFilterRules', [$rules])));
+ $this->assertEquals([$filesNode2], array_values(self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, null, null])));
}
- public function testProcessFilterRulesAndConditionWithOneEmptyResult() {
+ public function testProcessFilterRulesAndConditionWithOneEmptyResult(): void {
$this->groupManager->expects($this->any())
->method('isAdmin')
->willReturn(true);
- $this->tagMapper->expects($this->exactly(2))
- ->method('getObjectIdsForTags')
- ->withConsecutive(
- ['123', 'files'],
- ['456', 'files']
- )
+ $filesNode1 = $this->createMock(File::class);
+ $filesNode1->expects($this->any())
+ ->method('getSize')
+ ->willReturn(12);
+ $filesNode1->expects($this->any())
+ ->method('getId')
+ ->willReturn(111);
+ $filesNode2 = $this->createMock(Folder::class);
+ $filesNode2->expects($this->any())
+ ->method('getSize')
+ ->willReturn(10);
+ $filesNode2->expects($this->any())
+ ->method('getId')
+ ->willReturn(222);
+
+ $tag123 = $this->createMock(ISystemTag::class);
+ $tag123->expects($this->any())
+ ->method('getName')
+ ->willReturn('OneTwoThree');
+ $tag123->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+ $tag456 = $this->createMock(ISystemTag::class);
+ $tag456->expects($this->any())
+ ->method('getName')
+ ->willReturn('FourFiveSix');
+ $tag456->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['123', '456'])
+ ->willReturn([$tag123, $tag456]);
+
+ $this->userFolder->expects($this->exactly(2))
+ ->method('searchBySystemTag')
->willReturnMap([
- ['123', 'files', 0, '', ['111', '222']],
- ['456', 'files', 0, '', []],
+ ['OneTwoThree', 'testuser', 0, 0, [$filesNode1, $filesNode2]],
+ ['FourFiveSix', 'testuser', 0, 0, []],
]);
$rules = [
@@ -505,23 +521,53 @@ class FilesReportPluginTest extends \Test\TestCase {
['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'],
];
- $this->assertEquals([], array_values($this->invokePrivate($this->plugin, 'processFilterRules', [$rules])));
+ $this->assertEquals([], self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, null, null]));
}
- public function testProcessFilterRulesAndConditionWithFirstEmptyResult() {
+ public function testProcessFilterRulesAndConditionWithFirstEmptyResult(): void {
$this->groupManager->expects($this->any())
->method('isAdmin')
->willReturn(true);
- $this->tagMapper->expects($this->exactly(1))
- ->method('getObjectIdsForTags')
- ->withConsecutive(
- ['123', 'files'],
- ['456', 'files']
- )
+ $filesNode1 = $this->createMock(File::class);
+ $filesNode1->expects($this->any())
+ ->method('getSize')
+ ->willReturn(12);
+ $filesNode1->expects($this->any())
+ ->method('getId')
+ ->willReturn(111);
+ $filesNode2 = $this->createMock(Folder::class);
+ $filesNode2->expects($this->any())
+ ->method('getSize')
+ ->willReturn(10);
+ $filesNode2->expects($this->any())
+ ->method('getId')
+ ->willReturn(222);
+
+ $tag123 = $this->createMock(ISystemTag::class);
+ $tag123->expects($this->any())
+ ->method('getName')
+ ->willReturn('OneTwoThree');
+ $tag123->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+ $tag456 = $this->createMock(ISystemTag::class);
+ $tag456->expects($this->any())
+ ->method('getName')
+ ->willReturn('FourFiveSix');
+ $tag456->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['123', '456'])
+ ->willReturn([$tag123, $tag456]);
+
+ $this->userFolder->expects($this->once())
+ ->method('searchBySystemTag')
->willReturnMap([
- ['123', 'files', 0, '', []],
- ['456', 'files', 0, '', ['111', '222']],
+ ['OneTwoThree', 'testuser', 0, 0, []],
]);
$rules = [
@@ -529,25 +575,68 @@ class FilesReportPluginTest extends \Test\TestCase {
['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'],
];
- $this->assertEquals([], array_values($this->invokePrivate($this->plugin, 'processFilterRules', [$rules])));
+ $this->assertEquals([], self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, null, null]));
}
- public function testProcessFilterRulesAndConditionWithEmptyMidResult() {
+ public function testProcessFilterRulesAndConditionWithEmptyMidResult(): void {
$this->groupManager->expects($this->any())
->method('isAdmin')
->willReturn(true);
- $this->tagMapper->expects($this->exactly(2))
- ->method('getObjectIdsForTags')
- ->withConsecutive(
- ['123', 'files'],
- ['456', 'files'],
- ['789', 'files']
- )
+ $filesNode1 = $this->createMock(File::class);
+ $filesNode1->expects($this->any())
+ ->method('getSize')
+ ->willReturn(12);
+ $filesNode1->expects($this->any())
+ ->method('getId')
+ ->willReturn(111);
+ $filesNode2 = $this->createMock(Folder::class);
+ $filesNode2->expects($this->any())
+ ->method('getSize')
+ ->willReturn(10);
+ $filesNode2->expects($this->any())
+ ->method('getId')
+ ->willReturn(222);
+ $filesNode3 = $this->createMock(Folder::class);
+ $filesNode3->expects($this->any())
+ ->method('getSize')
+ ->willReturn(13);
+ $filesNode3->expects($this->any())
+ ->method('getId')
+ ->willReturn(333);
+
+ $tag123 = $this->createMock(ISystemTag::class);
+ $tag123->expects($this->any())
+ ->method('getName')
+ ->willReturn('OneTwoThree');
+ $tag123->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+ $tag456 = $this->createMock(ISystemTag::class);
+ $tag456->expects($this->any())
+ ->method('getName')
+ ->willReturn('FourFiveSix');
+ $tag456->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+ $tag789 = $this->createMock(ISystemTag::class);
+ $tag789->expects($this->any())
+ ->method('getName')
+ ->willReturn('SevenEightNine');
+ $tag789->expects($this->any())
+ ->method('isUserVisible')
+ ->willReturn(true);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['123', '456', '789'])
+ ->willReturn([$tag123, $tag456, $tag789]);
+
+ $this->userFolder->expects($this->exactly(2))
+ ->method('searchBySystemTag')
->willReturnMap([
- ['123', 'files', 0, '', ['111', '222']],
- ['456', 'files', 0, '', ['333']],
- ['789', 'files', 0, '', ['111', '222']],
+ ['OneTwoThree', 'testuser', 0, 0, [$filesNode1, $filesNode2]],
+ ['FourFiveSix', 'testuser', 0, 0, [$filesNode3]],
]);
$rules = [
@@ -556,144 +645,186 @@ class FilesReportPluginTest extends \Test\TestCase {
['name' => '{http://owncloud.org/ns}systemtag', 'value' => '789'],
];
- $this->assertEquals([], array_values($this->invokePrivate($this->plugin, 'processFilterRules', [$rules])));
+ $this->assertEquals([], array_values(self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, null, null])));
}
- public function testProcessFilterRulesInvisibleTagAsAdmin() {
+ public function testProcessFilterRulesInvisibleTagAsAdmin(): void {
$this->groupManager->expects($this->any())
->method('isAdmin')
->willReturn(true);
- $tag1 = $this->getMockBuilder(ISystemTag::class)
- ->disableOriginalConstructor()
- ->getMock();
- $tag1->expects($this->any())
+ $filesNode1 = $this->createMock(File::class);
+ $filesNode1->expects($this->any())
+ ->method('getSize')
+ ->willReturn(12);
+ $filesNode1->expects($this->any())
->method('getId')
- ->willReturn('123');
- $tag1->expects($this->any())
+ ->willReturn(111);
+ $filesNode2 = $this->createMock(Folder::class);
+ $filesNode2->expects($this->any())
+ ->method('getSize')
+ ->willReturn(10);
+ $filesNode2->expects($this->any())
+ ->method('getId')
+ ->willReturn(222);
+ $filesNode3 = $this->createMock(Folder::class);
+ $filesNode3->expects($this->any())
+ ->method('getSize')
+ ->willReturn(13);
+ $filesNode3->expects($this->any())
+ ->method('getId')
+ ->willReturn(333);
+
+ $tag123 = $this->createMock(ISystemTag::class);
+ $tag123->expects($this->any())
+ ->method('getName')
+ ->willReturn('OneTwoThree');
+ $tag123->expects($this->any())
->method('isUserVisible')
->willReturn(true);
-
- $tag2 = $this->getMockBuilder(ISystemTag::class)
- ->disableOriginalConstructor()
- ->getMock();
- $tag2->expects($this->any())
- ->method('getId')
- ->willReturn('123');
- $tag2->expects($this->any())
+ $tag456 = $this->createMock(ISystemTag::class);
+ $tag456->expects($this->any())
+ ->method('getName')
+ ->willReturn('FourFiveSix');
+ $tag456->expects($this->any())
->method('isUserVisible')
->willReturn(false);
- // no need to fetch tags to check permissions
- $this->tagManager->expects($this->never())
- ->method('getTagsByIds');
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['123', '456'])
+ ->willReturn([$tag123, $tag456]);
- $this->tagMapper->expects($this->at(0))
- ->method('getObjectIdsForTags')
- ->with('123')
- ->willReturn(['111', '222']);
- $this->tagMapper->expects($this->at(1))
- ->method('getObjectIdsForTags')
- ->with('456')
- ->willReturn(['222', '333']);
+ $this->userFolder->expects($this->exactly(2))
+ ->method('searchBySystemTag')
+ ->willReturnMap([
+ ['OneTwoThree', 'testuser', 0, 0, [$filesNode1, $filesNode2]],
+ ['FourFiveSix', 'testuser', 0, 0, [$filesNode2, $filesNode3]],
+ ]);
$rules = [
['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'],
['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'],
];
- $this->assertEquals(['222'], array_values($this->invokePrivate($this->plugin, 'processFilterRules', [$rules])));
+ $this->assertEquals([$filesNode2], array_values(self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, null, null])));
}
- public function testProcessFilterRulesInvisibleTagAsUser() {
- $this->expectException(\OCP\SystemTag\TagNotFoundException::class);
+ public function testProcessFilterRulesInvisibleTagAsUser(): void {
+ $this->expectException(TagNotFoundException::class);
$this->groupManager->expects($this->any())
->method('isAdmin')
->willReturn(false);
- $tag1 = $this->getMockBuilder(ISystemTag::class)
- ->disableOriginalConstructor()
- ->getMock();
- $tag1->expects($this->any())
- ->method('getId')
- ->willReturn('123');
- $tag1->expects($this->any())
+ $tag123 = $this->createMock(ISystemTag::class);
+ $tag123->expects($this->any())
+ ->method('getName')
+ ->willReturn('OneTwoThree');
+ $tag123->expects($this->any())
->method('isUserVisible')
->willReturn(true);
-
- $tag2 = $this->getMockBuilder(ISystemTag::class)
- ->disableOriginalConstructor()
- ->getMock();
- $tag2->expects($this->any())
- ->method('getId')
- ->willReturn('123');
- $tag2->expects($this->any())
+ $tag456 = $this->createMock(ISystemTag::class);
+ $tag456->expects($this->any())
+ ->method('getName')
+ ->willReturn('FourFiveSix');
+ $tag456->expects($this->any())
->method('isUserVisible')
- ->willReturn(false); // invisible
+ ->willReturn(false);
$this->tagManager->expects($this->once())
->method('getTagsByIds')
->with(['123', '456'])
- ->willReturn([$tag1, $tag2]);
+ ->willThrowException(new TagNotFoundException());
+
+ $this->userFolder->expects($this->never())
+ ->method('searchBySystemTag');
$rules = [
['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'],
['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'],
];
- $this->invokePrivate($this->plugin, 'processFilterRules', [$rules]);
+ self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, null, null]);
}
- public function testProcessFilterRulesVisibleTagAsUser() {
+ public function testProcessFilterRulesVisibleTagAsUser(): void {
$this->groupManager->expects($this->any())
->method('isAdmin')
->willReturn(false);
- $tag1 = $this->getMockBuilder(ISystemTag::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $tag1 = $this->createMock(ISystemTag::class);
$tag1->expects($this->any())
->method('getId')
->willReturn('123');
$tag1->expects($this->any())
->method('isUserVisible')
->willReturn(true);
+ $tag1->expects($this->any())
+ ->method('getName')
+ ->willReturn('OneTwoThree');
- $tag2 = $this->getMockBuilder(ISystemTag::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $tag2 = $this->createMock(ISystemTag::class);
$tag2->expects($this->any())
->method('getId')
->willReturn('123');
$tag2->expects($this->any())
->method('isUserVisible')
->willReturn(true);
+ $tag2->expects($this->any())
+ ->method('getName')
+ ->willReturn('FourFiveSix');
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['123', '456'])
+ ->willReturn([$tag1, $tag2]);
+
+ $filesNode1 = $this->createMock(File::class);
+ $filesNode1->expects($this->any())
+ ->method('getId')
+ ->willReturn(111);
+ $filesNode1->expects($this->any())
+ ->method('getSize')
+ ->willReturn(12);
+ $filesNode2 = $this->createMock(Folder::class);
+ $filesNode2->expects($this->any())
+ ->method('getId')
+ ->willReturn(222);
+ $filesNode2->expects($this->any())
+ ->method('getSize')
+ ->willReturn(10);
+ $filesNode3 = $this->createMock(Folder::class);
+ $filesNode3->expects($this->any())
+ ->method('getId')
+ ->willReturn(333);
+ $filesNode3->expects($this->any())
+ ->method('getSize')
+ ->willReturn(33);
$this->tagManager->expects($this->once())
->method('getTagsByIds')
->with(['123', '456'])
->willReturn([$tag1, $tag2]);
- $this->tagMapper->expects($this->at(0))
- ->method('getObjectIdsForTags')
- ->with('123')
- ->willReturn(['111', '222']);
- $this->tagMapper->expects($this->at(1))
- ->method('getObjectIdsForTags')
- ->with('456')
- ->willReturn(['222', '333']);
+ // main assertion: only user visible tags are being passed through.
+ $this->userFolder->expects($this->exactly(2))
+ ->method('searchBySystemTag')
+ ->willReturnMap([
+ ['OneTwoThree', 'testuser', 0, 0, [$filesNode1, $filesNode2]],
+ ['FourFiveSix', 'testuser', 0, 0, [$filesNode2, $filesNode3]],
+ ]);
$rules = [
['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'],
['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'],
];
- $this->assertEquals(['222'], array_values($this->invokePrivate($this->plugin, 'processFilterRules', [$rules])));
+ $this->assertEquals([$filesNode2], array_values(self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, null, null])));
}
- public function testProcessFavoriteFilter() {
+ public function testProcessFavoriteFilter(): void {
$rules = [
['name' => '{http://owncloud.org/ns}favorite', 'value' => '1'],
];
@@ -702,10 +833,10 @@ class FilesReportPluginTest extends \Test\TestCase {
->method('getFavorites')
->willReturn(['456', '789']);
- $this->assertEquals(['456', '789'], array_values($this->invokePrivate($this->plugin, 'processFilterRules', [$rules])));
+ $this->assertEquals(['456', '789'], array_values(self::invokePrivate($this->plugin, 'processFilterRulesForFileIDs', [$rules])));
}
- public function filesBaseUriProvider() {
+ public static function filesBaseUriProvider(): array {
return [
['', '', ''],
['files/username', '', '/files/username'],
@@ -715,10 +846,8 @@ class FilesReportPluginTest extends \Test\TestCase {
];
}
- /**
- * @dataProvider filesBaseUriProvider
- */
- public function testFilesBaseUri($uri, $reportPath, $expectedUri) {
- $this->assertEquals($expectedUri, $this->invokePrivate($this->plugin, 'getFilesBaseUri', [$uri, $reportPath]));
+ #[\PHPUnit\Framework\Attributes\DataProvider('filesBaseUriProvider')]
+ public function testFilesBaseUri(string $uri, string $reportPath, string $expectedUri): void {
+ $this->assertEquals($expectedUri, self::invokePrivate($this->plugin, 'getFilesBaseUri', [$uri, $reportPath]));
}
}
diff --git a/apps/dav/tests/unit/Connector/Sabre/MaintenancePluginTest.php b/apps/dav/tests/unit/Connector/Sabre/MaintenancePluginTest.php
index 3f38008559c..bc1d50ac41f 100644
--- a/apps/dav/tests/unit/Connector/Sabre/MaintenancePluginTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/MaintenancePluginTest.php
@@ -1,34 +1,17 @@
<?php
+
+declare(strict_types=1);
/**
- * @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 Valdnet <47037905+Valdnet@users.noreply.github.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\Tests\unit\Connector\Sabre;
use OCA\DAV\Connector\Sabre\MaintenancePlugin;
use OCP\IConfig;
use OCP\IL10N;
+use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
/**
@@ -37,23 +20,20 @@ use Test\TestCase;
* @package OCA\DAV\Tests\unit\Connector\Sabre
*/
class MaintenancePluginTest extends TestCase {
- /** @var IConfig */
- private $config;
- /** @var \PHPUnit\Framework\MockObject\Builder\InvocationMocker|\PHPUnit_Framework_MockObject_Builder_InvocationMocker|IL10N */
- private $l10n;
- /** @var MaintenancePlugin */
- private $maintenancePlugin;
+ private IConfig&MockObject $config;
+ private IL10N&MockObject $l10n;
+ private MaintenancePlugin $maintenancePlugin;
protected function setUp(): void {
parent::setUp();
- $this->config = $this->getMockBuilder(IConfig::class)->getMock();
- $this->l10n = $this->getMockBuilder(IL10N::class)->getMock();
+ $this->config = $this->createMock(IConfig::class);
+ $this->l10n = $this->createMock(IL10N::class);
$this->maintenancePlugin = new MaintenancePlugin($this->config, $this->l10n);
}
- public function testMaintenanceMode() {
+ public function testMaintenanceMode(): void {
$this->expectException(\Sabre\DAV\Exception\ServiceUnavailable::class);
$this->expectExceptionMessage('System is in maintenance mode.');
diff --git a/apps/dav/tests/unit/Connector/Sabre/NodeTest.php b/apps/dav/tests/unit/Connector/Sabre/NodeTest.php
index 00fd0ebd8aa..11970769a1e 100644
--- a/apps/dav/tests/unit/Connector/Sabre/NodeTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/NodeTest.php
@@ -1,38 +1,30 @@
<?php
+
+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 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>
- *
- * @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\Tests\unit\Connector\Sabre;
use OC\Files\FileInfo;
+use OC\Files\Mount\MountPoint;
+use OC\Files\Node\Folder;
use OC\Files\View;
+use OC\Share20\ShareAttributes;
+use OCA\DAV\Connector\Sabre\File;
+use OCA\Files_Sharing\SharedMount;
+use OCA\Files_Sharing\SharedStorage;
+use OCP\Constants;
+use OCP\Files\Cache\ICacheEntry;
use OCP\Files\Mount\IMountPoint;
-use OCP\Files\Storage;
+use OCP\Files\Storage\IStorage;
+use OCP\ICache;
use OCP\Share\IManager;
use OCP\Share\IShare;
+use PHPUnit\Framework\MockObject\MockObject;
/**
* Class NodeTest
@@ -41,51 +33,73 @@ use OCP\Share\IShare;
* @package OCA\DAV\Tests\unit\Connector\Sabre
*/
class NodeTest extends \Test\TestCase {
- public function davPermissionsProvider() {
+ public static function davPermissionsProvider(): array {
return [
- [\OCP\Constants::PERMISSION_ALL, 'file', false, false, 'RGDNVW'],
- [\OCP\Constants::PERMISSION_ALL, 'dir', false, false, 'RGDNVCK'],
- [\OCP\Constants::PERMISSION_ALL, 'file', true, false, 'SRGDNVW'],
- [\OCP\Constants::PERMISSION_ALL, 'file', true, true, 'SRMGDNVW'],
- [\OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_SHARE, 'file', true, false, 'SGDNVW'],
- [\OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_UPDATE, 'file', false, false, 'RGD'],
- [\OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_DELETE, 'file', false, false, 'RGNVW'],
- [\OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE, 'file', false, false, 'RGDNVW'],
- [\OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_READ, 'file', false, false, 'RDNVW'],
- [\OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE, 'dir', false, false, 'RGDNV'],
- [\OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_READ, 'dir', false, false, 'RDNVCK'],
+ [Constants::PERMISSION_ALL, 'file', false, Constants::PERMISSION_ALL, false, 'test', 'RGDNVW'],
+ [Constants::PERMISSION_ALL, 'dir', false, Constants::PERMISSION_ALL, false, 'test', 'RGDNVCK'],
+ [Constants::PERMISSION_ALL, 'file', true, Constants::PERMISSION_ALL, false, 'test', 'SRGDNVW'],
+ [Constants::PERMISSION_ALL, 'file', true, Constants::PERMISSION_ALL, true, 'test', 'SRMGDNVW'],
+ [Constants::PERMISSION_ALL, 'file', true, Constants::PERMISSION_ALL, true, '' , 'SRMGDNVW'],
+ [Constants::PERMISSION_ALL, 'file', true, Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE, true, '' , 'SRMGDNV'],
+ [Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE, 'file', true, Constants::PERMISSION_ALL, false, 'test', 'SGDNVW'],
+ [Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE, 'file', false, Constants::PERMISSION_ALL, false, 'test', 'RGD'],
+ [Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE, 'file', false, Constants::PERMISSION_ALL, false, 'test', 'RGNVW'],
+ [Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE, 'file', false, Constants::PERMISSION_ALL, false, 'test', 'RGDNVW'],
+ [Constants::PERMISSION_ALL - Constants::PERMISSION_READ, 'file', false, Constants::PERMISSION_ALL, false, 'test', 'RDNVW'],
+ [Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE, 'dir', false, Constants::PERMISSION_ALL, false, 'test', 'RGDNV'],
+ [Constants::PERMISSION_ALL - Constants::PERMISSION_READ, 'dir', false, Constants::PERMISSION_ALL, false, 'test', 'RDNVCK'],
];
}
- /**
- * @dataProvider davPermissionsProvider
- */
- public function testDavPermissions($permissions, $type, $shared, $mounted, $expected) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('davPermissionsProvider')]
+ public function testDavPermissions(int $permissions, string $type, bool $shared, int $shareRootPermissions, bool $mounted, string $internalPath, string $expected): void {
$info = $this->getMockBuilder(FileInfo::class)
->disableOriginalConstructor()
- ->setMethods(['getPermissions', 'isShared', 'isMounted', 'getType'])
+ ->onlyMethods(['getPermissions', 'isShared', 'isMounted', 'getType', 'getInternalPath', 'getStorage', 'getMountPoint'])
->getMock();
- $info->expects($this->any())
- ->method('getPermissions')
+ $info->method('getPermissions')
->willReturn($permissions);
- $info->expects($this->any())
- ->method('isShared')
+ $info->method('isShared')
->willReturn($shared);
- $info->expects($this->any())
- ->method('isMounted')
+ $info->method('isMounted')
->willReturn($mounted);
- $info->expects($this->any())
- ->method('getType')
+ $info->method('getType')
->willReturn($type);
- $view = $this->getMockBuilder(View::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $info->method('getInternalPath')
+ ->willReturn($internalPath);
+ $info->method('getMountPoint')
+ ->willReturnCallback(function () use ($shared) {
+ if ($shared) {
+ return $this->createMock(SharedMount::class);
+ } else {
+ return $this->createMock(MountPoint::class);
+ }
+ });
+ $storage = $this->createMock(IStorage::class);
+ if ($shared) {
+ $storage->method('instanceOfStorage')
+ ->willReturn(true);
+ $cache = $this->createMock(ICache::class);
+ $storage->method('getCache')
+ ->willReturn($cache);
+ $shareRootEntry = $this->createMock(ICacheEntry::class);
+ $cache->method('get')
+ ->willReturn($shareRootEntry);
+ $shareRootEntry->method('getPermissions')
+ ->willReturn($shareRootPermissions);
+ } else {
+ $storage->method('instanceOfStorage')
+ ->willReturn(false);
+ }
+ $info->method('getStorage')
+ ->willReturn($storage);
+ $view = $this->createMock(View::class);
- $node = new \OCA\DAV\Connector\Sabre\File($view, $info);
+ $node = new File($view, $info);
$this->assertEquals($expected, $node->getDavPermissions());
}
- public function sharePermissionsProvider() {
+ public static function sharePermissionsProvider(): array {
return [
[\OCP\Files\FileInfo::TYPE_FILE, null, 1, 1],
[\OCP\Files\FileInfo::TYPE_FILE, null, 3, 3],
@@ -125,21 +139,15 @@ class NodeTest extends \Test\TestCase {
];
}
- /**
- * @dataProvider sharePermissionsProvider
- */
- public function testSharePermissions($type, $user, $permissions, $expected) {
- $storage = $this->getMockBuilder(Storage::class)
- ->disableOriginalConstructor()
- ->getMock();
+ #[\PHPUnit\Framework\Attributes\DataProvider('sharePermissionsProvider')]
+ public function testSharePermissions(string $type, ?string $user, int $permissions, int $expected): void {
+ $storage = $this->createMock(IStorage::class);
$storage->method('getPermissions')->willReturn($permissions);
- $mountpoint = $this->getMockBuilder(IMountPoint::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $mountpoint = $this->createMock(IMountPoint::class);
$mountpoint->method('getMountPoint')->willReturn('myPath');
- $shareManager = $this->getMockBuilder(IManager::class)->disableOriginalConstructor()->getMock();
- $share = $this->getMockBuilder(IShare::class)->disableOriginalConstructor()->getMock();
+ $shareManager = $this->createMock(IManager::class);
+ $share = $this->createMock(IShare::class);
if ($user === null) {
$shareManager->expects($this->never())->method('getShareByToken');
@@ -152,7 +160,7 @@ class NodeTest extends \Test\TestCase {
$info = $this->getMockBuilder(FileInfo::class)
->disableOriginalConstructor()
- ->setMethods(['getStorage', 'getType', 'getMountPoint', 'getPermissions'])
+ ->onlyMethods(['getStorage', 'getType', 'getMountPoint', 'getPermissions'])
->getMock();
$info->method('getStorage')->willReturn($storage);
@@ -160,26 +168,78 @@ class NodeTest extends \Test\TestCase {
$info->method('getMountPoint')->willReturn($mountpoint);
$info->method('getPermissions')->willReturn($permissions);
- $view = $this->getMockBuilder(View::class)
+ $view = $this->createMock(View::class);
+
+ $node = new File($view, $info);
+ $this->invokePrivate($node, 'shareManager', [$shareManager]);
+ $this->assertEquals($expected, $node->getSharePermissions($user));
+ }
+
+ public function testShareAttributes(): void {
+ $storage = $this->getMockBuilder(SharedStorage::class)
->disableOriginalConstructor()
+ ->onlyMethods(['getShare'])
->getMock();
- $node = new \OCA\DAV\Connector\Sabre\File($view, $info);
+ $shareManager = $this->createMock(IManager::class);
+ $share = $this->createMock(IShare::class);
+
+ $storage->expects($this->once())
+ ->method('getShare')
+ ->willReturn($share);
+
+ $attributes = new ShareAttributes();
+ $attributes->setAttribute('permissions', 'download', false);
+
+ $share->expects($this->once())->method('getAttributes')->willReturn($attributes);
+
+ /** @var Folder&MockObject $info */
+ $info = $this->getMockBuilder(Folder::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['getStorage', 'getType'])
+ ->getMock();
+
+ $info->method('getStorage')->willReturn($storage);
+ $info->method('getType')->willReturn(FileInfo::TYPE_FOLDER);
+
+ /** @var View&MockObject $view */
+ $view = $this->createMock(View::class);
+
+ $node = new File($view, $info);
$this->invokePrivate($node, 'shareManager', [$shareManager]);
- $this->assertEquals($expected, $node->getSharePermissions($user));
+ $this->assertEquals($attributes->toArray(), $node->getShareAttributes());
}
- public function sanitizeMtimeProvider() {
+ public function testShareAttributesNonShare(): void {
+ $storage = $this->createMock(IStorage::class);
+ $shareManager = $this->createMock(IManager::class);
+
+ /** @var Folder&MockObject */
+ $info = $this->getMockBuilder(Folder::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['getStorage', 'getType'])
+ ->getMock();
+
+ $info->method('getStorage')->willReturn($storage);
+ $info->method('getType')->willReturn(FileInfo::TYPE_FOLDER);
+
+ /** @var View&MockObject */
+ $view = $this->createMock(View::class);
+
+ $node = new File($view, $info);
+ $this->invokePrivate($node, 'shareManager', [$shareManager]);
+ $this->assertEquals([], $node->getShareAttributes());
+ }
+
+ public static function sanitizeMtimeProvider(): array {
return [
[123456789, 123456789],
['987654321', 987654321],
];
}
- /**
- * @dataProvider sanitizeMtimeProvider
- */
- public function testSanitizeMtime($mtime, $expected) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('sanitizeMtimeProvider')]
+ public function testSanitizeMtime(string|int $mtime, int $expected): void {
$view = $this->getMockBuilder(View::class)
->disableOriginalConstructor()
->getMock();
@@ -187,31 +247,25 @@ class NodeTest extends \Test\TestCase {
->disableOriginalConstructor()
->getMock();
- $node = new \OCA\DAV\Connector\Sabre\File($view, $info);
+ $node = new File($view, $info);
$result = $this->invokePrivate($node, 'sanitizeMtime', [$mtime]);
$this->assertEquals($expected, $result);
}
- public function invalidSanitizeMtimeProvider() {
+ public static function invalidSanitizeMtimeProvider(): array {
return [
- [-1337], [0], ['abcdef'], ['-1337'], ['0'], [12321], [24 * 60 * 60 - 1]
+ [-1337], [0], ['abcdef'], ['-1337'], ['0'], [12321], [24 * 60 * 60 - 1],
];
}
- /**
- * @dataProvider invalidSanitizeMtimeProvider
- */
- public function testInvalidSanitizeMtime($mtime) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('invalidSanitizeMtimeProvider')]
+ public function testInvalidSanitizeMtime(int|string $mtime): void {
$this->expectException(\InvalidArgumentException::class);
- $view = $this->getMockBuilder(View::class)
- ->disableOriginalConstructor()
- ->getMock();
- $info = $this->getMockBuilder(FileInfo::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $view = $this->createMock(View::class);
+ $info = $this->createMock(FileInfo::class);
- $node = new \OCA\DAV\Connector\Sabre\File($view, $info);
- $result = $this->invokePrivate($node, 'sanitizeMtime', [$mtime]);
+ $node = new File($view, $info);
+ self::invokePrivate($node, 'sanitizeMtime', [$mtime]);
}
}
diff --git a/apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php b/apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php
index 5f516cec113..b07778e4fbd 100644
--- a/apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php
@@ -1,38 +1,22 @@
<?php
+
+declare(strict_types=1);
/**
- * @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 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\Tests\unit\Connector\Sabre;
use OC\Files\FileInfo;
use OC\Files\Filesystem;
use OC\Files\Mount\Manager;
+use OC\Files\Storage\Common;
use OC\Files\Storage\Temporary;
use OC\Files\View;
use OCA\DAV\Connector\Sabre\Directory;
+use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
+use OCA\DAV\Connector\Sabre\File;
use OCA\DAV\Connector\Sabre\ObjectTree;
use OCP\Files\Mount\IMountManager;
@@ -44,7 +28,7 @@ use OCP\Files\Mount\IMountManager;
* @package OCA\DAV\Tests\Unit\Connector\Sabre
*/
class ObjectTreeTest extends \Test\TestCase {
- public function copyDataProvider() {
+ public static function copyDataProvider(): array {
return [
// copy into same dir
['a', 'b', ''],
@@ -55,15 +39,12 @@ class ObjectTreeTest extends \Test\TestCase {
];
}
- /**
- * @dataProvider copyDataProvider
- */
- public function testCopy($sourcePath, $targetPath, $targetParent) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('copyDataProvider')]
+ public function testCopy(string $sourcePath, string $targetPath, string $targetParent): void {
$view = $this->createMock(View::class);
$view->expects($this->once())
->method('verifyPath')
- ->with($targetParent)
- ->willReturn(true);
+ ->with($targetParent);
$view->expects($this->once())
->method('file_exists')
->with($targetPath)
@@ -85,7 +66,7 @@ class ObjectTreeTest extends \Test\TestCase {
$rootDir = new Directory($view, $info);
$objectTree = $this->getMockBuilder(ObjectTree::class)
- ->setMethods(['nodeExists', 'getNodeForPath'])
+ ->onlyMethods(['nodeExists', 'getNodeForPath'])
->setConstructorArgs([$rootDir, $view])
->getMock();
@@ -94,16 +75,14 @@ class ObjectTreeTest extends \Test\TestCase {
->with($this->identicalTo($sourcePath))
->willReturn(false);
- /** @var $objectTree \OCA\DAV\Connector\Sabre\ObjectTree */
+ /** @var ObjectTree $objectTree */
$mountManager = Filesystem::getMountManager();
$objectTree->init($rootDir, $view, $mountManager);
$objectTree->copy($sourcePath, $targetPath);
}
- /**
- * @dataProvider copyDataProvider
- */
- public function testCopyFailNotCreatable($sourcePath, $targetPath, $targetParent) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('copyDataProvider')]
+ public function testCopyFailNotCreatable($sourcePath, $targetPath, $targetParent): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$view = $this->createMock(View::class);
@@ -128,57 +107,42 @@ class ObjectTreeTest extends \Test\TestCase {
$rootDir = new Directory($view, $info);
$objectTree = $this->getMockBuilder(ObjectTree::class)
- ->setMethods(['nodeExists', 'getNodeForPath'])
+ ->onlyMethods(['nodeExists', 'getNodeForPath'])
->setConstructorArgs([$rootDir, $view])
->getMock();
$objectTree->expects($this->never())
->method('getNodeForPath');
- /** @var $objectTree \OCA\DAV\Connector\Sabre\ObjectTree */
+ /** @var ObjectTree $objectTree */
$mountManager = Filesystem::getMountManager();
$objectTree->init($rootDir, $view, $mountManager);
$objectTree->copy($sourcePath, $targetPath);
}
- /**
- * @dataProvider nodeForPathProvider
- */
+ #[\PHPUnit\Framework\Attributes\DataProvider('nodeForPathProvider')]
public function testGetNodeForPath(
- $inputFileName,
- $fileInfoQueryPath,
- $outputFileName,
- $type,
- $enableChunkingHeader
- ) {
- if ($enableChunkingHeader) {
- $_SERVER['HTTP_OC_CHUNKED'] = true;
- }
-
- $rootNode = $this->getMockBuilder(Directory::class)
- ->disableOriginalConstructor()
- ->getMock();
- $mountManager = $this->getMockBuilder(Manager::class)
- ->disableOriginalConstructor()
- ->getMock();
- $view = $this->getMockBuilder(View::class)
- ->disableOriginalConstructor()
- ->getMock();
- $fileInfo = $this->getMockBuilder(FileInfo::class)
- ->disableOriginalConstructor()
- ->getMock();
+ string $inputFileName,
+ string $fileInfoQueryPath,
+ string $outputFileName,
+ string $type,
+ ): void {
+ $rootNode = $this->createMock(Directory::class);
+ $mountManager = $this->createMock(Manager::class);
+ $view = $this->createMock(View::class);
+ $fileInfo = $this->createMock(FileInfo::class);
$fileInfo->method('getType')
->willReturn($type);
$fileInfo->method('getName')
->willReturn($outputFileName);
$fileInfo->method('getStorage')
- ->willReturn($this->createMock(\OC\Files\Storage\Common::class));
+ ->willReturn($this->createMock(Common::class));
$view->method('getFileInfo')
->with($fileInfoQueryPath)
->willReturn($fileInfo);
- $tree = new \OCA\DAV\Connector\Sabre\ObjectTree();
+ $tree = new ObjectTree();
$tree->init($rootNode, $view, $mountManager);
$node = $tree->getNodeForPath($inputFileName);
@@ -187,15 +151,13 @@ class ObjectTreeTest extends \Test\TestCase {
$this->assertEquals($outputFileName, $node->getName());
if ($type === 'file') {
- $this->assertTrue($node instanceof \OCA\DAV\Connector\Sabre\File);
+ $this->assertInstanceOf(File::class, $node);
} else {
- $this->assertTrue($node instanceof \OCA\DAV\Connector\Sabre\Directory);
+ $this->assertInstanceOf(Directory::class, $node);
}
-
- unset($_SERVER['HTTP_OC_CHUNKED']);
}
- public function nodeForPathProvider() {
+ public static function nodeForPathProvider(): array {
return [
// regular file
[
@@ -203,7 +165,6 @@ class ObjectTreeTest extends \Test\TestCase {
'regularfile.txt',
'regularfile.txt',
'file',
- false
],
// regular directory
[
@@ -211,31 +172,6 @@ class ObjectTreeTest extends \Test\TestCase {
'regulardir',
'regulardir',
'dir',
- false
- ],
- // regular file with chunking
- [
- 'regularfile.txt',
- 'regularfile.txt',
- 'regularfile.txt',
- 'file',
- true
- ],
- // regular directory with chunking
- [
- 'regulardir',
- 'regulardir',
- 'regulardir',
- 'dir',
- true
- ],
- // file with chunky file name
- [
- 'regularfile.txt-chunking-123566789-10-1',
- 'regularfile.txt',
- 'regularfile.txt',
- 'file',
- true
],
// regular file in subdir
[
@@ -243,7 +179,6 @@ class ObjectTreeTest extends \Test\TestCase {
'subdir/regularfile.txt',
'regularfile.txt',
'file',
- false
],
// regular directory in subdir
[
@@ -251,22 +186,13 @@ class ObjectTreeTest extends \Test\TestCase {
'subdir/regulardir',
'regulardir',
'dir',
- false
- ],
- // file with chunky file name in subdir
- [
- 'subdir/regularfile.txt-chunking-123566789-10-1',
- 'subdir/regularfile.txt',
- 'regularfile.txt',
- 'file',
- true
],
];
}
- public function testGetNodeForPathInvalidPath() {
- $this->expectException(\OCA\DAV\Connector\Sabre\Exception\InvalidPath::class);
+ public function testGetNodeForPathInvalidPath(): void {
+ $this->expectException(InvalidPath::class);
$path = '/foo\bar';
@@ -274,7 +200,7 @@ class ObjectTreeTest extends \Test\TestCase {
$storage = new Temporary([]);
$view = $this->getMockBuilder(View::class)
- ->setMethods(['resolvePath'])
+ ->onlyMethods(['resolvePath'])
->getMock();
$view->expects($this->once())
->method('resolvePath')
@@ -282,25 +208,23 @@ class ObjectTreeTest extends \Test\TestCase {
return [$storage, ltrim($path, '/')];
});
- $rootNode = $this->getMockBuilder(Directory::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $rootNode = $this->createMock(Directory::class);
$mountManager = $this->createMock(IMountManager::class);
- $tree = new \OCA\DAV\Connector\Sabre\ObjectTree();
+ $tree = new ObjectTree();
$tree->init($rootNode, $view, $mountManager);
$tree->getNodeForPath($path);
}
- public function testGetNodeForPathRoot() {
+ public function testGetNodeForPathRoot(): void {
$path = '/';
$storage = new Temporary([]);
$view = $this->getMockBuilder(View::class)
- ->setMethods(['resolvePath'])
+ ->onlyMethods(['resolvePath'])
->getMock();
$view->expects($this->any())
->method('resolvePath')
@@ -308,12 +232,10 @@ class ObjectTreeTest extends \Test\TestCase {
return [$storage, ltrim($path, '/')];
});
- $rootNode = $this->getMockBuilder(Directory::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $rootNode = $this->createMock(Directory::class);
$mountManager = $this->createMock(IMountManager::class);
- $tree = new \OCA\DAV\Connector\Sabre\ObjectTree();
+ $tree = new ObjectTree();
$tree->init($rootNode, $view, $mountManager);
$this->assertInstanceOf('\Sabre\DAV\INode', $tree->getNodeForPath($path));
diff --git a/apps/dav/tests/unit/Connector/Sabre/PrincipalTest.php b/apps/dav/tests/unit/Connector/Sabre/PrincipalTest.php
index d7c074c9e3b..e32d2671063 100644
--- a/apps/dav/tests/unit/Connector/Sabre/PrincipalTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/PrincipalTest.php
@@ -1,31 +1,10 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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 Julius Härtl <jus@bitgrid.net>
- * @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: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Tests\unit\Connector\Sabre;
@@ -34,6 +13,10 @@ use OC\User\User;
use OCA\DAV\CalDAV\Proxy\Proxy;
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
use OCA\DAV\Connector\Sabre\Principal;
+use OCP\Accounts\IAccount;
+use OCP\Accounts\IAccountManager;
+use OCP\Accounts\IAccountProperty;
+use OCP\Accounts\IAccountPropertyCollection;
use OCP\App\IAppManager;
use OCP\IConfig;
use OCP\IGroup;
@@ -49,38 +32,24 @@ use Sabre\DAV\PropPatch;
use Test\TestCase;
class PrincipalTest extends TestCase {
-
- /** @var IUserManager | MockObject */
- private $userManager;
-
- /** @var Principal */
- private $connector;
-
- /** @var IGroupManager | MockObject */
- private $groupManager;
-
- /** @var IManager | MockObject */
- private $shareManager;
-
- /** @var IUserSession | MockObject */
- private $userSession;
-
- /** @var IAppManager | MockObject */
- private $appManager;
-
- /** @var ProxyMapper | MockObject */
- private $proxyMapper;
-
- /** @var KnownUserService|MockObject */
- private $knownUserService;
- /** @var IConfig | MockObject */
- private $config;
- /** @var IFactory|MockObject */
- private $languageFactory;
+ private IUserManager&MockObject $userManager;
+ private IGroupManager&MockObject $groupManager;
+ private IAccountManager&MockObject $accountManager;
+ private IManager&MockObject $shareManager;
+ private IUserSession&MockObject $userSession;
+ private IAppManager&MockObject $appManager;
+ private ProxyMapper&MockObject $proxyMapper;
+ private KnownUserService&MockObject $knownUserService;
+ private IConfig&MockObject $config;
+ private IFactory&MockObject $languageFactory;
+ private Principal $connector;
protected function setUp(): void {
+ parent::setUp();
+
$this->userManager = $this->createMock(IUserManager::class);
$this->groupManager = $this->createMock(IGroupManager::class);
+ $this->accountManager = $this->createMock(IAccountManager::class);
$this->shareManager = $this->createMock(IManager::class);
$this->userSession = $this->createMock(IUserSession::class);
$this->appManager = $this->createMock(IAppManager::class);
@@ -92,6 +61,7 @@ class PrincipalTest extends TestCase {
$this->connector = new Principal(
$this->userManager,
$this->groupManager,
+ $this->accountManager,
$this->shareManager,
$this->userSession,
$this->appManager,
@@ -100,7 +70,6 @@ class PrincipalTest extends TestCase {
$this->config,
$this->languageFactory
);
- parent::setUp();
}
public function testGetPrincipalsByPrefixWithoutPrefix(): void {
@@ -111,26 +80,26 @@ class PrincipalTest extends TestCase {
public function testGetPrincipalsByPrefixWithUsers(): void {
$fooUser = $this->createMock(User::class);
$fooUser
- ->expects($this->once())
- ->method('getUID')
- ->willReturn('foo');
+ ->expects($this->once())
+ ->method('getUID')
+ ->willReturn('foo');
$fooUser
- ->expects($this->once())
- ->method('getDisplayName')
- ->willReturn('Dr. Foo-Bar');
+ ->expects($this->once())
+ ->method('getDisplayName')
+ ->willReturn('Dr. Foo-Bar');
$fooUser
- ->expects($this->once())
- ->method('getSystemEMailAddress')
- ->willReturn('');
+ ->expects($this->once())
+ ->method('getSystemEMailAddress')
+ ->willReturn('');
$barUser = $this->createMock(User::class);
$barUser
->expects($this->once())
->method('getUID')
->willReturn('bar');
$barUser
- ->expects($this->once())
- ->method('getSystemEMailAddress')
- ->willReturn('bar@nextcloud.com');
+ ->expects($this->once())
+ ->method('getSystemEMailAddress')
+ ->willReturn('bar@nextcloud.com');
$this->userManager
->expects($this->once())
->method('search')
@@ -140,8 +109,47 @@ class PrincipalTest extends TestCase {
$this->languageFactory
->expects($this->exactly(2))
->method('getUserLanguage')
- ->withConsecutive([$fooUser], [$barUser])
- ->willReturnOnConsecutiveCalls('de', 'en');
+ ->willReturnMap([
+ [$fooUser, 'de'],
+ [$barUser, 'en'],
+ ]);
+
+ $fooAccountPropertyCollection = $this->createMock(IAccountPropertyCollection::class);
+ $fooAccountPropertyCollection->expects($this->once())
+ ->method('getProperties')
+ ->willReturn([]);
+ $fooAccount = $this->createMock(IAccount::class);
+ $fooAccount->expects($this->once())
+ ->method('getPropertyCollection')
+ ->with(IAccountManager::COLLECTION_EMAIL)
+ ->willReturn($fooAccountPropertyCollection);
+
+ $emailPropertyOne = $this->createMock(IAccountProperty::class);
+ $emailPropertyOne->expects($this->once())
+ ->method('getValue')
+ ->willReturn('alias@nextcloud.com');
+ $emailPropertyTwo = $this->createMock(IAccountProperty::class);
+ $emailPropertyTwo->expects($this->once())
+ ->method('getValue')
+ ->willReturn('alias2@nextcloud.com');
+
+ $barAccountPropertyCollection = $this->createMock(IAccountPropertyCollection::class);
+ $barAccountPropertyCollection->expects($this->once())
+ ->method('getProperties')
+ ->willReturn([$emailPropertyOne, $emailPropertyTwo]);
+ $barAccount = $this->createMock(IAccount::class);
+ $barAccount->expects($this->once())
+ ->method('getPropertyCollection')
+ ->with(IAccountManager::COLLECTION_EMAIL)
+ ->willReturn($barAccountPropertyCollection);
+
+ $this->accountManager
+ ->expects($this->exactly(2))
+ ->method('getAccount')
+ ->willReturnMap([
+ [$fooUser, $fooAccount],
+ [$barUser, $barAccount],
+ ]);
$expectedResponse = [
0 => [
@@ -156,6 +164,7 @@ class PrincipalTest extends TestCase {
'{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'INDIVIDUAL',
'{http://nextcloud.com/ns}language' => 'en',
'{http://sabredav.org/ns}email-address' => 'bar@nextcloud.com',
+ '{DAV:}alternate-URI-set' => ['mailto:alias@nextcloud.com', 'mailto:alias2@nextcloud.com']
]
];
$response = $this->connector->getPrincipalsByPrefix('principals/users');
@@ -204,13 +213,13 @@ class PrincipalTest extends TestCase {
public function testGetPrincipalsByPathWithMail(): void {
$fooUser = $this->createMock(User::class);
$fooUser
- ->expects($this->once())
- ->method('getSystemEMailAddress')
- ->willReturn('foo@nextcloud.com');
+ ->expects($this->once())
+ ->method('getSystemEMailAddress')
+ ->willReturn('foo@nextcloud.com');
$fooUser
- ->expects($this->once())
- ->method('getUID')
- ->willReturn('foo');
+ ->expects($this->once())
+ ->method('getUID')
+ ->willReturn('foo');
$this->userManager
->expects($this->once())
->method('get')
@@ -409,22 +418,19 @@ class PrincipalTest extends TestCase {
->method('getUID')
->willReturn('bar');
$this->userManager
- ->expects($this->at(0))
- ->method('get')
- ->with('foo')
- ->willReturn($fooUser);
- $this->userManager
- ->expects($this->at(1))
+ ->expects($this->exactly(2))
->method('get')
- ->with('bar')
- ->willReturn($barUser);
+ ->willReturnMap([
+ ['foo', $fooUser],
+ ['bar', $barUser],
+ ]);
- $this->proxyMapper->expects($this->at(0))
+ $this->proxyMapper->expects($this->once())
->method('getProxiesOf')
->with('principals/users/foo')
->willReturn([]);
- $this->proxyMapper->expects($this->at(1))
+ $this->proxyMapper->expects($this->once())
->method('insert')
->with($this->callback(function ($proxy) {
/** @var Proxy $proxy */
@@ -457,14 +463,14 @@ class PrincipalTest extends TestCase {
['{http://sabredav.org/ns}email-address' => 'foo']));
}
- /**
- * @dataProvider searchPrincipalsDataProvider
- */
- public function testSearchPrincipals($sharingEnabled, $groupsOnly, $test, $result): void {
+ #[\PHPUnit\Framework\Attributes\DataProvider('searchPrincipalsDataProvider')]
+ public function testSearchPrincipals(bool $sharingEnabled, bool $groupsOnly, string $test, array $result): void {
$this->shareManager->expects($this->once())
->method('shareAPIEnabled')
->willReturn($sharingEnabled);
+ $getUserGroupIdsReturnMap = [];
+
if ($sharingEnabled) {
$this->shareManager->expects($this->once())
->method('allowEnumeration')
@@ -480,10 +486,7 @@ class PrincipalTest extends TestCase {
->method('getUser')
->willReturn($user);
- $this->groupManager->expects($this->at(0))
- ->method('getUserGroupIds')
- ->with($user)
- ->willReturn(['group1', 'group2', 'group5']);
+ $getUserGroupIdsReturnMap[] = [$user, ['group1', 'group2', 'group5']];
}
} else {
$this->config->expects($this->never())
@@ -502,12 +505,12 @@ class PrincipalTest extends TestCase {
$user4->method('getUID')->willReturn('user4');
if ($sharingEnabled) {
- $this->userManager->expects($this->at(0))
+ $this->userManager->expects($this->once())
->method('getByEmail')
->with('user@example.com')
->willReturn([$user2, $user3]);
- $this->userManager->expects($this->at(1))
+ $this->userManager->expects($this->once())
->method('searchDisplayName')
->with('User 12')
->willReturn([$user3, $user4]);
@@ -520,31 +523,22 @@ class PrincipalTest extends TestCase {
}
if ($sharingEnabled && $groupsOnly) {
- $this->groupManager->expects($this->at(1))
- ->method('getUserGroupIds')
- ->with($user2)
- ->willReturn(['group1', 'group3']);
- $this->groupManager->expects($this->at(2))
- ->method('getUserGroupIds')
- ->with($user3)
- ->willReturn(['group3', 'group4']);
- $this->groupManager->expects($this->at(3))
- ->method('getUserGroupIds')
- ->with($user3)
- ->willReturn(['group3', 'group4']);
- $this->groupManager->expects($this->at(4))
- ->method('getUserGroupIds')
- ->with($user4)
- ->willReturn(['group4', 'group5']);
+ $getUserGroupIdsReturnMap[] = [$user2, ['group1', 'group3']];
+ $getUserGroupIdsReturnMap[] = [$user3, ['group3', 'group4']];
+ $getUserGroupIdsReturnMap[] = [$user4, ['group4', 'group5']];
}
+ $this->groupManager->expects($this->any())
+ ->method('getUserGroupIds')
+ ->willReturnMap($getUserGroupIdsReturnMap);
+
$this->assertEquals($result, $this->connector->searchPrincipals('principals/users',
['{http://sabredav.org/ns}email-address' => 'user@example.com',
'{DAV:}displayname' => 'User 12'], $test));
}
- public function searchPrincipalsDataProvider(): array {
+ public static function searchPrincipalsDataProvider(): array {
return [
[true, false, 'allof', ['principals/users/user3']],
[true, false, 'anyof', ['principals/users/user2', 'principals/users/user3', 'principals/users/user4']],
@@ -573,7 +567,7 @@ class PrincipalTest extends TestCase {
$user3 = $this->createMock(IUser::class);
$user3->method('getUID')->willReturn('user3');
- $this->userManager->expects($this->at(0))
+ $this->userManager->expects($this->once())
->method('getByEmail')
->with('user@example.com')
->willReturn([$user2, $user3]);
@@ -608,14 +602,14 @@ class PrincipalTest extends TestCase {
$user2->method('getSystemEMailAddress')->willReturn('user2@foo.bar');
$user3 = $this->createMock(IUser::class);
$user3->method('getUID')->willReturn('user3');
- $user2->method('getDisplayName')->willReturn('User 22');
- $user2->method('getSystemEMailAddress')->willReturn('user2@foo.bar123');
+ $user3->method('getDisplayName')->willReturn('User 22');
+ $user3->method('getSystemEMailAddress')->willReturn('user2@foo.bar123');
$user4 = $this->createMock(IUser::class);
$user4->method('getUID')->willReturn('user4');
- $user2->method('getDisplayName')->willReturn('User 222');
- $user2->method('getSystemEMailAddress')->willReturn('user2@foo.bar456');
+ $user4->method('getDisplayName')->willReturn('User 222');
+ $user4->method('getSystemEMailAddress')->willReturn('user2@foo.bar456');
- $this->userManager->expects($this->at(0))
+ $this->userManager->expects($this->once())
->method('searchDisplayName')
->with('User 2')
->willReturn([$user2, $user3, $user4]);
@@ -662,6 +656,10 @@ class PrincipalTest extends TestCase {
->method('allowEnumerationFullMatch')
->willReturn(true);
+ $this->shareManager->expects($this->once())
+ ->method('matchEmail')
+ ->willReturn(true);
+
$user2 = $this->createMock(IUser::class);
$user2->method('getUID')->willReturn('user2');
$user2->method('getDisplayName')->willReturn('User 2');
@@ -707,15 +705,15 @@ class PrincipalTest extends TestCase {
}
public function testSearchPrincipalWithEnumerationLimitedDisplayname(): void {
- $this->shareManager->expects($this->at(0))
+ $this->shareManager->expects($this->once())
->method('shareAPIEnabled')
->willReturn(true);
- $this->shareManager->expects($this->at(1))
+ $this->shareManager->expects($this->once())
->method('allowEnumeration')
->willReturn(true);
- $this->shareManager->expects($this->at(2))
+ $this->shareManager->expects($this->once())
->method('limitEnumerationToGroups')
->willReturn(true);
@@ -737,24 +735,19 @@ class PrincipalTest extends TestCase {
$user4->method('getSystemEMailAddress')->willReturn('user2@foo.bar456');
- $this->userSession->expects($this->at(0))
+ $this->userSession->expects($this->once())
->method('getUser')
->willReturn($user2);
- $this->groupManager->expects($this->at(0))
- ->method('getUserGroupIds')
- ->willReturn(['group1']);
- $this->groupManager->expects($this->at(1))
- ->method('getUserGroupIds')
- ->willReturn(['group1']);
- $this->groupManager->expects($this->at(2))
+ $this->groupManager->expects($this->exactly(4))
->method('getUserGroupIds')
- ->willReturn(['group1']);
- $this->groupManager->expects($this->at(3))
- ->method('getUserGroupIds')
- ->willReturn(['group2']);
+ ->willReturnMap([
+ [$user2, ['group1']],
+ [$user3, ['group1']],
+ [$user4, ['group2']],
+ ]);
- $this->userManager->expects($this->at(0))
+ $this->userManager->expects($this->once())
->method('searchDisplayName')
->with('User')
->willReturn([$user2, $user3, $user4]);
@@ -768,15 +761,15 @@ class PrincipalTest extends TestCase {
}
public function testSearchPrincipalWithEnumerationLimitedMail(): void {
- $this->shareManager->expects($this->at(0))
+ $this->shareManager->expects($this->once())
->method('shareAPIEnabled')
->willReturn(true);
- $this->shareManager->expects($this->at(1))
+ $this->shareManager->expects($this->once())
->method('allowEnumeration')
->willReturn(true);
- $this->shareManager->expects($this->at(2))
+ $this->shareManager->expects($this->once())
->method('limitEnumerationToGroups')
->willReturn(true);
@@ -798,24 +791,19 @@ class PrincipalTest extends TestCase {
$user4->method('getSystemEMailAddress')->willReturn('user2@foo.bar456');
- $this->userSession->expects($this->at(0))
+ $this->userSession->expects($this->once())
->method('getUser')
->willReturn($user2);
- $this->groupManager->expects($this->at(0))
- ->method('getUserGroupIds')
- ->willReturn(['group1']);
- $this->groupManager->expects($this->at(1))
- ->method('getUserGroupIds')
- ->willReturn(['group1']);
- $this->groupManager->expects($this->at(2))
+ $this->groupManager->expects($this->exactly(4))
->method('getUserGroupIds')
- ->willReturn(['group1']);
- $this->groupManager->expects($this->at(3))
- ->method('getUserGroupIds')
- ->willReturn(['group2']);
+ ->willReturnMap([
+ [$user2, ['group1']],
+ [$user3, ['group1']],
+ [$user4, ['group2']],
+ ]);
- $this->userManager->expects($this->at(0))
+ $this->userManager->expects($this->once())
->method('getByEmail')
->with('user')
->willReturn([$user2, $user3, $user4]);
@@ -836,27 +824,20 @@ class PrincipalTest extends TestCase {
$this->assertEquals(null, $this->connector->findByUri('mailto:user@foo.com', 'principals/users'));
}
- /**
- * @dataProvider findByUriWithGroupRestrictionDataProvider
- */
- public function testFindByUriWithGroupRestriction($uri, $email, $expects): void {
+ #[\PHPUnit\Framework\Attributes\DataProvider('findByUriWithGroupRestrictionDataProvider')]
+ public function testFindByUriWithGroupRestriction(string $uri, string $email, ?string $expects): void {
$this->shareManager->expects($this->once())
->method('shareApiEnabled')
->willReturn(true);
$this->shareManager->expects($this->once())
- ->method('shareWithGroupMembersOnly')
- ->willReturn(true);
+ ->method('shareWithGroupMembersOnly')
+ ->willReturn(true);
$user = $this->createMock(IUser::class);
$this->userSession->expects($this->once())
- ->method('getUser')
- ->willReturn($user);
-
- $this->groupManager->expects($this->at(0))
- ->method('getUserGroupIds')
- ->with($user)
- ->willReturn(['group1', 'group2']);
+ ->method('getUser')
+ ->willReturn($user);
$user2 = $this->createMock(IUser::class);
$user2->method('getUID')->willReturn('user2');
@@ -864,43 +845,45 @@ class PrincipalTest extends TestCase {
$user3->method('getUID')->willReturn('user3');
$this->userManager->expects($this->once())
- ->method('getByEmail')
- ->with($email)
- ->willReturn([$email === 'user2@foo.bar' ? $user2 : $user3]);
+ ->method('getByEmail')
+ ->with($email)
+ ->willReturn([$email === 'user2@foo.bar' ? $user2 : $user3]);
if ($email === 'user2@foo.bar') {
- $this->groupManager->expects($this->at(1))
- ->method('getUserGroupIds')
- ->with($user2)
- ->willReturn(['group1', 'group3']);
+ $this->groupManager->expects($this->exactly(2))
+ ->method('getUserGroupIds')
+ ->willReturnMap([
+ [$user, ['group1', 'group2']],
+ [$user2, ['group1', 'group3']],
+ ]);
} else {
- $this->groupManager->expects($this->at(1))
- ->method('getUserGroupIds')
- ->with($user3)
- ->willReturn(['group3', 'group3']);
+ $this->groupManager->expects($this->exactly(2))
+ ->method('getUserGroupIds')
+ ->willReturnMap([
+ [$user, ['group1', 'group2']],
+ [$user3, ['group3', 'group3']],
+ ]);
}
$this->assertEquals($expects, $this->connector->findByUri($uri, 'principals/users'));
}
- public function findByUriWithGroupRestrictionDataProvider(): array {
+ public static function findByUriWithGroupRestrictionDataProvider(): array {
return [
['mailto:user2@foo.bar', 'user2@foo.bar', 'principals/users/user2'],
['mailto:user3@foo.bar', 'user3@foo.bar', null],
];
}
- /**
- * @dataProvider findByUriWithoutGroupRestrictionDataProvider
- */
- public function testFindByUriWithoutGroupRestriction($uri, $email, $expects): void {
+ #[\PHPUnit\Framework\Attributes\DataProvider('findByUriWithoutGroupRestrictionDataProvider')]
+ public function testFindByUriWithoutGroupRestriction(string $uri, string $email, string $expects): void {
$this->shareManager->expects($this->once())
->method('shareApiEnabled')
->willReturn(true);
$this->shareManager->expects($this->once())
- ->method('shareWithGroupMembersOnly')
- ->willReturn(false);
+ ->method('shareWithGroupMembersOnly')
+ ->willReturn(false);
$user2 = $this->createMock(IUser::class);
$user2->method('getUID')->willReturn('user2');
@@ -908,17 +891,47 @@ class PrincipalTest extends TestCase {
$user3->method('getUID')->willReturn('user3');
$this->userManager->expects($this->once())
- ->method('getByEmail')
- ->with($email)
- ->willReturn([$email === 'user2@foo.bar' ? $user2 : $user3]);
+ ->method('getByEmail')
+ ->with($email)
+ ->willReturn([$email === 'user2@foo.bar' ? $user2 : $user3]);
$this->assertEquals($expects, $this->connector->findByUri($uri, 'principals/users'));
}
- public function findByUriWithoutGroupRestrictionDataProvider(): array {
+ public static function findByUriWithoutGroupRestrictionDataProvider(): array {
return [
['mailto:user2@foo.bar', 'user2@foo.bar', 'principals/users/user2'],
['mailto:user3@foo.bar', 'user3@foo.bar', 'principals/users/user3'],
];
}
+
+ public function testGetEmailAddressesOfPrincipal(): void {
+ $principal = [
+ '{http://sabredav.org/ns}email-address' => 'bar@company.org',
+ '{DAV:}alternate-URI-set' => [
+ '/some/url',
+ 'mailto:foo@bar.com',
+ 'mailto:duplicate@example.com',
+ ],
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => [
+ 'mailto:bernard@example.com',
+ 'mailto:bernard.desruisseaux@example.com',
+ ],
+ '{http://calendarserver.org/ns/}email-address-set' => [
+ 'mailto:duplicate@example.com',
+ 'mailto:user@some.org',
+ ],
+ ];
+
+ $expected = [
+ 'bar@company.org',
+ 'foo@bar.com',
+ 'duplicate@example.com',
+ 'bernard@example.com',
+ 'bernard.desruisseaux@example.com',
+ 'user@some.org',
+ ];
+ $actual = $this->connector->getEmailAddressesOfPrincipal($principal);
+ $this->assertEquals($expected, $actual);
+ }
}
diff --git a/apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php
new file mode 100644
index 00000000000..b528c3d731c
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php
@@ -0,0 +1,123 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace unit\Connector\Sabre;
+
+use OCA\DAV\Connector\Sabre\PropFindMonitorPlugin;
+use OCA\DAV\Connector\Sabre\Server;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\HTTP\Request;
+use Sabre\HTTP\Response;
+use Test\TestCase;
+
+class PropFindMonitorPluginTest extends TestCase {
+
+ private PropFindMonitorPlugin $plugin;
+ private Server&MockObject $server;
+ private LoggerInterface&MockObject $logger;
+ private Request&MockObject $request;
+ private Response&MockObject $response;
+
+ public static function dataTest(): array {
+ $minQueriesTrigger = PropFindMonitorPlugin::THRESHOLD_QUERY_FACTOR
+ * PropFindMonitorPlugin::THRESHOLD_NODES;
+ return [
+ 'No queries logged' => [[], 0],
+ 'Plugins with queries in less than threshold nodes should not be logged' => [
+ [
+ [
+ 'PluginName' => ['queries' => 100, 'nodes'
+ => PropFindMonitorPlugin::THRESHOLD_NODES - 1]
+ ],
+ [],
+ ],
+ 0
+ ],
+ 'Plugins with query-to-node ratio less than threshold should not be logged' => [
+ [
+ [
+ 'PluginName' => [
+ 'queries' => $minQueriesTrigger - 1,
+ 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES ],
+ ],
+ [],
+ ],
+ 0
+ ],
+ 'Plugins with more nodes scanned than queries executed should not be logged' => [
+ [
+ [
+ 'PluginName' => [
+ 'queries' => $minQueriesTrigger,
+ 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES * 2],
+ ],
+ [],
+ ],
+ 0
+ ],
+ 'Plugins with queries only in highest depth level should not be logged' => [
+ [
+ [
+ 'PluginName' => [
+ 'queries' => $minQueriesTrigger,
+ 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES - 1
+ ]
+ ],
+ [
+ 'PluginName' => [
+ 'queries' => $minQueriesTrigger * 2,
+ 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES
+ ]
+ ]
+ ],
+ 0
+ ],
+ 'Plugins with too many queries should be logged' => [
+ [
+ [
+ 'FirstPlugin' => [
+ 'queries' => $minQueriesTrigger,
+ 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES,
+ ],
+ 'SecondPlugin' => [
+ 'queries' => $minQueriesTrigger,
+ 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES,
+ ]
+ ],
+ []
+ ],
+ 2
+ ]
+ ];
+ }
+
+ /**
+ * @dataProvider dataTest
+ */
+ public function test(array $queries, $expectedLogCalls): void {
+ $this->plugin->initialize($this->server);
+ $this->server->expects($this->once())->method('getPluginQueries')
+ ->willReturn($queries);
+
+ $this->server->expects(empty($queries) ? $this->never() : $this->once())
+ ->method('getLogger')
+ ->willReturn($this->logger);
+
+ $this->logger->expects($this->exactly($expectedLogCalls))->method('error');
+ $this->plugin->afterResponse($this->request, $this->response);
+ }
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->plugin = new PropFindMonitorPlugin();
+ $this->server = $this->createMock(Server::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->request = $this->createMock(Request::class);
+ $this->response = $this->createMock(Response::class);
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/PropfindCompressionPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/PropfindCompressionPluginTest.php
index e048190e633..e6f696ed160 100644
--- a/apps/dav/tests/unit/Connector/Sabre/PropfindCompressionPluginTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/PropfindCompressionPluginTest.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\Tests\unit\Connector\Sabre;
@@ -31,8 +14,7 @@ use Sabre\HTTP\Response;
use Test\TestCase;
class PropfindCompressionPluginTest extends TestCase {
- /** @var PropfindCompressionPlugin */
- private $plugin;
+ private PropfindCompressionPlugin $plugin;
protected function setUp(): void {
parent::setUp();
@@ -40,7 +22,7 @@ class PropfindCompressionPluginTest extends TestCase {
$this->plugin = new PropfindCompressionPlugin();
}
- public function testNoHeader() {
+ public function testNoHeader(): void {
$request = $this->createMock(Request::class);
$response = $this->createMock(Response::class);
@@ -55,7 +37,7 @@ class PropfindCompressionPluginTest extends TestCase {
$this->assertSame($response, $result);
}
- public function testHeaderButNoGzip() {
+ public function testHeaderButNoGzip(): void {
$request = $this->createMock(Request::class);
$response = $this->createMock(Response::class);
@@ -70,7 +52,7 @@ class PropfindCompressionPluginTest extends TestCase {
$this->assertSame($response, $result);
}
- public function testHeaderGzipButNoStringBody() {
+ public function testHeaderGzipButNoStringBody(): void {
$request = $this->createMock(Request::class);
$response = $this->createMock(Response::class);
@@ -86,7 +68,7 @@ class PropfindCompressionPluginTest extends TestCase {
}
- public function testProperGzip() {
+ public function testProperGzip(): void {
$request = $this->createMock(Request::class);
$response = $this->createMock(Response::class);
diff --git a/apps/dav/tests/unit/Connector/Sabre/PublicAuthTest.php b/apps/dav/tests/unit/Connector/Sabre/PublicAuthTest.php
new file mode 100644
index 00000000000..fef62b51c67
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/PublicAuthTest.php
@@ -0,0 +1,384 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\Connector;
+
+use OCA\DAV\Connector\Sabre\PublicAuth;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\IURLGenerator;
+use OCP\Security\Bruteforce\IThrottler;
+use OCP\Share\Exceptions\ShareNotFound;
+use OCP\Share\IManager;
+use OCP\Share\IShare;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Class PublicAuthTest
+ *
+ * @group DB
+ *
+ * @package OCA\DAV\Tests\unit\Connector
+ */
+class PublicAuthTest extends \Test\TestCase {
+
+ private ISession&MockObject $session;
+ private IRequest&MockObject $request;
+ private IManager&MockObject $shareManager;
+ private IThrottler&MockObject $throttler;
+ private LoggerInterface&MockObject $logger;
+ private IURLGenerator&MockObject $urlGenerator;
+ private PublicAuth $auth;
+
+ private bool|string $oldUser;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->session = $this->createMock(ISession::class);
+ $this->request = $this->createMock(IRequest::class);
+ $this->shareManager = $this->createMock(IManager::class);
+ $this->throttler = $this->createMock(IThrottler::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+
+ $this->auth = new PublicAuth(
+ $this->request,
+ $this->shareManager,
+ $this->session,
+ $this->throttler,
+ $this->logger,
+ $this->urlGenerator,
+ );
+
+ // Store current user
+ $this->oldUser = \OC_User::getUser();
+ }
+
+ protected function tearDown(): void {
+ \OC_User::setIncognitoMode(false);
+
+ // Set old user
+ \OC_User::setUserId($this->oldUser);
+ if ($this->oldUser !== false) {
+ \OC_Util::setupFS($this->oldUser);
+ }
+
+ parent::tearDown();
+ }
+
+ public function testGetToken(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $result = self::invokePrivate($this->auth, 'getToken');
+
+ $this->assertSame('GX9HSGQrGE', $result);
+ }
+
+ public function testGetTokenInvalid(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files');
+
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+ self::invokePrivate($this->auth, 'getToken');
+ }
+
+ public function testCheckTokenValidShare(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn(null);
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $result = self::invokePrivate($this->auth, 'checkToken');
+ $this->assertSame([true, 'principals/GX9HSGQrGE'], $result);
+ }
+
+ public function testCheckTokenInvalidShare(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $this->shareManager
+ ->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willThrowException(new ShareNotFound());
+
+ $this->expectException(\Sabre\DAV\Exception\NotFound::class);
+ self::invokePrivate($this->auth, 'checkToken');
+ }
+
+ public function testCheckTokenAlreadyAuthenticated(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getShareType')->willReturn(42);
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $this->session->method('exists')->with('public_link_authenticated')->willReturn(true);
+ $this->session->method('get')->with('public_link_authenticated')->willReturn('42');
+
+ $result = self::invokePrivate($this->auth, 'checkToken');
+ $this->assertSame([true, 'principals/GX9HSGQrGE'], $result);
+ }
+
+ public function testCheckTokenPasswordNotAuthenticated(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(42);
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $this->session->method('exists')->with('public_link_authenticated')->willReturn(false);
+
+ $this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class);
+ self::invokePrivate($this->auth, 'checkToken');
+ }
+
+ public function testCheckTokenPasswordAuthenticatedWrongShare(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(42);
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $this->session->method('exists')->with('public_link_authenticated')->willReturn(false);
+ $this->session->method('get')->with('public_link_authenticated')->willReturn('43');
+
+ $this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class);
+ self::invokePrivate($this->auth, 'checkToken');
+ }
+
+ public function testNoShare(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willThrowException(new ShareNotFound());
+
+ $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertFalse($result);
+ }
+
+ public function testShareNoPassword(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn(null);
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertTrue($result);
+ }
+
+ public function testSharePasswordFancyShareType(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(42);
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertFalse($result);
+ }
+
+
+ public function testSharePasswordRemote(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(IShare::TYPE_REMOTE);
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertTrue($result);
+ }
+
+ public function testSharePasswordLinkValidPassword(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(IShare::TYPE_LINK);
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $this->shareManager->expects($this->once())
+ ->method('checkPassword')->with(
+ $this->equalTo($share),
+ $this->equalTo('password')
+ )->willReturn(true);
+
+ $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertTrue($result);
+ }
+
+ public function testSharePasswordMailValidPassword(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(IShare::TYPE_EMAIL);
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $this->shareManager->expects($this->once())
+ ->method('checkPassword')->with(
+ $this->equalTo($share),
+ $this->equalTo('password')
+ )->willReturn(true);
+
+ $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertTrue($result);
+ }
+
+ public function testInvalidSharePasswordLinkValidSession(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(IShare::TYPE_LINK);
+ $share->method('getId')->willReturn('42');
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $this->shareManager->expects($this->once())
+ ->method('checkPassword')
+ ->with(
+ $this->equalTo($share),
+ $this->equalTo('password')
+ )->willReturn(false);
+
+ $this->session->method('exists')->with('public_link_authenticated')->willReturn(true);
+ $this->session->method('get')->with('public_link_authenticated')->willReturn('42');
+
+ $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertTrue($result);
+ }
+
+ public function testSharePasswordLinkInvalidSession(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(IShare::TYPE_LINK);
+ $share->method('getId')->willReturn('42');
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $this->shareManager->expects($this->once())
+ ->method('checkPassword')
+ ->with(
+ $this->equalTo($share),
+ $this->equalTo('password')
+ )->willReturn(false);
+
+ $this->session->method('exists')->with('public_link_authenticated')->willReturn(true);
+ $this->session->method('get')->with('public_link_authenticated')->willReturn('43');
+
+ $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertFalse($result);
+ }
+
+
+ public function testSharePasswordMailInvalidSession(): void {
+ $this->request->method('getPathInfo')
+ ->willReturn('/dav/files/GX9HSGQrGE');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getPassword')->willReturn('password');
+ $share->method('getShareType')->willReturn(IShare::TYPE_EMAIL);
+ $share->method('getId')->willReturn('42');
+
+ $this->shareManager->expects($this->once())
+ ->method('getShareByToken')
+ ->with('GX9HSGQrGE')
+ ->willReturn($share);
+
+ $this->shareManager->expects($this->once())
+ ->method('checkPassword')
+ ->with(
+ $this->equalTo($share),
+ $this->equalTo('password')
+ )->willReturn(false);
+
+ $this->session->method('exists')->with('public_link_authenticated')->willReturn(true);
+ $this->session->method('get')->with('public_link_authenticated')->willReturn('43');
+
+ $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);
+
+ $this->assertFalse($result);
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/QuotaPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/QuotaPluginTest.php
index 69b2140e640..6fe2d6ccabe 100644
--- a/apps/dav/tests/unit/Connector/Sabre/QuotaPluginTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/QuotaPluginTest.php
@@ -1,32 +1,10 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * Copyright (c) 2013 Thomas Müller <thomas.mueller@tmit.eu>
- * Copyright (c) 2013 Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @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 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: 2013-2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Tests\unit\Connector\Sabre;
@@ -35,83 +13,56 @@ use OCA\DAV\Connector\Sabre\QuotaPlugin;
use OCP\Files\FileInfo;
use Test\TestCase;
-/**
- * Copyright (c) 2013 Thomas Müller <thomas.mueller@tmit.eu>
- * This file is licensed under the Affero General Public License version 3 or
- * later.
- * See the COPYING-README file.
- */
class QuotaPluginTest extends TestCase {
+ private \Sabre\DAV\Server $server;
- /** @var \Sabre\DAV\Server | \PHPUnit\Framework\MockObject\MockObject */
- private $server;
-
- /** @var \OCA\DAV\Connector\Sabre\QuotaPlugin | \PHPUnit\Framework\MockObject\MockObject */
- private $plugin;
+ private QuotaPlugin $plugin;
- private function init($quota, $checkedPath = '') {
- $view = $this->buildFileViewMock($quota, $checkedPath);
+ private function init(int $quota, string $checkedPath = ''): void {
+ $view = $this->buildFileViewMock((string)$quota, $checkedPath);
$this->server = new \Sabre\DAV\Server();
- $this->plugin = $this->getMockBuilder(QuotaPlugin::class)
- ->setConstructorArgs([$view])
- ->setMethods(['getFileChunking'])
- ->getMock();
+ $this->plugin = new QuotaPlugin($view);
$this->plugin->initialize($this->server);
}
- /**
- * @dataProvider lengthProvider
- */
- public function testLength($expected, $headers) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('lengthProvider')]
+ public function testLength(?int $expected, array $headers): void {
$this->init(0);
- $this->plugin->expects($this->never())
- ->method('getFileChunking');
+
$this->server->httpRequest = new \Sabre\HTTP\Request('POST', 'dummy.file', $headers);
$length = $this->plugin->getLength();
$this->assertEquals($expected, $length);
}
- /**
- * @dataProvider quotaOkayProvider
- */
- public function testCheckQuota($quota, $headers) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('quotaOkayProvider')]
+ public function testCheckQuota(int $quota, array $headers): void {
$this->init($quota);
- $this->plugin->expects($this->never())
- ->method('getFileChunking');
$this->server->httpRequest = new \Sabre\HTTP\Request('POST', 'dummy.file', $headers);
$result = $this->plugin->checkQuota('');
$this->assertTrue($result);
}
- /**
- * @dataProvider quotaExceededProvider
- */
- public function testCheckExceededQuota($quota, $headers) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('quotaExceededProvider')]
+ public function testCheckExceededQuota(int $quota, array $headers): void {
$this->expectException(\Sabre\DAV\Exception\InsufficientStorage::class);
$this->init($quota);
- $this->plugin->expects($this->never())
- ->method('getFileChunking');
$this->server->httpRequest = new \Sabre\HTTP\Request('POST', 'dummy.file', $headers);
$this->plugin->checkQuota('');
}
- /**
- * @dataProvider quotaOkayProvider
- */
- public function testCheckQuotaOnPath($quota, $headers) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('quotaOkayProvider')]
+ public function testCheckQuotaOnPath(int $quota, array $headers): void {
$this->init($quota, 'sub/test.txt');
- $this->plugin->expects($this->never())
- ->method('getFileChunking');
$this->server->httpRequest = new \Sabre\HTTP\Request('POST', 'dummy.file', $headers);
$result = $this->plugin->checkQuota('/sub/test.txt');
$this->assertTrue($result);
}
- public function quotaOkayProvider() {
+ public static function quotaOkayProvider(): array {
return [
[1024, []],
[1024, ['X-EXPECTED-ENTITY-LENGTH' => '1024']],
@@ -130,7 +81,7 @@ class QuotaPluginTest extends TestCase {
];
}
- public function quotaExceededProvider() {
+ public static function quotaExceededProvider(): array {
return [
[1023, ['X-EXPECTED-ENTITY-LENGTH' => '1024']],
[511, ['CONTENT-LENGTH' => '512']],
@@ -138,7 +89,7 @@ class QuotaPluginTest extends TestCase {
];
}
- public function lengthProvider() {
+ public static function lengthProvider(): array {
return [
[null, []],
[1024, ['X-EXPECTED-ENTITY-LENGTH' => '1024']],
@@ -149,12 +100,12 @@ class QuotaPluginTest extends TestCase {
[null, ['CONTENT-LENGTH' => 'A']],
[1024, ['OC-TOTAL-LENGTH' => 'A', 'CONTENT-LENGTH' => '1024']],
[1024, ['OC-TOTAL-LENGTH' => 'A', 'X-EXPECTED-ENTITY-LENGTH' => '1024']],
- [null, ['OC-TOTAL-LENGTH' => '2048', 'X-EXPECTED-ENTITY-LENGTH' => 'A']],
- [null, ['OC-TOTAL-LENGTH' => '2048', 'CONTENT-LENGTH' => 'A']],
+ [2048, ['OC-TOTAL-LENGTH' => '2048', 'X-EXPECTED-ENTITY-LENGTH' => 'A']],
+ [2048, ['OC-TOTAL-LENGTH' => '2048', 'CONTENT-LENGTH' => 'A']],
];
}
- public function quotaChunkedOkProvider() {
+ public static function quotaChunkedOkProvider(): array {
return [
[1024, 0, ['X-EXPECTED-ENTITY-LENGTH' => '1024']],
[1024, 0, ['CONTENT-LENGTH' => '512']],
@@ -173,30 +124,7 @@ class QuotaPluginTest extends TestCase {
];
}
- /**
- * @dataProvider quotaChunkedOkProvider
- */
- public function testCheckQuotaChunkedOk($quota, $chunkTotalSize, $headers) {
- $this->init($quota, 'sub/test.txt');
-
- $mockChunking = $this->getMockBuilder(\OC_FileChunking::class)
- ->disableOriginalConstructor()
- ->getMock();
- $mockChunking->expects($this->once())
- ->method('getCurrentSize')
- ->willReturn($chunkTotalSize);
-
- $this->plugin->expects($this->once())
- ->method('getFileChunking')
- ->willReturn($mockChunking);
-
- $headers['OC-CHUNKED'] = 1;
- $this->server->httpRequest = new \Sabre\HTTP\Request('POST', 'dummy.file', $headers);
- $result = $this->plugin->checkQuota('/sub/test.txt-chunking-12345-3-1');
- $this->assertTrue($result);
- }
-
- public function quotaChunkedFailProvider() {
+ public static function quotaChunkedFailProvider(): array {
return [
[400, 0, ['X-EXPECTED-ENTITY-LENGTH' => '1024']],
[400, 0, ['CONTENT-LENGTH' => '512']],
@@ -208,39 +136,15 @@ class QuotaPluginTest extends TestCase {
];
}
- /**
- * @dataProvider quotaChunkedFailProvider
- */
- public function testCheckQuotaChunkedFail($quota, $chunkTotalSize, $headers) {
- $this->expectException(\Sabre\DAV\Exception\InsufficientStorage::class);
-
- $this->init($quota, 'sub/test.txt');
-
- $mockChunking = $this->getMockBuilder(\OC_FileChunking::class)
- ->disableOriginalConstructor()
- ->getMock();
- $mockChunking->expects($this->once())
- ->method('getCurrentSize')
- ->willReturn($chunkTotalSize);
-
- $this->plugin->expects($this->once())
- ->method('getFileChunking')
- ->willReturn($mockChunking);
-
- $headers['OC-CHUNKED'] = 1;
- $this->server->httpRequest = new \Sabre\HTTP\Request('POST', 'dummy.file', $headers);
- $this->plugin->checkQuota('/sub/test.txt-chunking-12345-3-1');
- }
-
- private function buildFileViewMock($quota, $checkedPath) {
- // mock filesysten
+ private function buildFileViewMock(string $quota, string $checkedPath): View {
+ // mock filesystem
$view = $this->getMockBuilder(View::class)
- ->setMethods(['free_space'])
+ ->onlyMethods(['free_space'])
->disableOriginalConstructor()
->getMock();
$view->expects($this->any())
->method('free_space')
- ->with($this->identicalTo($checkedPath))
+ ->with($checkedPath)
->willReturn($quota);
return $view;
diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/Auth.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/Auth.php
index 917c63038cb..b01807d5bbb 100644
--- a/apps/dav/tests/unit/Connector/Sabre/RequestTest/Auth.php
+++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/Auth.php
@@ -1,53 +1,30 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @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\Tests\unit\Connector\Sabre\RequestTest;
+use OCP\IUserSession;
+use OCP\Server;
use Sabre\DAV\Auth\Backend\BackendInterface;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class Auth implements BackendInterface {
/**
- * @var string
- */
- private $user;
-
- /**
- * @var string
- */
- private $password;
-
- /**
* Auth constructor.
*
* @param string $user
* @param string $password
*/
- public function __construct($user, $password) {
- $this->user = $user;
- $this->password = $password;
+ public function __construct(
+ private $user,
+ private $password,
+ ) {
}
/**
@@ -79,7 +56,7 @@ class Auth implements BackendInterface {
* @return array
*/
public function check(RequestInterface $request, ResponseInterface $response) {
- $userSession = \OC::$server->getUserSession();
+ $userSession = Server::get(IUserSession::class);
$result = $userSession->login($this->user, $this->password);
if ($result) {
//we need to pass the user name, which may differ from login name
@@ -89,7 +66,7 @@ class Auth implements BackendInterface {
\OC::$server->getUserFolder($user);
return [true, "principals/$user"];
}
- return [false, "login failed"];
+ return [false, 'login failed'];
}
/**
@@ -113,7 +90,7 @@ class Auth implements BackendInterface {
* @param ResponseInterface $response
* @return void
*/
- public function challenge(RequestInterface $request, ResponseInterface $response) {
+ public function challenge(RequestInterface $request, ResponseInterface $response): void {
// TODO: Implement challenge() method.
}
}
diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/DeleteTest.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/DeleteTest.php
index b76564e59d4..7d3488e6b5a 100644
--- a/apps/dav/tests/unit/Connector/Sabre/RequestTest/DeleteTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/DeleteTest.php
@@ -1,28 +1,15 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Joas Schilling <coding@schilljs.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\Tests\unit\Connector\Sabre\RequestTest;
use OCP\AppFramework\Http;
+use OCP\Files\FileInfo;
/**
* Class DeleteTest
@@ -32,8 +19,8 @@ use OCP\AppFramework\Http;
* @package OCA\DAV\Tests\unit\Connector\Sabre\RequestTest
*/
class DeleteTest extends RequestTestCase {
- public function testBasicUpload() {
- $user = $this->getUniqueID();
+ public function testBasicUpload(): void {
+ $user = self::getUniqueID();
$view = $this->setupUser($user, 'pass');
$view->file_put_contents('foo.txt', 'asd');
@@ -44,7 +31,7 @@ class DeleteTest extends RequestTestCase {
$mount->getStorage()->unlink($mount->getInternalPath($internalPath));
// cache entry still exists
- $this->assertInstanceOf('\OCP\Files\FileInfo', $view->getFileInfo('foo.txt'));
+ $this->assertInstanceOf(FileInfo::class, $view->getFileInfo('foo.txt'));
$response = $this->request($view, $user, 'pass', 'DELETE', '/foo.txt');
diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/DownloadTest.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/DownloadTest.php
index ceae6fadf28..34171963ef0 100644
--- a/apps/dav/tests/unit/Connector/Sabre/RequestTest/DownloadTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/DownloadTest.php
@@ -1,26 +1,10 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @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\Tests\unit\Connector\Sabre\RequestTest;
@@ -35,8 +19,8 @@ use OCP\Lock\ILockingProvider;
* @package OCA\DAV\Tests\unit\Connector\Sabre\RequestTest
*/
class DownloadTest extends RequestTestCase {
- public function testDownload() {
- $user = $this->getUniqueID();
+ public function testDownload(): void {
+ $user = self::getUniqueID();
$view = $this->setupUser($user, 'pass');
$view->file_put_contents('foo.txt', 'bar');
@@ -46,8 +30,8 @@ class DownloadTest extends RequestTestCase {
$this->assertEquals(stream_get_contents($response->getBody()), 'bar');
}
- public function testDownloadWriteLocked() {
- $user = $this->getUniqueID();
+ public function testDownloadWriteLocked(): void {
+ $user = self::getUniqueID();
$view = $this->setupUser($user, 'pass');
$view->file_put_contents('foo.txt', 'bar');
@@ -58,8 +42,8 @@ class DownloadTest extends RequestTestCase {
$this->assertEquals(Http::STATUS_LOCKED, $result->getStatus());
}
- public function testDownloadReadLocked() {
- $user = $this->getUniqueID();
+ public function testDownloadReadLocked(): void {
+ $user = self::getUniqueID();
$view = $this->setupUser($user, 'pass');
$view->file_put_contents('foo.txt', 'bar');
diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionMasterKeyUploadTest.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionMasterKeyUploadTest.php
index f0fd9e5a833..615490ddc92 100644
--- a/apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionMasterKeyUploadTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionMasterKeyUploadTest.php
@@ -1,27 +1,17 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- *
- * @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, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Tests\unit\Connector\Sabre\RequestTest;
use OC\Files\View;
+use OCP\IConfig;
+use OCP\ITempManager;
+use OCP\Server;
use Test\Traits\EncryptionTrait;
/**
@@ -34,12 +24,12 @@ use Test\Traits\EncryptionTrait;
class EncryptionMasterKeyUploadTest extends UploadTest {
use EncryptionTrait;
- protected function setupUser($name, $password) {
+ protected function setupUser($name, $password): View {
$this->createUser($name, $password);
- $tmpFolder = \OC::$server->getTempManager()->getTemporaryFolder();
+ $tmpFolder = Server::get(ITempManager::class)->getTemporaryFolder();
$this->registerMount($name, '\OC\Files\Storage\Local', '/' . $name, ['datadir' => $tmpFolder]);
// we use the master key
- \OC::$server->getConfig()->setAppValue('encryption', 'useMasterKey', '1');
+ Server::get(IConfig::class)->setAppValue('encryption', 'useMasterKey', '1');
$this->setupForUser($name, $password);
$this->loginWithEncryption($name);
return new View('/' . $name . '/files');
diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionUploadTest.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionUploadTest.php
index da7c35560cb..efa7bb54cf8 100644
--- a/apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionUploadTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionUploadTest.php
@@ -1,30 +1,17 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Joas Schilling <coding@schilljs.com>
- * @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\Tests\unit\Connector\Sabre\RequestTest;
use OC\Files\View;
+use OCP\IConfig;
+use OCP\ITempManager;
+use OCP\Server;
use Test\Traits\EncryptionTrait;
/**
@@ -37,12 +24,12 @@ use Test\Traits\EncryptionTrait;
class EncryptionUploadTest extends UploadTest {
use EncryptionTrait;
- protected function setupUser($name, $password) {
+ protected function setupUser($name, $password): View {
$this->createUser($name, $password);
- $tmpFolder = \OC::$server->getTempManager()->getTemporaryFolder();
+ $tmpFolder = Server::get(ITempManager::class)->getTemporaryFolder();
$this->registerMount($name, '\OC\Files\Storage\Local', '/' . $name, ['datadir' => $tmpFolder]);
// we use per-user keys
- \OC::$server->getConfig()->setAppValue('encryption', 'useMasterKey', '0');
+ Server::get(IConfig::class)->setAppValue('encryption', 'useMasterKey', '0');
$this->setupForUser($name, $password);
$this->loginWithEncryption($name);
return new View('/' . $name . '/files');
diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/ExceptionPlugin.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/ExceptionPlugin.php
index eb459912bf4..0c53e4b1009 100644
--- a/apps/dav/tests/unit/Connector/Sabre/RequestTest/ExceptionPlugin.php
+++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/ExceptionPlugin.php
@@ -1,35 +1,22 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @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\Tests\unit\Connector\Sabre\RequestTest;
-class ExceptionPlugin extends \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin {
+use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin;
+
+class ExceptionPlugin extends ExceptionLoggerPlugin {
/**
* @var \Throwable[]
*/
protected $exceptions = [];
- public function logException(\Throwable $ex) {
+ public function logException(\Throwable $ex): void {
$exceptionClass = get_class($ex);
if (!isset($this->nonFatalExceptions[$exceptionClass])) {
$this->exceptions[] = $ex;
diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/PartFileInRootUploadTest.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/PartFileInRootUploadTest.php
index 88ec848ad89..e6fa489fb24 100644
--- a/apps/dav/tests/unit/Connector/Sabre/RequestTest/PartFileInRootUploadTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/PartFileInRootUploadTest.php
@@ -1,31 +1,15 @@
<?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 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\Tests\unit\Connector\Sabre\RequestTest;
+use OC\AllConfig;
use OCP\IConfig;
+use OCP\Server;
/**
* Class PartFileInRootUploadTest
@@ -36,10 +20,8 @@ use OCP\IConfig;
*/
class PartFileInRootUploadTest extends UploadTest {
protected function setUp(): void {
- $config = \OC::$server->getConfig();
- $mockConfig = $this->getMockBuilder(IConfig::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $config = Server::get(IConfig::class);
+ $mockConfig = $this->createMock(IConfig::class);
$mockConfig->expects($this->any())
->method('getSystemValue')
->willReturnCallback(function ($key, $default) use ($config) {
@@ -49,7 +31,7 @@ class PartFileInRootUploadTest extends UploadTest {
return $config->getSystemValue($key, $default);
}
});
- $this->overwriteService('AllConfig', $mockConfig);
+ $this->overwriteService(AllConfig::class, $mockConfig);
parent::setUp();
}
diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/RequestTestCase.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/RequestTestCase.php
index 5adfece42e8..404dc7fa5d7 100644
--- a/apps/dav/tests/unit/Connector/Sabre/RequestTest/RequestTestCase.php
+++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/RequestTestCase.php
@@ -1,37 +1,28 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @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>
- *
- * @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\Tests\unit\Connector\Sabre\RequestTest;
use OC\Files\View;
use OCA\DAV\Connector\Sabre\Server;
use OCA\DAV\Connector\Sabre\ServerFactory;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\Mount\IMountManager;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IPreview;
use OCP\IRequest;
+use OCP\IRequestId;
+use OCP\ITagManager;
+use OCP\ITempManager;
+use OCP\IUserSession;
+use OCP\L10N\IFactory;
+use Psr\Log\LoggerInterface;
use Sabre\HTTP\Request;
use Test\TestCase;
use Test\Traits\MountProviderTrait;
@@ -40,11 +31,7 @@ use Test\Traits\UserTrait;
abstract class RequestTestCase extends TestCase {
use UserTrait;
use MountProviderTrait;
-
- /**
- * @var \OCA\DAV\Connector\Sabre\ServerFactory
- */
- protected $serverFactory;
+ protected ServerFactory $serverFactory;
protected function getStream($string) {
$stream = fopen('php://temp', 'r+');
@@ -56,34 +43,30 @@ abstract class RequestTestCase extends TestCase {
protected function setUp(): void {
parent::setUp();
- unset($_SERVER['HTTP_OC_CHUNKED']);
-
$this->serverFactory = new ServerFactory(
- \OC::$server->getConfig(),
- \OC::$server->getLogger(),
- \OC::$server->getDatabaseConnection(),
- \OC::$server->getUserSession(),
- \OC::$server->getMountManager(),
- \OC::$server->getTagManager(),
- $this->getMockBuilder(IRequest::class)
- ->disableOriginalConstructor()
- ->getMock(),
- \OC::$server->getPreviewManager(),
- \OC::$server->getEventDispatcher(),
- \OC::$server->getL10N('dav')
+ \OCP\Server::get(IConfig::class),
+ \OCP\Server::get(LoggerInterface::class),
+ \OCP\Server::get(IDBConnection::class),
+ \OCP\Server::get(IUserSession::class),
+ \OCP\Server::get(IMountManager::class),
+ \OCP\Server::get(ITagManager::class),
+ $this->createMock(IRequest::class),
+ \OCP\Server::get(IPreview::class),
+ \OCP\Server::get(IEventDispatcher::class),
+ \OCP\Server::get(IFactory::class)->get('dav'),
);
}
- protected function setupUser($name, $password) {
+ protected function setupUser($name, $password): View {
$this->createUser($name, $password);
- $tmpFolder = \OC::$server->getTempManager()->getTemporaryFolder();
+ $tmpFolder = \OCP\Server::get(ITempManager::class)->getTemporaryFolder();
$this->registerMount($name, '\OC\Files\Storage\Local', '/' . $name, ['datadir' => $tmpFolder]);
- $this->loginAsUser($name);
+ self::loginAsUser($name);
return new View('/' . $name . '/files');
}
/**
- * @param \OC\Files\View $view the view to run the webdav server against
+ * @param View $view the view to run the webdav server against
* @param string $user
* @param string $password
* @param string $method
@@ -98,26 +81,31 @@ abstract class RequestTestCase extends TestCase {
$body = $this->getStream($body);
}
$this->logout();
- $exceptionPlugin = new ExceptionPlugin('webdav', null);
+ $exceptionPlugin = new ExceptionPlugin('webdav', \OCP\Server::get(LoggerInterface::class));
$server = $this->getSabreServer($view, $user, $password, $exceptionPlugin);
$request = new Request($method, $url, $headers, $body);
// since sabre catches all exceptions we need to save them and throw them from outside the sabre server
- $originalServer = $_SERVER;
-
+ $serverParams = [];
if (is_array($headers)) {
foreach ($headers as $header => $value) {
- $_SERVER['HTTP_' . strtoupper(str_replace('-', '_', $header))] = $value;
+ $serverParams['HTTP_' . strtoupper(str_replace('-', '_', $header))] = $value;
}
}
+ $ncRequest = new \OC\AppFramework\Http\Request([
+ 'server' => $serverParams
+ ], $this->createMock(IRequestId::class), $this->createMock(IConfig::class), null);
+
+ $this->overwriteService(IRequest::class, $ncRequest);
$result = $this->makeRequest($server, $request);
+ $this->restoreService(IRequest::class);
+
foreach ($exceptionPlugin->getExceptions() as $exception) {
throw $exception;
}
- $_SERVER = $originalServer;
return $result;
}
@@ -145,7 +133,7 @@ abstract class RequestTestCase extends TestCase {
$authBackend = new Auth($user, $password);
$authPlugin = new \Sabre\DAV\Auth\Plugin($authBackend);
- $server = $this->serverFactory->createServer('/', 'dummy', $authPlugin, function () use ($view) {
+ $server = $this->serverFactory->createServer(false, '/', 'dummy', $authPlugin, function () use ($view) {
return $view;
});
$server->addPlugin($exceptionPlugin);
diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/Sapi.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/Sapi.php
index 4a2e025f018..08d774e56b8 100644
--- a/apps/dav/tests/unit/Connector/Sabre/RequestTest/Sapi.php
+++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/Sapi.php
@@ -1,25 +1,10 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.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\Tests\unit\Connector\Sabre\RequestTest;
@@ -28,11 +13,6 @@ use Sabre\HTTP\Response;
class Sapi {
/**
- * @var \Sabre\HTTP\Request
- */
- private $request;
-
- /**
* @var \Sabre\HTTP\Response
*/
private $response;
@@ -47,15 +27,16 @@ class Sapi {
return $this->request;
}
- public function __construct(Request $request) {
- $this->request = $request;
+ public function __construct(
+ private Request $request,
+ ) {
}
/**
* @param \Sabre\HTTP\Response $response
* @return void
*/
- public function sendResponse(Response $response) {
+ public function sendResponse(Response $response): void {
// we need to copy the body since we close the source stream
$copyStream = fopen('php://temp', 'r+');
if (is_string($response->getBody())) {
diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/UploadTest.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/UploadTest.php
index 9f7d381ad14..5c6d0f03334 100644
--- a/apps/dav/tests/unit/Connector/Sabre/RequestTest/UploadTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/UploadTest.php
@@ -1,26 +1,10 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @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\Tests\unit\Connector\Sabre\RequestTest;
@@ -35,8 +19,8 @@ use OCP\Lock\ILockingProvider;
* @package OCA\DAV\Tests\unit\Connector\Sabre\RequestTest
*/
class UploadTest extends RequestTestCase {
- public function testBasicUpload() {
- $user = $this->getUniqueID();
+ public function testBasicUpload(): void {
+ $user = self::getUniqueID();
$view = $this->setupUser($user, 'pass');
$this->assertFalse($view->file_exists('foo.txt'));
@@ -51,8 +35,8 @@ class UploadTest extends RequestTestCase {
$this->assertEquals(3, $info->getSize());
}
- public function testUploadOverWrite() {
- $user = $this->getUniqueID();
+ public function testUploadOverWrite(): void {
+ $user = self::getUniqueID();
$view = $this->setupUser($user, 'pass');
$view->file_put_contents('foo.txt', 'foobar');
@@ -67,8 +51,8 @@ class UploadTest extends RequestTestCase {
$this->assertEquals(3, $info->getSize());
}
- public function testUploadOverWriteReadLocked() {
- $user = $this->getUniqueID();
+ public function testUploadOverWriteReadLocked(): void {
+ $user = self::getUniqueID();
$view = $this->setupUser($user, 'pass');
$view->file_put_contents('foo.txt', 'bar');
@@ -79,8 +63,8 @@ class UploadTest extends RequestTestCase {
$this->assertEquals(Http::STATUS_LOCKED, $result->getStatus());
}
- public function testUploadOverWriteWriteLocked() {
- $user = $this->getUniqueID();
+ public function testUploadOverWriteWriteLocked(): void {
+ $user = self::getUniqueID();
$view = $this->setupUser($user, 'pass');
$this->loginAsUser($user);
@@ -91,115 +75,4 @@ class UploadTest extends RequestTestCase {
$result = $this->request($view, $user, 'pass', 'PUT', '/foo.txt', 'asd');
$this->assertEquals(Http::STATUS_LOCKED, $result->getStatus());
}
-
- public function testChunkedUpload() {
- $user = $this->getUniqueID();
- $view = $this->setupUser($user, 'pass');
-
- $this->assertFalse($view->file_exists('foo.txt'));
- $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-0', 'asd', ['OC-Chunked' => '1']);
-
- $this->assertEquals(201, $response->getStatus());
- $this->assertFalse($view->file_exists('foo.txt'));
-
- $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-1', 'bar', ['OC-Chunked' => '1']);
-
- $this->assertEquals(Http::STATUS_CREATED, $response->getStatus());
- $this->assertTrue($view->file_exists('foo.txt'));
-
- $this->assertEquals('asdbar', $view->file_get_contents('foo.txt'));
-
- $info = $view->getFileInfo('foo.txt');
- $this->assertInstanceOf('\OC\Files\FileInfo', $info);
- $this->assertEquals(6, $info->getSize());
- }
-
- public function testChunkedUploadOverWrite() {
- $user = $this->getUniqueID();
- $view = $this->setupUser($user, 'pass');
-
- $view->file_put_contents('foo.txt', 'bar');
- $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-0', 'asd', ['OC-Chunked' => '1']);
-
- $this->assertEquals(Http::STATUS_CREATED, $response->getStatus());
- $this->assertEquals('bar', $view->file_get_contents('foo.txt'));
-
- $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-1', 'bar', ['OC-Chunked' => '1']);
-
- $this->assertEquals(Http::STATUS_CREATED, $response->getStatus());
-
- $this->assertEquals('asdbar', $view->file_get_contents('foo.txt'));
-
- $info = $view->getFileInfo('foo.txt');
- $this->assertInstanceOf('\OC\Files\FileInfo', $info);
- $this->assertEquals(6, $info->getSize());
- }
-
- public function testChunkedUploadOutOfOrder() {
- $user = $this->getUniqueID();
- $view = $this->setupUser($user, 'pass');
-
- $this->assertFalse($view->file_exists('foo.txt'));
- $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-1', 'bar', ['OC-Chunked' => '1']);
-
- $this->assertEquals(Http::STATUS_CREATED, $response->getStatus());
- $this->assertFalse($view->file_exists('foo.txt'));
-
- $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-0', 'asd', ['OC-Chunked' => '1']);
-
- $this->assertEquals(201, $response->getStatus());
- $this->assertTrue($view->file_exists('foo.txt'));
-
- $this->assertEquals('asdbar', $view->file_get_contents('foo.txt'));
-
- $info = $view->getFileInfo('foo.txt');
- $this->assertInstanceOf('\OC\Files\FileInfo', $info);
- $this->assertEquals(6, $info->getSize());
- }
-
- public function testChunkedUploadOutOfOrderReadLocked() {
- $user = $this->getUniqueID();
- $view = $this->setupUser($user, 'pass');
-
- $this->assertFalse($view->file_exists('foo.txt'));
-
- $view->lockFile('/foo.txt', ILockingProvider::LOCK_SHARED);
-
- try {
- $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-1', 'bar', ['OC-Chunked' => '1']);
- } catch (\OCA\DAV\Connector\Sabre\Exception\FileLocked $e) {
- $this->fail('Didn\'t expect locked error for the first chunk on read lock');
- return;
- }
-
- $this->assertEquals(Http::STATUS_CREATED, $response->getStatus());
- $this->assertFalse($view->file_exists('foo.txt'));
-
- // last chunk should trigger the locked error since it tries to assemble
- $result = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-0', 'asd', ['OC-Chunked' => '1']);
- $this->assertEquals(Http::STATUS_LOCKED, $result->getStatus());
- }
-
- public function testChunkedUploadOutOfOrderWriteLocked() {
- $user = $this->getUniqueID();
- $view = $this->setupUser($user, 'pass');
-
- $this->assertFalse($view->file_exists('foo.txt'));
-
- $view->lockFile('/foo.txt', ILockingProvider::LOCK_EXCLUSIVE);
-
- try {
- $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-1', 'bar', ['OC-Chunked' => '1']);
- } catch (\OCA\DAV\Connector\Sabre\Exception\FileLocked $e) {
- $this->fail('Didn\'t expect locked error for the first chunk on write lock'); // maybe forbid this in the future for write locks only?
- return;
- }
-
- $this->assertEquals(Http::STATUS_CREATED, $response->getStatus());
- $this->assertFalse($view->file_exists('foo.txt'));
-
- // last chunk should trigger the locked error since it tries to assemble
- $result = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-0', 'asd', ['OC-Chunked' => '1']);
- $this->assertEquals(Http::STATUS_LOCKED, $result->getStatus());
- }
}
diff --git a/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php
index a81226ccb5e..1c8e29dab38 100644
--- a/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php
@@ -1,72 +1,34 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Maxence Lange <maxence@nextcloud.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @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\Tests\unit\Connector\Sabre;
use OCA\DAV\Connector\Sabre\Directory;
use OCA\DAV\Connector\Sabre\File;
use OCA\DAV\Connector\Sabre\Node;
+use OCA\DAV\Connector\Sabre\SharesPlugin;
use OCA\DAV\Upload\UploadFile;
use OCP\Files\Folder;
use OCP\IUser;
use OCP\IUserSession;
use OCP\Share\IManager;
use OCP\Share\IShare;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\Tree;
class SharesPluginTest extends \Test\TestCase {
- public const SHARETYPES_PROPERTYNAME = \OCA\DAV\Connector\Sabre\SharesPlugin::SHARETYPES_PROPERTYNAME;
-
- /**
- * @var \Sabre\DAV\Server
- */
- private $server;
-
- /**
- * @var \Sabre\DAV\Tree
- */
- private $tree;
+ public const SHARETYPES_PROPERTYNAME = SharesPlugin::SHARETYPES_PROPERTYNAME;
- /**
- * @var \OCP\Share\IManager
- */
- private $shareManager;
-
- /**
- * @var \OCP\Files\Folder
- */
- private $userFolder;
-
- /**
- * @var \OCA\DAV\Connector\Sabre\SharesPlugin
- */
- private $plugin;
+ private \Sabre\DAV\Server $server;
+ private \Sabre\DAV\Tree&MockObject $tree;
+ private \OCP\Share\IManager&MockObject $shareManager;
+ private Folder&MockObject $userFolder;
+ private SharesPlugin $plugin;
protected function setUp(): void {
parent::setUp();
@@ -83,7 +45,7 @@ class SharesPluginTest extends \Test\TestCase {
->willReturn($user);
$this->userFolder = $this->createMock(Folder::class);
- $this->plugin = new \OCA\DAV\Connector\Sabre\SharesPlugin(
+ $this->plugin = new SharesPlugin(
$this->tree,
$userSession,
$this->userFolder,
@@ -92,13 +54,9 @@ class SharesPluginTest extends \Test\TestCase {
$this->plugin->initialize($this->server);
}
- /**
- * @dataProvider sharesGetPropertiesDataProvider
- */
- public function testGetProperties($shareTypes) {
- $sabreNode = $this->getMockBuilder(Node::class)
- ->disableOriginalConstructor()
- ->getMock();
+ #[\PHPUnit\Framework\Attributes\DataProvider('sharesGetPropertiesDataProvider')]
+ public function testGetProperties(array $shareTypes): void {
+ $sabreNode = $this->createMock(Node::class);
$sabreNode->expects($this->any())
->method('getId')
->willReturn(123);
@@ -107,9 +65,7 @@ class SharesPluginTest extends \Test\TestCase {
->willReturn('/subdir');
// node API nodes
- $node = $this->getMockBuilder(Folder::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $node = $this->createMock(Folder::class);
$sabreNode->method('getNode')
->willReturn($node);
@@ -119,7 +75,7 @@ class SharesPluginTest extends \Test\TestCase {
->with(
$this->equalTo('user1'),
$this->anything(),
- $this->anything(),
+ $this->equalTo($node),
$this->equalTo(false),
$this->equalTo(-1)
)
@@ -133,6 +89,16 @@ class SharesPluginTest extends \Test\TestCase {
return [];
});
+ $this->shareManager->expects($this->any())
+ ->method('getSharedWith')
+ ->with(
+ $this->equalTo('user1'),
+ $this->anything(),
+ $this->equalTo($node),
+ $this->equalTo(-1)
+ )
+ ->willReturn([]);
+
$propFind = new \Sabre\DAV\PropFind(
'/dummyPath',
[self::SHARETYPES_PROPERTYNAME],
@@ -151,10 +117,8 @@ class SharesPluginTest extends \Test\TestCase {
$this->assertEquals($shareTypes, $result[200][self::SHARETYPES_PROPERTYNAME]->getShareTypes());
}
- /**
- * @dataProvider sharesGetPropertiesDataProvider
- */
- public function testPreloadThenGetProperties($shareTypes) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('sharesGetPropertiesDataProvider')]
+ public function testPreloadThenGetProperties(array $shareTypes): void {
$sabreNode1 = $this->createMock(File::class);
$sabreNode1->method('getId')
->willReturn(111);
@@ -193,7 +157,7 @@ class SharesPluginTest extends \Test\TestCase {
->willReturn($node2);
$dummyShares = array_map(function ($type) {
- $share = $this->getMockBuilder(IShare::class)->getMock();
+ $share = $this->createMock(IShare::class);
$share->expects($this->any())
->method('getShareType')
->willReturn($type);
@@ -222,6 +186,16 @@ class SharesPluginTest extends \Test\TestCase {
});
$this->shareManager->expects($this->any())
+ ->method('getSharedWith')
+ ->with(
+ $this->equalTo('user1'),
+ $this->anything(),
+ $this->equalTo($node),
+ $this->equalTo(-1)
+ )
+ ->willReturn([]);
+
+ $this->shareManager->expects($this->any())
->method('getSharesInFolder')
->with(
$this->equalTo('user1'),
@@ -269,7 +243,7 @@ class SharesPluginTest extends \Test\TestCase {
$this->assertEquals($shareTypes, $result[200][self::SHARETYPES_PROPERTYNAME]->getShareTypes());
}
- public function sharesGetPropertiesDataProvider() {
+ public static function sharesGetPropertiesDataProvider(): array {
return [
[[]],
[[IShare::TYPE_USER]],
@@ -278,6 +252,7 @@ class SharesPluginTest extends \Test\TestCase {
[[IShare::TYPE_REMOTE]],
[[IShare::TYPE_ROOM]],
[[IShare::TYPE_DECK]],
+ [[IShare::TYPE_SCIENCEMESH]],
[[IShare::TYPE_USER, IShare::TYPE_GROUP]],
[[IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK]],
[[IShare::TYPE_USER, IShare::TYPE_LINK]],
@@ -287,9 +262,7 @@ class SharesPluginTest extends \Test\TestCase {
}
public function testGetPropertiesSkipChunks(): void {
- $sabreNode = $this->getMockBuilder(UploadFile::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $sabreNode = $this->createMock(UploadFile::class);
$propFind = new \Sabre\DAV\PropFind(
'/dummyPath',
diff --git a/apps/dav/tests/unit/Connector/Sabre/TagsPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/TagsPluginTest.php
index 995342db003..5003280bfdc 100644
--- a/apps/dav/tests/unit/Connector/Sabre/TagsPluginTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/TagsPluginTest.php
@@ -1,105 +1,66 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
- * Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @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: 2014-2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Tests\unit\Connector\Sabre;
use OCA\DAV\Connector\Sabre\Directory;
use OCA\DAV\Connector\Sabre\File;
use OCA\DAV\Connector\Sabre\Node;
+use OCA\DAV\Connector\Sabre\TagList;
+use OCA\DAV\Connector\Sabre\TagsPlugin;
use OCA\DAV\Upload\UploadFile;
+use OCP\EventDispatcher\IEventDispatcher;
use OCP\ITagManager;
use OCP\ITags;
+use OCP\IUser;
+use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\Tree;
-/**
- * Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
- * This file is licensed under the Affero General Public License version 3 or
- * later.
- * See the COPYING-README file.
- */
class TagsPluginTest extends \Test\TestCase {
- public const TAGS_PROPERTYNAME = \OCA\DAV\Connector\Sabre\TagsPlugin::TAGS_PROPERTYNAME;
- public const FAVORITE_PROPERTYNAME = \OCA\DAV\Connector\Sabre\TagsPlugin::FAVORITE_PROPERTYNAME;
- public const TAG_FAVORITE = \OCA\DAV\Connector\Sabre\TagsPlugin::TAG_FAVORITE;
-
- /**
- * @var \Sabre\DAV\Server
- */
- private $server;
-
- /**
- * @var Tree
- */
- private $tree;
-
- /**
- * @var \OCP\ITagManager
- */
- private $tagManager;
-
- /**
- * @var \OCP\ITags
- */
- private $tagger;
-
- /**
- * @var \OCA\DAV\Connector\Sabre\TagsPlugin
- */
- private $plugin;
+ public const TAGS_PROPERTYNAME = TagsPlugin::TAGS_PROPERTYNAME;
+ public const FAVORITE_PROPERTYNAME = TagsPlugin::FAVORITE_PROPERTYNAME;
+ public const TAG_FAVORITE = TagsPlugin::TAG_FAVORITE;
+
+ private \Sabre\DAV\Server $server;
+ private Tree&MockObject $tree;
+ private ITagManager&MockObject $tagManager;
+ private ITags&MockObject $tagger;
+ private IEventDispatcher&MockObject $eventDispatcher;
+ private IUserSession&MockObject $userSession;
+ private TagsPlugin $plugin;
protected function setUp(): void {
parent::setUp();
+
$this->server = new \Sabre\DAV\Server();
- $this->tree = $this->getMockBuilder(Tree::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->tagger = $this->getMockBuilder(ITags::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->tagManager = $this->getMockBuilder(ITagManager::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $this->tree = $this->createMock(Tree::class);
+ $this->tagger = $this->createMock(ITags::class);
+ $this->tagManager = $this->createMock(ITagManager::class);
+ $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
+ $user = $this->createMock(IUser::class);
+
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->userSession->expects($this->any())
+ ->method('getUser')
+ ->withAnyParameters()
+ ->willReturn($user);
$this->tagManager->expects($this->any())
->method('load')
->with('files')
->willReturn($this->tagger);
- $this->plugin = new \OCA\DAV\Connector\Sabre\TagsPlugin($this->tree, $this->tagManager);
+ $this->plugin = new TagsPlugin($this->tree, $this->tagManager, $this->eventDispatcher, $this->userSession);
$this->plugin->initialize($this->server);
}
- /**
- * @dataProvider tagsGetPropertiesDataProvider
- */
- public function testGetProperties($tags, $requestedProperties, $expectedProperties) {
- $node = $this->getMockBuilder(Node::class)
- ->disableOriginalConstructor()
- ->getMock();
+ #[\PHPUnit\Framework\Attributes\DataProvider('tagsGetPropertiesDataProvider')]
+ public function testGetProperties(array $tags, array $requestedProperties, array $expectedProperties): void {
+ $node = $this->createMock(Node::class);
$node->expects($this->any())
->method('getId')
->willReturn(123);
@@ -132,19 +93,13 @@ class TagsPluginTest extends \Test\TestCase {
$this->assertEquals($expectedProperties, $result);
}
- /**
- * @dataProvider tagsGetPropertiesDataProvider
- */
- public function testPreloadThenGetProperties($tags, $requestedProperties, $expectedProperties) {
- $node1 = $this->getMockBuilder(File::class)
- ->disableOriginalConstructor()
- ->getMock();
+ #[\PHPUnit\Framework\Attributes\DataProvider('tagsGetPropertiesDataProvider')]
+ public function testPreloadThenGetProperties(array $tags, array $requestedProperties, array $expectedProperties): void {
+ $node1 = $this->createMock(File::class);
$node1->expects($this->any())
->method('getId')
->willReturn(111);
- $node2 = $this->getMockBuilder(File::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $node2 = $this->createMock(File::class);
$node2->expects($this->any())
->method('getId')
->willReturn(222);
@@ -157,9 +112,7 @@ class TagsPluginTest extends \Test\TestCase {
$expectedCallCount = 1;
}
- $node = $this->getMockBuilder(Directory::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $node = $this->createMock(Directory::class);
$node->expects($this->any())
->method('getId')
->willReturn(123);
@@ -214,7 +167,7 @@ class TagsPluginTest extends \Test\TestCase {
$this->assertEquals($expectedProperties, $result);
}
- public function tagsGetPropertiesDataProvider() {
+ public static function tagsGetPropertiesDataProvider(): array {
return [
// request both, receive both
[
@@ -222,7 +175,7 @@ class TagsPluginTest extends \Test\TestCase {
[self::TAGS_PROPERTYNAME, self::FAVORITE_PROPERTYNAME],
[
200 => [
- self::TAGS_PROPERTYNAME => new \OCA\DAV\Connector\Sabre\TagList(['tag1', 'tag2']),
+ self::TAGS_PROPERTYNAME => new TagList(['tag1', 'tag2']),
self::FAVORITE_PROPERTYNAME => true,
]
]
@@ -233,7 +186,7 @@ class TagsPluginTest extends \Test\TestCase {
[self::TAGS_PROPERTYNAME],
[
200 => [
- self::TAGS_PROPERTYNAME => new \OCA\DAV\Connector\Sabre\TagList(['tag1', 'tag2']),
+ self::TAGS_PROPERTYNAME => new TagList(['tag1', 'tag2']),
]
]
],
@@ -261,7 +214,7 @@ class TagsPluginTest extends \Test\TestCase {
[self::TAGS_PROPERTYNAME, self::FAVORITE_PROPERTYNAME],
[
200 => [
- self::TAGS_PROPERTYNAME => new \OCA\DAV\Connector\Sabre\TagList([]),
+ self::TAGS_PROPERTYNAME => new TagList([]),
self::FAVORITE_PROPERTYNAME => false,
]
]
@@ -270,9 +223,7 @@ class TagsPluginTest extends \Test\TestCase {
}
public function testGetPropertiesSkipChunks(): void {
- $sabreNode = $this->getMockBuilder(UploadFile::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $sabreNode = $this->createMock(UploadFile::class);
$propFind = new \Sabre\DAV\PropFind(
'/dummyPath',
@@ -289,12 +240,10 @@ class TagsPluginTest extends \Test\TestCase {
$this->assertCount(2, $result[404]);
}
- public function testUpdateTags() {
+ public function testUpdateTags(): void {
// this test will replace the existing tags "tagremove" with "tag1" and "tag2"
// and keep "tagkeep"
- $node = $this->getMockBuilder(Node::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $node = $this->createMock(Node::class);
$node->expects($this->any())
->method('getId')
->willReturn(123);
@@ -304,27 +253,31 @@ class TagsPluginTest extends \Test\TestCase {
->with('/dummypath')
->willReturn($node);
- $this->tagger->expects($this->at(0))
+ $this->tagger->expects($this->once())
->method('getTagsForObjects')
->with($this->equalTo([123]))
->willReturn([123 => ['tagkeep', 'tagremove', self::TAG_FAVORITE]]);
// then tag as tag1 and tag2
- $this->tagger->expects($this->at(1))
- ->method('tagAs')
- ->with(123, 'tag1');
- $this->tagger->expects($this->at(2))
+ $calls = [
+ [123, 'tag1'],
+ [123, 'tag2'],
+ ];
+ $this->tagger->expects($this->exactly(count($calls)))
->method('tagAs')
- ->with(123, 'tag2');
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
// it will untag tag3
- $this->tagger->expects($this->at(3))
+ $this->tagger->expects($this->once())
->method('unTag')
->with(123, 'tagremove');
// properties to set
$propPatch = new \Sabre\DAV\PropPatch([
- self::TAGS_PROPERTYNAME => new \OCA\DAV\Connector\Sabre\TagList(['tag1', 'tag2', 'tagkeep'])
+ self::TAGS_PROPERTYNAME => new TagList(['tag1', 'tag2', 'tagkeep'])
]);
$this->plugin->handleUpdateProperties(
@@ -339,13 +292,11 @@ class TagsPluginTest extends \Test\TestCase {
$result = $propPatch->getResult();
$this->assertEquals(200, $result[self::TAGS_PROPERTYNAME]);
- $this->assertFalse(isset($result[self::FAVORITE_PROPERTYNAME]));
+ $this->assertArrayNotHasKey(self::FAVORITE_PROPERTYNAME, $result);
}
- public function testUpdateTagsFromScratch() {
- $node = $this->getMockBuilder(Node::class)
- ->disableOriginalConstructor()
- ->getMock();
+ public function testUpdateTagsFromScratch(): void {
+ $node = $this->createMock(Node::class);
$node->expects($this->any())
->method('getId')
->willReturn(123);
@@ -355,22 +306,26 @@ class TagsPluginTest extends \Test\TestCase {
->with('/dummypath')
->willReturn($node);
- $this->tagger->expects($this->at(0))
+ $this->tagger->expects($this->once())
->method('getTagsForObjects')
->with($this->equalTo([123]))
->willReturn([]);
// then tag as tag1 and tag2
- $this->tagger->expects($this->at(1))
- ->method('tagAs')
- ->with(123, 'tag1');
- $this->tagger->expects($this->at(2))
+ $calls = [
+ [123, 'tag1'],
+ [123, 'tag2'],
+ ];
+ $this->tagger->expects($this->exactly(count($calls)))
->method('tagAs')
- ->with(123, 'tag2');
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
// properties to set
$propPatch = new \Sabre\DAV\PropPatch([
- self::TAGS_PROPERTYNAME => new \OCA\DAV\Connector\Sabre\TagList(['tag1', 'tag2', 'tagkeep'])
+ self::TAGS_PROPERTYNAME => new TagList(['tag1', 'tag2'])
]);
$this->plugin->handleUpdateProperties(
@@ -385,15 +340,13 @@ class TagsPluginTest extends \Test\TestCase {
$result = $propPatch->getResult();
$this->assertEquals(200, $result[self::TAGS_PROPERTYNAME]);
- $this->assertFalse(false, isset($result[self::FAVORITE_PROPERTYNAME]));
+ $this->assertArrayNotHasKey(self::FAVORITE_PROPERTYNAME, $result);
}
- public function testUpdateFav() {
+ public function testUpdateFav(): void {
// this test will replace the existing tags "tagremove" with "tag1" and "tag2"
// and keep "tagkeep"
- $node = $this->getMockBuilder(Node::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $node = $this->createMock(Node::class);
$node->expects($this->any())
->method('getId')
->willReturn(123);
@@ -424,8 +377,8 @@ class TagsPluginTest extends \Test\TestCase {
$this->assertEmpty($propPatch->getRemainingMutations());
$result = $propPatch->getResult();
- $this->assertFalse(false, isset($result[self::TAGS_PROPERTYNAME]));
- $this->assertEquals(200, isset($result[self::FAVORITE_PROPERTYNAME]));
+ $this->assertArrayNotHasKey(self::TAGS_PROPERTYNAME, $result);
+ $this->assertEquals(200, $result[self::FAVORITE_PROPERTYNAME]);
// unfavorite now
// set favorite tag
@@ -449,7 +402,7 @@ class TagsPluginTest extends \Test\TestCase {
$this->assertEmpty($propPatch->getRemainingMutations());
$result = $propPatch->getResult();
- $this->assertFalse(false, isset($result[self::TAGS_PROPERTYNAME]));
- $this->assertEquals(200, isset($result[self::FAVORITE_PROPERTYNAME]));
+ $this->assertArrayNotHasKey(self::TAGS_PROPERTYNAME, $result);
+ $this->assertEquals(200, $result[self::FAVORITE_PROPERTYNAME]);
}
}
diff --git a/apps/dav/tests/unit/Controller/BirthdayCalendarControllerTest.php b/apps/dav/tests/unit/Controller/BirthdayCalendarControllerTest.php
index 37650e6f1ed..9aa0ef3a2a7 100644
--- a/apps/dav/tests/unit/Controller/BirthdayCalendarControllerTest.php
+++ b/apps/dav/tests/unit/Controller/BirthdayCalendarControllerTest.php
@@ -1,65 +1,33 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright 2017, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author François Freitag <mail@franek.fr>
- * @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\Tests\Unit\DAV\Controller;
+namespace OCA\DAV\Tests\unit\DAV\Controller;
use OCA\DAV\BackgroundJob\GenerateBirthdayCalendarBackgroundJob;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\Controller\BirthdayCalendarController;
+use OCP\AppFramework\Http\JSONResponse;
use OCP\BackgroundJob\IJobList;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IRequest;
use OCP\IUser;
use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class BirthdayCalendarControllerTest extends TestCase {
-
- /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
- private $config;
-
- /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
- private $request;
-
- /** @var IDBConnection|\PHPUnit\Framework\MockObject\MockObject */
- private $db;
-
- /** @var IJobList|\PHPUnit\Framework\MockObject\MockObject */
- private $jobList;
-
- /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */
- private $userManager;
-
- /** @var CalDavBackend|\PHPUnit\Framework\MockObject\MockObject */
- private $caldav;
-
- /** @var BirthdayCalendarController|\PHPUnit\Framework\MockObject\MockObject */
- private $controller;
+ private IConfig&MockObject $config;
+ private IRequest&MockObject $request;
+ private IDBConnection&MockObject $db;
+ private IJobList&MockObject $jobList;
+ private IUserManager&MockObject $userManager;
+ private CalDavBackend&MockObject $caldav;
+ private BirthdayCalendarController $controller;
protected function setUp(): void {
parent::setUp();
@@ -76,14 +44,14 @@ class BirthdayCalendarControllerTest extends TestCase {
$this->userManager, $this->caldav);
}
- public function testEnable() {
+ public function testEnable(): void {
$this->config->expects($this->once())
->method('setAppValue')
->with('dav', 'generateBirthdayCalendar', 'yes');
$this->userManager->expects($this->once())
->method('callForSeenUsers')
- ->willReturnCallback(function ($closure) {
+ ->willReturnCallback(function ($closure): void {
$user1 = $this->createMock(IUser::class);
$user1->method('getUID')->willReturn('uid1');
$user2 = $this->createMock(IUser::class);
@@ -96,19 +64,23 @@ class BirthdayCalendarControllerTest extends TestCase {
$closure($user3);
});
+ $calls = [
+ [GenerateBirthdayCalendarBackgroundJob::class, ['userId' => 'uid1']],
+ [GenerateBirthdayCalendarBackgroundJob::class, ['userId' => 'uid2']],
+ [GenerateBirthdayCalendarBackgroundJob::class, ['userId' => 'uid3']],
+ ];
$this->jobList->expects($this->exactly(3))
->method('add')
- ->withConsecutive(
- [GenerateBirthdayCalendarBackgroundJob::class, ['userId' => 'uid1']],
- [GenerateBirthdayCalendarBackgroundJob::class, ['userId' => 'uid2']],
- [GenerateBirthdayCalendarBackgroundJob::class, ['userId' => 'uid3']],
- );
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
$response = $this->controller->enable();
- $this->assertInstanceOf('OCP\AppFramework\Http\JSONResponse', $response);
+ $this->assertInstanceOf(JSONResponse::class, $response);
}
- public function testDisable() {
+ public function testDisable(): void {
$this->config->expects($this->once())
->method('setAppValue')
->with('dav', 'generateBirthdayCalendar', 'no');
@@ -119,6 +91,6 @@ class BirthdayCalendarControllerTest extends TestCase {
->method('deleteAllBirthdayCalendars');
$response = $this->controller->disable();
- $this->assertInstanceOf('OCP\AppFramework\Http\JSONResponse', $response);
+ $this->assertInstanceOf(JSONResponse::class, $response);
}
}
diff --git a/apps/dav/tests/unit/Controller/DirectControllerTest.php b/apps/dav/tests/unit/Controller/DirectControllerTest.php
index 00771e7f7a6..837adde1da7 100644
--- a/apps/dav/tests/unit/Controller/DirectControllerTest.php
+++ b/apps/dav/tests/unit/Controller/DirectControllerTest.php
@@ -3,29 +3,10 @@
declare(strict_types=1);
/**
- * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @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: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-namespace OCA\DAV\Tests\Unit\DAV\Controller;
+namespace OCA\DAV\Tests\unit\DAV\Controller;
use OCA\DAV\Controller\DirectController;
use OCA\DAV\Db\Direct;
@@ -34,33 +15,25 @@ use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSBadRequestException;
use OCP\AppFramework\OCS\OCSNotFoundException;
use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\Security\ISecureRandom;
+use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class DirectControllerTest extends TestCase {
+ private IRootFolder&MockObject $rootFolder;
+ private DirectMapper&MockObject $directMapper;
+ private ISecureRandom&MockObject $random;
+ private ITimeFactory&MockObject $timeFactory;
+ private IURLGenerator&MockObject $urlGenerator;
+ private IEventDispatcher&MockObject $eventDispatcher;
- /** @var IRootFolder|\PHPUnit\Framework\MockObject\MockObject */
- private $rootFolder;
-
- /** @var DirectMapper|\PHPUnit\Framework\MockObject\MockObject */
- private $directMapper;
-
- /** @var ISecureRandom|\PHPUnit\Framework\MockObject\MockObject */
- private $random;
-
- /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */
- private $timeFactory;
-
- /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
- private $urlGenerator;
-
- /** @var DirectController */
- private $controller;
+ private DirectController $controller;
protected function setUp(): void {
parent::setUp();
@@ -70,6 +43,7 @@ class DirectControllerTest extends TestCase {
$this->random = $this->createMock(ISecureRandom::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->controller = new DirectController(
'dav',
@@ -79,11 +53,12 @@ class DirectControllerTest extends TestCase {
$this->directMapper,
$this->random,
$this->timeFactory,
- $this->urlGenerator
+ $this->urlGenerator,
+ $this->eventDispatcher
);
}
- public function testGetUrlNonExistingFileId() {
+ public function testGetUrlNonExistingFileId(): void {
$userFolder = $this->createMock(Folder::class);
$this->rootFolder->method('getUserFolder')
->with('awesomeUser')
@@ -97,7 +72,7 @@ class DirectControllerTest extends TestCase {
$this->controller->getUrl(101);
}
- public function testGetUrlForFolder() {
+ public function testGetUrlForFolder(): void {
$userFolder = $this->createMock(Folder::class);
$this->rootFolder->method('getUserFolder')
->with('awesomeUser')
@@ -105,15 +80,15 @@ class DirectControllerTest extends TestCase {
$folder = $this->createMock(Folder::class);
- $userFolder->method('getById')
+ $userFolder->method('getFirstNodeById')
->with(101)
- ->willReturn([$folder]);
+ ->willReturn($folder);
$this->expectException(OCSBadRequestException::class);
$this->controller->getUrl(101);
}
- public function testGetUrlValid() {
+ public function testGetUrlValid(): void {
$userFolder = $this->createMock(Folder::class);
$this->rootFolder->method('getUserFolder')
->with('awesomeUser')
@@ -124,9 +99,12 @@ class DirectControllerTest extends TestCase {
$this->timeFactory->method('getTime')
->willReturn(42);
- $userFolder->method('getById')
+ $userFolder->method('getFirstNodeById')
->with(101)
- ->willReturn([$file]);
+ ->willReturn($file);
+
+ $userFolder->method('getRelativePath')
+ ->willReturn('/path');
$this->random->method('generate')
->with(
@@ -147,7 +125,7 @@ class DirectControllerTest extends TestCase {
$this->urlGenerator->method('getAbsoluteURL')
->willReturnCallback(function (string $url) {
- return 'https://my.nextcloud/'.$url;
+ return 'https://my.nextcloud/' . $url;
});
$result = $this->controller->getUrl(101);
diff --git a/apps/dav/tests/unit/Controller/InvitationResponseControllerTest.php b/apps/dav/tests/unit/Controller/InvitationResponseControllerTest.php
index cd3269d657c..15b18d6c1b1 100644
--- a/apps/dav/tests/unit/Controller/InvitationResponseControllerTest.php
+++ b/apps/dav/tests/unit/Controller/InvitationResponseControllerTest.php
@@ -3,32 +3,11 @@
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 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: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-namespace OCA\DAV\Tests\Unit\DAV\Controller;
+namespace OCA\DAV\Tests\unit\DAV\Controller;
use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
use OCA\DAV\Controller\InvitationResponseController;
@@ -39,25 +18,16 @@ use OCP\DB\QueryBuilder\IExpressionBuilder;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IRequest;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\VObject\ITip\Message;
use Test\TestCase;
class InvitationResponseControllerTest extends TestCase {
-
- /** @var InvitationResponseController */
- private $controller;
-
- /** @var IDBConnection|\PHPUnit\Framework\MockObject\MockObject */
- private $dbConnection;
-
- /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
- private $request;
-
- /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */
- private $timeFactory;
-
- /** @var InvitationResponseServer|\PHPUnit\Framework\MockObject\MockObject */
- private $responseServer;
+ private IDBConnection&MockObject $dbConnection;
+ private IRequest&MockObject $request;
+ private ITimeFactory&MockObject $timeFactory;
+ private InvitationResponseServer&MockObject $responseServer;
+ private InvitationResponseController $controller;
protected function setUp(): void {
parent::setUp();
@@ -65,9 +35,7 @@ class InvitationResponseControllerTest extends TestCase {
$this->dbConnection = $this->createMock(IDBConnection::class);
$this->request = $this->createMock(IRequest::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
- $this->responseServer = $this->getMockBuilder(InvitationResponseServer::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $this->responseServer = $this->createMock(InvitationResponseServer::class);
$this->controller = new InvitationResponseController(
'appName',
@@ -78,16 +46,14 @@ class InvitationResponseControllerTest extends TestCase {
);
}
- public function attendeeProvider(): array {
+ public static function attendeeProvider(): array {
return [
'local attendee' => [false],
'external attendee' => [true]
];
}
- /**
- * @dataProvider attendeeProvider
- */
+ #[\PHPUnit\Framework\Attributes\DataProvider('attendeeProvider')]
public function testAccept(bool $isExternalAttendee): void {
$this->buildQueryExpects('TOKEN123', [
'id' => 0,
@@ -121,7 +87,7 @@ EOF;
$called = false;
$this->responseServer->expects($this->once())
->method('handleITipMessage')
- ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected) {
+ ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected): void {
$called = true;
$this->assertEquals('this-is-the-events-uid', $iTipMessage->uid);
$this->assertEquals('VEVENT', $iTipMessage->component);
@@ -149,9 +115,7 @@ EOF;
$this->assertTrue($called);
}
- /**
- * @dataProvider attendeeProvider
- */
+ #[\PHPUnit\Framework\Attributes\DataProvider('attendeeProvider')]
public function testAcceptSequence(bool $isExternalAttendee): void {
$this->buildQueryExpects('TOKEN123', [
'id' => 0,
@@ -185,7 +149,7 @@ EOF;
$called = false;
$this->responseServer->expects($this->once())
->method('handleITipMessage')
- ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected) {
+ ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected): void {
$called = true;
$this->assertEquals('this-is-the-events-uid', $iTipMessage->uid);
$this->assertEquals('VEVENT', $iTipMessage->component);
@@ -213,9 +177,7 @@ EOF;
$this->assertTrue($called);
}
- /**
- * @dataProvider attendeeProvider
- */
+ #[\PHPUnit\Framework\Attributes\DataProvider('attendeeProvider')]
public function testAcceptRecurrenceId(bool $isExternalAttendee): void {
$this->buildQueryExpects('TOKEN123', [
'id' => 0,
@@ -250,7 +212,7 @@ EOF;
$called = false;
$this->responseServer->expects($this->once())
->method('handleITipMessage')
- ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected) {
+ ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected): void {
$called = true;
$this->assertEquals('this-is-the-events-uid', $iTipMessage->uid);
$this->assertEquals('VEVENT', $iTipMessage->component);
@@ -278,7 +240,7 @@ EOF;
$this->assertTrue($called);
}
- public function testAcceptTokenNotFound() {
+ public function testAcceptTokenNotFound(): void {
$this->buildQueryExpects('TOKEN123', null, 1337);
$response = $this->controller->accept('TOKEN123');
@@ -287,7 +249,7 @@ EOF;
$this->assertEquals([], $response->getParams());
}
- public function testAcceptExpiredToken() {
+ public function testAcceptExpiredToken(): void {
$this->buildQueryExpects('TOKEN123', [
'id' => 0,
'uid' => 'this-is-the-events-uid',
@@ -305,9 +267,7 @@ EOF;
$this->assertEquals([], $response->getParams());
}
- /**
- * @dataProvider attendeeProvider
- */
+ #[\PHPUnit\Framework\Attributes\DataProvider('attendeeProvider')]
public function testDecline(bool $isExternalAttendee): void {
$this->buildQueryExpects('TOKEN123', [
'id' => 0,
@@ -341,7 +301,7 @@ EOF;
$called = false;
$this->responseServer->expects($this->once())
->method('handleITipMessage')
- ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected) {
+ ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected): void {
$called = true;
$this->assertEquals('this-is-the-events-uid', $iTipMessage->uid);
$this->assertEquals('VEVENT', $iTipMessage->component);
@@ -369,29 +329,19 @@ EOF;
$this->assertTrue($called);
}
- public function testOptions() {
+ public function testOptions(): void {
$response = $this->controller->options('TOKEN123');
$this->assertInstanceOf(TemplateResponse::class, $response);
$this->assertEquals('schedule-response-options', $response->getTemplateName());
$this->assertEquals(['token' => 'TOKEN123'], $response->getParams());
}
- /**
- * @dataProvider attendeeProvider
- */
+ #[\PHPUnit\Framework\Attributes\DataProvider('attendeeProvider')]
public function testProcessMoreOptionsResult(bool $isExternalAttendee): void {
- $this->request->expects($this->at(0))
+ $this->request->expects($this->once())
->method('getParam')
->with('partStat')
->willReturn('TENTATIVE');
- $this->request->expects($this->at(1))
- ->method('getParam')
- ->with('guests')
- ->willReturn('7');
- $this->request->expects($this->at(2))
- ->method('getParam')
- ->with('comment')
- ->willReturn('Foo bar Bli blub');
$this->buildQueryExpects('TOKEN123', [
'id' => 0,
@@ -410,14 +360,12 @@ VERSION:2.0
PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
METHOD:REPLY
BEGIN:VEVENT
-ATTENDEE;PARTSTAT=TENTATIVE;X-RESPONSE-COMMENT=Foo bar Bli blub;X-NUM-GUEST
- S=7:mailto:attendee@foo.bar
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:attendee@foo.bar
ORGANIZER:mailto:organizer@foo.bar
UID:this-is-the-events-uid
SEQUENCE:0
REQUEST-STATUS:2.0;Success
DTSTAMP:19700101T002217Z
-COMMENT:Foo bar Bli blub
END:VEVENT
END:VCALENDAR
@@ -427,7 +375,7 @@ EOF;
$called = false;
$this->responseServer->expects($this->once())
->method('handleITipMessage')
- ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected) {
+ ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected): void {
$called = true;
$this->assertEquals('this-is-the-events-uid', $iTipMessage->uid);
$this->assertEquals('VEVENT', $iTipMessage->component);
@@ -456,7 +404,7 @@ EOF;
$this->assertTrue($called);
}
- private function buildQueryExpects($token, $return, $time) {
+ private function buildQueryExpects(string $token, ?array $return, int $time): void {
$queryBuilder = $this->createMock(IQueryBuilder::class);
$stmt = $this->createMock(IResult::class);
$expr = $this->createMock(IExpressionBuilder::class);
@@ -476,31 +424,34 @@ EOF;
->method('fetch')
->with(\PDO::FETCH_ASSOC)
->willReturn($return);
+ $stmt->expects($this->once())
+ ->method('closeCursor');
+ $function = 'functionToken';
$expr->expects($this->once())
->method('eq')
->with('token', 'namedParameterToken')
- ->willReturn('EQ STATEMENT');
+ ->willReturn((string)$function);
$this->dbConnection->expects($this->once())
->method('getQueryBuilder')
->with()
->willReturn($queryBuilder);
- $queryBuilder->expects($this->at(0))
+ $queryBuilder->expects($this->once())
->method('select')
->with('*')
->willReturn($queryBuilder);
- $queryBuilder->expects($this->at(1))
+ $queryBuilder->expects($this->once())
->method('from')
->with('calendar_invitations')
->willReturn($queryBuilder);
- $queryBuilder->expects($this->at(4))
+ $queryBuilder->expects($this->once())
->method('where')
- ->with('EQ STATEMENT')
+ ->with($function)
->willReturn($queryBuilder);
- $queryBuilder->expects($this->at(5))
- ->method('execute')
+ $queryBuilder->expects($this->once())
+ ->method('executeQuery')
->with()
->willReturn($stmt);
diff --git a/apps/dav/tests/unit/Controller/UpcomingEventsControllerTest.php b/apps/dav/tests/unit/Controller/UpcomingEventsControllerTest.php
new file mode 100644
index 00000000000..527943e5221
--- /dev/null
+++ b/apps/dav/tests/unit/Controller/UpcomingEventsControllerTest.php
@@ -0,0 +1,73 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\DAV\Service;
+
+use OCA\DAV\CalDAV\UpcomingEvent;
+use OCA\DAV\CalDAV\UpcomingEventsService;
+use OCA\DAV\Controller\UpcomingEventsController;
+use OCP\IRequest;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+class UpcomingEventsControllerTest extends TestCase {
+ private IRequest&MockObject $request;
+ private UpcomingEventsService&MockObject $service;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->request = $this->createMock(IRequest::class);
+ $this->service = $this->createMock(UpcomingEventsService::class);
+ }
+
+ public function testGetEventsAnonymously(): void {
+ $controller = new UpcomingEventsController(
+ $this->request,
+ null,
+ $this->service,
+ );
+
+ $response = $controller->getEvents('https://cloud.example.com/call/123');
+
+ self::assertNull($response->getData());
+ self::assertSame(401, $response->getStatus());
+ }
+
+ public function testGetEventsByLocation(): void {
+ $controller = new UpcomingEventsController(
+ $this->request,
+ 'u1',
+ $this->service,
+ );
+ $this->service->expects(self::once())
+ ->method('getEvents')
+ ->with('u1', 'https://cloud.example.com/call/123')
+ ->willReturn([
+ new UpcomingEvent(
+ 'abc-123',
+ null,
+ 'personal',
+ 123,
+ 'Test',
+ 'https://cloud.example.com/call/123',
+ null,
+ ),
+ ]);
+
+ $response = $controller->getEvents('https://cloud.example.com/call/123');
+
+ self::assertNotNull($response->getData());
+ self::assertIsArray($response->getData());
+ self::assertCount(1, $response->getData()['events']);
+ self::assertSame(200, $response->getStatus());
+ $event1 = $response->getData()['events'][0];
+ self::assertEquals('abc-123', $event1['uri']);
+ }
+}
diff --git a/apps/dav/tests/unit/DAV/AnonymousOptionsTest.php b/apps/dav/tests/unit/DAV/AnonymousOptionsTest.php
index 8c178ba8e44..c99ebf327c8 100644
--- a/apps/dav/tests/unit/DAV/AnonymousOptionsTest.php
+++ b/apps/dav/tests/unit/DAV/AnonymousOptionsTest.php
@@ -1,29 +1,11 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl>
- *
- * @author Bastien Durel <bastien@durel.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @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\tests\unit\DAV;
+namespace OCA\DAV\Tests\unit\DAV;
use OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin;
use Sabre\DAV\Auth\Backend\BasicCallBack;
@@ -34,7 +16,7 @@ use Sabre\HTTP\Sapi;
use Test\TestCase;
class AnonymousOptionsTest extends TestCase {
- private function sendRequest($method, $path, $userAgent = '') {
+ private function sendRequest(string $method, string $path, string $userAgent = '') {
$server = new Server();
$server->addPlugin(new AnonymousOptionsPlugin());
$server->addPlugin(new Plugin(new BasicCallBack(function () {
@@ -50,49 +32,49 @@ class AnonymousOptionsTest extends TestCase {
return $server->httpResponse;
}
- public function testAnonymousOptionsRoot() {
+ public function testAnonymousOptionsRoot(): void {
$response = $this->sendRequest('OPTIONS', '');
$this->assertEquals(401, $response->getStatus());
}
- public function testAnonymousOptionsNonRoot() {
+ public function testAnonymousOptionsNonRoot(): void {
$response = $this->sendRequest('OPTIONS', 'foo');
$this->assertEquals(401, $response->getStatus());
}
- public function testAnonymousOptionsNonRootSubDir() {
+ public function testAnonymousOptionsNonRootSubDir(): void {
$response = $this->sendRequest('OPTIONS', 'foo/bar');
$this->assertEquals(401, $response->getStatus());
}
- public function testAnonymousOptionsRootOffice() {
+ public function testAnonymousOptionsRootOffice(): void {
$response = $this->sendRequest('OPTIONS', '', 'Microsoft Office does strange things');
$this->assertEquals(200, $response->getStatus());
}
- public function testAnonymousOptionsNonRootOffice() {
+ public function testAnonymousOptionsNonRootOffice(): void {
$response = $this->sendRequest('OPTIONS', 'foo', 'Microsoft Office does strange things');
$this->assertEquals(200, $response->getStatus());
}
- public function testAnonymousOptionsNonRootSubDirOffice() {
+ public function testAnonymousOptionsNonRootSubDirOffice(): void {
$response = $this->sendRequest('OPTIONS', 'foo/bar', 'Microsoft Office does strange things');
$this->assertEquals(200, $response->getStatus());
}
- public function testAnonymousHead() {
+ public function testAnonymousHead(): void {
$response = $this->sendRequest('HEAD', '', 'Microsoft Office does strange things');
$this->assertEquals(200, $response->getStatus());
}
- public function testAnonymousHeadNoOffice() {
+ public function testAnonymousHeadNoOffice(): void {
$response = $this->sendRequest('HEAD', '');
$this->assertEquals(401, $response->getStatus(), 'curl');
@@ -105,6 +87,6 @@ class SapiMock extends Sapi {
*
* @return void
*/
- public static function sendResponse(ResponseInterface $response) {
+ public static function sendResponse(ResponseInterface $response): void {
}
}
diff --git a/apps/dav/tests/unit/DAV/BrowserErrorPagePluginTest.php b/apps/dav/tests/unit/DAV/BrowserErrorPagePluginTest.php
index a0733a68550..0e82ef0a3ae 100644
--- a/apps/dav/tests/unit/DAV/BrowserErrorPagePluginTest.php
+++ b/apps/dav/tests/unit/DAV/BrowserErrorPagePluginTest.php
@@ -1,48 +1,30 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Joas Schilling <coding@schilljs.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\Tests\unit\DAV;
use OCA\DAV\Files\BrowserErrorPagePlugin;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\Exception\NotFound;
use Sabre\HTTP\Response;
class BrowserErrorPagePluginTest extends \Test\TestCase {
- /**
- * @dataProvider providesExceptions
- * @param $expectedCode
- * @param $exception
- */
- public function test($expectedCode, $exception) {
- /** @var BrowserErrorPagePlugin | \PHPUnit\Framework\MockObject\MockObject $plugin */
- $plugin = $this->getMockBuilder(BrowserErrorPagePlugin::class)->setMethods(['sendResponse', 'generateBody'])->getMock();
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesExceptions')]
+ public function test(int $expectedCode, \Throwable $exception): void {
+ /** @var BrowserErrorPagePlugin&MockObject $plugin */
+ $plugin = $this->getMockBuilder(BrowserErrorPagePlugin::class)->onlyMethods(['sendResponse', 'generateBody'])->getMock();
$plugin->expects($this->once())->method('generateBody')->willReturn(':boom:');
$plugin->expects($this->once())->method('sendResponse');
- /** @var \Sabre\DAV\Server | \PHPUnit\Framework\MockObject\MockObject $server */
- $server = $this->getMockBuilder('Sabre\DAV\Server')->disableOriginalConstructor()->getMock();
+ /** @var \Sabre\DAV\Server&MockObject $server */
+ $server = $this->createMock('Sabre\DAV\Server');
$server->expects($this->once())->method('on');
- $httpResponse = $this->getMockBuilder(Response::class)->disableOriginalConstructor()->getMock();
+ $httpResponse = $this->createMock(Response::class);
$httpResponse->expects($this->once())->method('addHeaders');
$httpResponse->expects($this->once())->method('setStatus')->with($expectedCode);
$httpResponse->expects($this->once())->method('setBody')->with(':boom:');
@@ -51,7 +33,7 @@ class BrowserErrorPagePluginTest extends \Test\TestCase {
$plugin->logException($exception);
}
- public function providesExceptions() {
+ public static function providesExceptions(): array {
return [
[ 404, new NotFound()],
[ 500, new \RuntimeException()],
diff --git a/apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php b/apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php
index 2e7939aa614..2a85c0cbecd 100644
--- a/apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php
+++ b/apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php
@@ -1,71 +1,62 @@
<?php
+
+declare(strict_types=1);
/**
- * @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 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\Tests\DAV;
+namespace OCA\DAV\Tests\unit\DAV;
+use OCA\DAV\CalDAV\Calendar;
+use OCA\DAV\CalDAV\DefaultCalendarValidator;
use OCA\DAV\DAV\CustomPropertiesBackend;
+use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IUser;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\PropFind;
use Sabre\DAV\PropPatch;
+use Sabre\DAV\Server;
use Sabre\DAV\Tree;
+use Sabre\DAV\Xml\Property\Href;
+use Sabre\DAVACL\IACL;
+use Sabre\DAVACL\IPrincipal;
use Test\TestCase;
/**
* @group DB
*/
class CustomPropertiesBackendTest extends TestCase {
+ private const BASE_URI = '/remote.php/dav/';
- /** @var Tree | \PHPUnit\Framework\MockObject\MockObject */
- private $tree;
-
- /** @var IDBConnection */
- private $dbConnection;
-
- /** @var IUser | \PHPUnit\Framework\MockObject\MockObject */
- private $user;
-
- /** @var CustomPropertiesBackend | \PHPUnit\Framework\MockObject\MockObject */
- private $backend;
+ private Server&MockObject $server;
+ private Tree&MockObject $tree;
+ private IDBConnection $dbConnection;
+ private IUser&MockObject $user;
+ private DefaultCalendarValidator&MockObject $defaultCalendarValidator;
+ private CustomPropertiesBackend $backend;
protected function setUp(): void {
parent::setUp();
+ $this->server = $this->createMock(Server::class);
+ $this->server->method('getBaseUri')
+ ->willReturn(self::BASE_URI);
$this->tree = $this->createMock(Tree::class);
$this->user = $this->createMock(IUser::class);
$this->user->method('getUID')
->with()
->willReturn('dummy_user_42');
- $this->dbConnection = \OC::$server->getDatabaseConnection();
+ $this->dbConnection = \OCP\Server::get(IDBConnection::class);
+ $this->defaultCalendarValidator = $this->createMock(DefaultCalendarValidator::class);
$this->backend = new CustomPropertiesBackend(
+ $this->server,
$this->tree,
$this->dbConnection,
- $this->user
+ $this->user,
+ $this->defaultCalendarValidator,
);
}
@@ -85,13 +76,19 @@ class CustomPropertiesBackendTest extends TestCase {
}
}
- protected function insertProps(string $user, string $path, array $props) {
+ protected function insertProps(string $user, string $path, array $props): void {
foreach ($props as $name => $value) {
$this->insertProp($user, $path, $name, $value);
}
}
- protected function insertProp(string $user, string $path, string $name, string $value) {
+ protected function insertProp(string $user, string $path, string $name, mixed $value): void {
+ $type = CustomPropertiesBackend::PROPERTY_TYPE_STRING;
+ if ($value instanceof Href) {
+ $value = $value->getHref();
+ $type = CustomPropertiesBackend::PROPERTY_TYPE_HREF;
+ }
+
$query = $this->dbConnection->getQueryBuilder();
$query->insert('properties')
->values([
@@ -99,37 +96,44 @@ class CustomPropertiesBackendTest extends TestCase {
'propertypath' => $query->createNamedParameter($this->formatPath($path)),
'propertyname' => $query->createNamedParameter($name),
'propertyvalue' => $query->createNamedParameter($value),
+ 'valuetype' => $query->createNamedParameter($type, IQueryBuilder::PARAM_INT)
]);
$query->execute();
}
- protected function getProps(string $user, string $path) {
+ protected function getProps(string $user, string $path): array {
$query = $this->dbConnection->getQueryBuilder();
- $query->select('propertyname', 'propertyvalue')
+ $query->select('propertyname', 'propertyvalue', 'valuetype')
->from('properties')
->where($query->expr()->eq('userid', $query->createNamedParameter($user)))
- ->where($query->expr()->eq('propertypath', $query->createNamedParameter($this->formatPath($path))));
+ ->andWhere($query->expr()->eq('propertypath', $query->createNamedParameter($this->formatPath($path))));
$result = $query->execute();
$data = [];
while ($row = $result->fetch()) {
- $data[$row['propertyname']] = $row['propertyvalue'];
+ $value = $row['propertyvalue'];
+ if ((int)$row['valuetype'] === CustomPropertiesBackend::PROPERTY_TYPE_HREF) {
+ $value = new Href($value);
+ }
+ $data[$row['propertyname']] = $value;
}
$result->closeCursor();
return $data;
}
- public function testPropFindNoDbCalls() {
+ public function testPropFindNoDbCalls(): void {
$db = $this->createMock(IDBConnection::class);
$backend = new CustomPropertiesBackend(
+ $this->server,
$this->tree,
$db,
- $this->user
+ $this->user,
+ $this->defaultCalendarValidator,
);
$propFind = $this->createMock(PropFind::class);
- $propFind->expects($this->at(0))
+ $propFind->expects($this->once())
->method('get404Properties')
->with()
->willReturn([
@@ -145,7 +149,7 @@ class CustomPropertiesBackendTest extends TestCase {
$backend->propFind('foo_bar_path_1337_0', $propFind);
}
- public function testPropFindCalendarCall() {
+ public function testPropFindCalendarCall(): void {
$propFind = $this->createMock(PropFind::class);
$propFind->method('get404Properties')
->with()
@@ -179,7 +183,7 @@ class CustomPropertiesBackendTest extends TestCase {
$setProps = [];
$propFind->method('set')
- ->willReturnCallback(function ($name, $value, $status) use (&$setProps) {
+ ->willReturnCallback(function ($name, $value, $status) use (&$setProps): void {
$setProps[$name] = $value;
});
@@ -187,10 +191,165 @@ class CustomPropertiesBackendTest extends TestCase {
$this->assertEquals($props, $setProps);
}
- /**
- * @dataProvider propPatchProvider
- */
- public function testPropPatch(string $path, array $existing, array $props, array $result) {
+ public function testPropFindPrincipalCall(): void {
+ $this->tree->method('getNodeForPath')
+ ->willReturnCallback(function ($uri) {
+ $node = $this->createMock(Calendar::class);
+ $node->method('getOwner')
+ ->willReturn('principals/users/dummy_user_42');
+ return $node;
+ });
+
+ $propFind = $this->createMock(PropFind::class);
+ $propFind->method('get404Properties')
+ ->with()
+ ->willReturn([
+ '{DAV:}getcontentlength',
+ '{DAV:}getcontenttype',
+ '{DAV:}getetag',
+ '{abc}def',
+ ]);
+
+ $propFind->method('getRequestedProperties')
+ ->with()
+ ->willReturn([
+ '{DAV:}getcontentlength',
+ '{DAV:}getcontenttype',
+ '{DAV:}getetag',
+ '{abc}def',
+ '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL',
+ ]);
+
+ $props = [
+ '{abc}def' => 'a',
+ '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/admin/personal'),
+ ];
+ $this->insertProps('dummy_user_42', 'principals/users/dummy_user_42', $props);
+
+ $setProps = [];
+ $propFind->method('set')
+ ->willReturnCallback(function ($name, $value, $status) use (&$setProps): void {
+ $setProps[$name] = $value;
+ });
+
+ $this->backend->propFind('principals/users/dummy_user_42', $propFind);
+ $this->assertEquals($props, $setProps);
+ }
+
+ public static function propFindPrincipalScheduleDefaultCalendarProviderUrlProvider(): array {
+ // [ user, nodes, existingProps, requestedProps, returnedProps ]
+ return [
+ [ // Exists
+ 'dummy_user_42',
+ ['calendars/dummy_user_42/foo/' => Calendar::class],
+ ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/dummy_user_42/foo/')],
+ ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'],
+ ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/dummy_user_42/foo/')],
+ ],
+ [ // Doesn't exist
+ 'dummy_user_42',
+ ['calendars/dummy_user_42/foo/' => Calendar::class],
+ ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/dummy_user_42/bar/')],
+ ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'],
+ [],
+ ],
+ [ // No privilege
+ 'dummy_user_42',
+ ['calendars/user2/baz/' => Calendar::class],
+ ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/user2/baz/')],
+ ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'],
+ [],
+ ],
+ [ // Not a calendar
+ 'dummy_user_42',
+ ['foo/dummy_user_42/bar/' => IACL::class],
+ ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/dummy_user_42/bar/')],
+ ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'],
+ [],
+ ],
+ ];
+
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('propFindPrincipalScheduleDefaultCalendarProviderUrlProvider')]
+ public function testPropFindPrincipalScheduleDefaultCalendarUrl(
+ string $user,
+ array $nodes,
+ array $existingProps,
+ array $requestedProps,
+ array $returnedProps,
+ ): void {
+ $propFind = $this->createMock(PropFind::class);
+ $propFind->method('get404Properties')
+ ->with()
+ ->willReturn([
+ '{DAV:}getcontentlength',
+ '{DAV:}getcontenttype',
+ '{DAV:}getetag',
+ ]);
+
+ $propFind->method('getRequestedProperties')
+ ->with()
+ ->willReturn(array_merge([
+ '{DAV:}getcontentlength',
+ '{DAV:}getcontenttype',
+ '{DAV:}getetag',
+ '{abc}def',
+ ],
+ $requestedProps,
+ ));
+
+ $this->server->method('calculateUri')
+ ->willReturnCallback(function ($uri) {
+ if (!str_starts_with($uri, self::BASE_URI)) {
+ return trim(substr($uri, strlen(self::BASE_URI)), '/');
+ }
+ return null;
+ });
+ $this->tree->method('getNodeForPath')
+ ->willReturnCallback(function ($uri) use ($nodes) {
+ if (str_starts_with($uri, 'principals/')) {
+ return $this->createMock(IPrincipal::class);
+ }
+ if (array_key_exists($uri, $nodes)) {
+ $owner = explode('/', $uri)[1];
+ $node = $this->createMock($nodes[$uri]);
+ $node->method('getOwner')
+ ->willReturn("principals/users/$owner");
+ return $node;
+ }
+ throw new NotFound('Node not found');
+ });
+
+ $this->insertProps($user, "principals/users/$user", $existingProps);
+
+ $setProps = [];
+ $propFind->method('set')
+ ->willReturnCallback(function ($name, $value, $status) use (&$setProps): void {
+ $setProps[$name] = $value;
+ });
+
+ $this->backend->propFind("principals/users/$user", $propFind);
+ $this->assertEquals($returnedProps, $setProps);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('propPatchProvider')]
+ public function testPropPatch(string $path, array $existing, array $props, array $result): void {
+ $this->server->method('calculateUri')
+ ->willReturnCallback(function ($uri) {
+ if (str_starts_with($uri, self::BASE_URI)) {
+ return trim(substr($uri, strlen(self::BASE_URI)), '/');
+ }
+ return null;
+ });
+ $this->tree->method('getNodeForPath')
+ ->willReturnCallback(function ($uri) {
+ $node = $this->createMock(Calendar::class);
+ $node->method('getOwner')
+ ->willReturn('principals/users/' . $this->user->getUID());
+ return $node;
+ });
+
$this->insertProps($this->user->getUID(), $path, $existing);
$propPatch = new PropPatch($props);
@@ -201,46 +360,102 @@ class CustomPropertiesBackendTest extends TestCase {
$this->assertEquals($result, $storedProps);
}
- public function propPatchProvider() {
+ public static function propPatchProvider(): array {
$longPath = str_repeat('long_path', 100);
return [
['foo_bar_path_1337', [], ['{DAV:}displayname' => 'anything'], ['{DAV:}displayname' => 'anything']],
['foo_bar_path_1337', ['{DAV:}displayname' => 'foo'], ['{DAV:}displayname' => 'anything'], ['{DAV:}displayname' => 'anything']],
['foo_bar_path_1337', ['{DAV:}displayname' => 'foo'], ['{DAV:}displayname' => null], []],
[$longPath, [], ['{DAV:}displayname' => 'anything'], ['{DAV:}displayname' => 'anything']],
+ ['principals/users/dummy_user_42', [], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/bar/')], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/bar/')]],
+ ['principals/users/dummy_user_42', [], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href(self::BASE_URI . 'foo/bar/')], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/bar/')]],
];
}
- /**
- * @dataProvider deleteProvider
- */
- public function testDelete(string $path) {
+ public function testPropPatchWithUnsuitableCalendar(): void {
+ $path = 'principals/users/' . $this->user->getUID();
+
+ $node = $this->createMock(Calendar::class);
+ $node->expects(self::once())
+ ->method('getOwner')
+ ->willReturn($path);
+
+ $this->defaultCalendarValidator->expects(self::once())
+ ->method('validateScheduleDefaultCalendar')
+ ->with($node)
+ ->willThrowException(new \Sabre\DAV\Exception('Invalid calendar'));
+
+ $this->server->method('calculateUri')
+ ->willReturnCallback(function ($uri) {
+ if (str_starts_with($uri, self::BASE_URI)) {
+ return trim(substr($uri, strlen(self::BASE_URI)), '/');
+ }
+ return null;
+ });
+ $this->tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('foo/bar/')
+ ->willReturn($node);
+
+ $storedProps = $this->getProps($this->user->getUID(), $path);
+ $this->assertEquals([], $storedProps);
+
+ $propPatch = new PropPatch([
+ '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/bar/'),
+ ]);
+ $this->backend->propPatch($path, $propPatch);
+ try {
+ $propPatch->commit();
+ } catch (\Throwable $e) {
+ $this->assertInstanceOf(\Sabre\DAV\Exception::class, $e);
+ }
+
+ $storedProps = $this->getProps($this->user->getUID(), $path);
+ $this->assertEquals([], $storedProps);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('deleteProvider')]
+ public function testDelete(string $path): void {
$this->insertProps('dummy_user_42', $path, ['foo' => 'bar']);
$this->backend->delete($path);
$this->assertEquals([], $this->getProps('dummy_user_42', $path));
}
- public function deleteProvider() {
+ public static function deleteProvider(): array {
return [
['foo_bar_path_1337'],
[str_repeat('long_path', 100)]
];
}
- /**
- * @dataProvider moveProvider
- */
- public function testMove(string $source, string $target) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('moveProvider')]
+ public function testMove(string $source, string $target): void {
$this->insertProps('dummy_user_42', $source, ['foo' => 'bar']);
$this->backend->move($source, $target);
$this->assertEquals([], $this->getProps('dummy_user_42', $source));
$this->assertEquals(['foo' => 'bar'], $this->getProps('dummy_user_42', $target));
}
- public function moveProvider() {
+ public static function moveProvider(): array {
return [
['foo_bar_path_1337', 'foo_bar_path_7333'],
[str_repeat('long_path1', 100), str_repeat('long_path2', 100)]
];
}
+
+ public function testDecodeValueFromDatabaseObjectCurrent(): void {
+ $propertyValue = 'O:48:"Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp":1:{s:8:"\x00*\x00value";s:6:"opaque";}';
+ $propertyType = 3;
+ $decodeValue = $this->invokePrivate($this->backend, 'decodeValueFromDatabase', [$propertyValue, $propertyType]);
+ $this->assertInstanceOf(\Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp::class, $decodeValue);
+ $this->assertEquals('opaque', $decodeValue->getValue());
+ }
+
+ public function testDecodeValueFromDatabaseObjectLegacy(): void {
+ $propertyValue = 'O:48:"Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp":1:{s:8:"' . chr(0) . '*' . chr(0) . 'value";s:6:"opaque";}';
+ $propertyType = 3;
+ $decodeValue = $this->invokePrivate($this->backend, 'decodeValueFromDatabase', [$propertyValue, $propertyType]);
+ $this->assertInstanceOf(\Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp::class, $decodeValue);
+ $this->assertEquals('opaque', $decodeValue->getValue());
+ }
}
diff --git a/apps/dav/tests/unit/DAV/GroupPrincipalTest.php b/apps/dav/tests/unit/DAV/GroupPrincipalTest.php
index 8f86961a6af..2756152a6e2 100644
--- a/apps/dav/tests/unit/DAV/GroupPrincipalTest.php
+++ b/apps/dav/tests/unit/DAV/GroupPrincipalTest.php
@@ -1,32 +1,10 @@
<?php
+
+declare(strict_types=1);
/**
- * @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 Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.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 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\Tests\unit\DAV;
@@ -42,20 +20,11 @@ use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\PropPatch;
class GroupPrincipalTest extends \Test\TestCase {
- /** @var IConfig|MockObject */
- private $config;
-
- /** @var IGroupManager | MockObject */
- private $groupManager;
-
- /** @var IUserSession | MockObject */
- private $userSession;
-
- /** @var IManager | MockObject */
- private $shareManager;
-
- /** @var GroupPrincipalBackend */
- private $connector;
+ private IConfig&MockObject $config;
+ private IGroupManager&MockObject $groupManager;
+ private IUserSession&MockObject $userSession;
+ private IManager&MockObject $shareManager;
+ private GroupPrincipalBackend $connector;
protected function setUp(): void {
$this->groupManager = $this->createMock(IGroupManager::class);
@@ -72,12 +41,12 @@ class GroupPrincipalTest extends \Test\TestCase {
parent::setUp();
}
- public function testGetPrincipalsByPrefixWithoutPrefix() {
+ public function testGetPrincipalsByPrefixWithoutPrefix(): void {
$response = $this->connector->getPrincipalsByPrefix('');
$this->assertSame([], $response);
}
- public function testGetPrincipalsByPrefixWithUsers() {
+ public function testGetPrincipalsByPrefixWithUsers(): void {
$group1 = $this->mockGroup('foo');
$group2 = $this->mockGroup('bar');
$this->groupManager
@@ -102,7 +71,7 @@ class GroupPrincipalTest extends \Test\TestCase {
$this->assertSame($expectedResponse, $response);
}
- public function testGetPrincipalsByPrefixEmpty() {
+ public function testGetPrincipalsByPrefixEmpty(): void {
$this->groupManager
->expects($this->once())
->method('search')
@@ -113,7 +82,7 @@ class GroupPrincipalTest extends \Test\TestCase {
$this->assertSame([], $response);
}
- public function testGetPrincipalsByPathWithoutMail() {
+ public function testGetPrincipalsByPathWithoutMail(): void {
$group1 = $this->mockGroup('foo');
$this->groupManager
->expects($this->once())
@@ -130,7 +99,7 @@ class GroupPrincipalTest extends \Test\TestCase {
$this->assertSame($expectedResponse, $response);
}
- public function testGetPrincipalsByPathWithMail() {
+ public function testGetPrincipalsByPathWithMail(): void {
$fooUser = $this->mockGroup('foo');
$this->groupManager
->expects($this->once())
@@ -147,7 +116,7 @@ class GroupPrincipalTest extends \Test\TestCase {
$this->assertSame($expectedResponse, $response);
}
- public function testGetPrincipalsByPathEmpty() {
+ public function testGetPrincipalsByPathEmpty(): void {
$this->groupManager
->expects($this->once())
->method('get')
@@ -158,7 +127,7 @@ class GroupPrincipalTest extends \Test\TestCase {
$this->assertSame(null, $response);
}
- public function testGetPrincipalsByPathGroupWithSlash() {
+ public function testGetPrincipalsByPathGroupWithSlash(): void {
$group1 = $this->mockGroup('foo/bar');
$this->groupManager
->expects($this->once())
@@ -175,7 +144,7 @@ class GroupPrincipalTest extends \Test\TestCase {
$this->assertSame($expectedResponse, $response);
}
- public function testGetPrincipalsByPathGroupWithHash() {
+ public function testGetPrincipalsByPathGroupWithHash(): void {
$group1 = $this->mockGroup('foo#bar');
$this->groupManager
->expects($this->once())
@@ -192,45 +161,38 @@ class GroupPrincipalTest extends \Test\TestCase {
$this->assertSame($expectedResponse, $response);
}
- public function testGetGroupMemberSet() {
+ public function testGetGroupMemberSet(): void {
$response = $this->connector->getGroupMemberSet('principals/groups/foo');
$this->assertSame([], $response);
}
- public function testGetGroupMembership() {
+ public function testGetGroupMembership(): void {
$response = $this->connector->getGroupMembership('principals/groups/foo');
$this->assertSame([], $response);
}
- public function testSetGroupMembership() {
+ public function testSetGroupMembership(): void {
$this->expectException(\Sabre\DAV\Exception::class);
$this->expectExceptionMessage('Setting members of the group is not supported yet');
$this->connector->setGroupMemberSet('principals/groups/foo', ['foo']);
}
- public function testUpdatePrincipal() {
+ public function testUpdatePrincipal(): void {
$this->assertSame(0, $this->connector->updatePrincipal('foo', new PropPatch([])));
}
- public function testSearchPrincipalsWithEmptySearchProperties() {
+ public function testSearchPrincipalsWithEmptySearchProperties(): void {
$this->assertSame([], $this->connector->searchPrincipals('principals/groups', []));
}
- public function testSearchPrincipalsWithWrongPrefixPath() {
+ public function testSearchPrincipalsWithWrongPrefixPath(): void {
$this->assertSame([], $this->connector->searchPrincipals('principals/users',
['{DAV:}displayname' => 'Foo']));
}
- /**
- * @dataProvider searchPrincipalsDataProvider
- * @param bool $sharingEnabled
- * @param bool $groupSharingEnabled
- * @param bool $groupsOnly
- * @param string $test
- * @param array $result
- */
+ #[\PHPUnit\Framework\Attributes\DataProvider('searchPrincipalsDataProvider')]
public function testSearchPrincipals(bool $sharingEnabled, bool $groupSharingEnabled, bool $groupsOnly, string $test, array $result): void {
$this->shareManager->expects($this->once())
->method('shareAPIEnabled')
@@ -288,7 +250,7 @@ class GroupPrincipalTest extends \Test\TestCase {
['{DAV:}displayname' => 'Foo'], $test));
}
- public function searchPrincipalsDataProvider() {
+ public static function searchPrincipalsDataProvider(): array {
return [
[true, true, false, 'allof', ['principals/groups/group1', 'principals/groups/group2', 'principals/groups/group3', 'principals/groups/group4', 'principals/groups/group5']],
[true, true, false, 'anyof', ['principals/groups/group1', 'principals/groups/group2', 'principals/groups/group3', 'principals/groups/group4', 'principals/groups/group5']],
@@ -301,14 +263,7 @@ class GroupPrincipalTest extends \Test\TestCase {
];
}
- /**
- * @dataProvider findByUriDataProvider
- * @param bool $sharingEnabled
- * @param bool $groupSharingEnabled
- * @param bool $groupsOnly
- * @param string $findUri
- * @param string|null $result
- */
+ #[\PHPUnit\Framework\Attributes\DataProvider('findByUriDataProvider')]
public function testFindByUri(bool $sharingEnabled, bool $groupSharingEnabled, bool $groupsOnly, string $findUri, ?string $result): void {
$this->shareManager->expects($this->once())
->method('shareAPIEnabled')
@@ -329,7 +284,7 @@ class GroupPrincipalTest extends \Test\TestCase {
->method('getUser')
->willReturn($user);
- $this->groupManager->expects($this->at(0))
+ $this->groupManager->expects($this->once())
->method('getUserGroupIds')
->with($user)
->willReturn(['group1', 'group2', 'group5']);
@@ -344,7 +299,7 @@ class GroupPrincipalTest extends \Test\TestCase {
$this->assertEquals($result, $this->connector->findByUri($findUri, 'principals/groups'));
}
- public function findByUriDataProvider() {
+ public static function findByUriDataProvider(): array {
return [
[false, false, false, 'principal:principals/groups/group1', null],
[false, false, false, 'principal:principals/groups/group3', null],
@@ -361,10 +316,7 @@ class GroupPrincipalTest extends \Test\TestCase {
];
}
- /**
- * @return Group|MockObject
- */
- private function mockGroup($gid) {
+ private function mockGroup(string $gid): Group&MockObject {
$fooGroup = $this->createMock(Group::class);
$fooGroup
->expects($this->exactly(1))
@@ -373,7 +325,7 @@ class GroupPrincipalTest extends \Test\TestCase {
$fooGroup
->expects($this->exactly(1))
->method('getDisplayName')
- ->willReturn('Group '.$gid);
+ ->willReturn('Group ' . $gid);
return $fooGroup;
}
}
diff --git a/apps/dav/tests/unit/DAV/HookManagerTest.php b/apps/dav/tests/unit/DAV/HookManagerTest.php
deleted file mode 100644
index 503062c75db..00000000000
--- a/apps/dav/tests/unit/DAV/HookManagerTest.php
+++ /dev/null
@@ -1,250 +0,0 @@
-<?php
-/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @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 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 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\Tests\unit\DAV;
-
-use OCA\DAV\CalDAV\CalDavBackend;
-use OCA\DAV\CardDAV\CardDavBackend;
-use OCA\DAV\CardDAV\SyncService;
-use OCA\DAV\HookManager;
-use OCP\Defaults;
-use OCP\IL10N;
-use OCP\IUser;
-use OCP\IUserManager;
-use PHPUnit\Framework\MockObject\MockObject;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
-use Test\TestCase;
-
-class HookManagerTest extends TestCase {
- /** @var IL10N */
- private $l10n;
-
- /** @var EventDispatcherInterface | MockObject */
- private $eventDispatcher;
-
- protected function setUp(): void {
- parent::setUp();
- $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class);
- $this->l10n = $this->createMock(IL10N::class);
- $this->l10n
- ->expects($this->any())
- ->method('t')
- ->willReturnCallback(function ($text, $parameters = []) {
- return vsprintf($text, $parameters);
- });
- }
-
- public function test() {
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
- $user->expects($this->once())->method('getUID')->willReturn('newUser');
-
- /** @var IUserManager | MockObject $userManager */
- $userManager = $this->getMockBuilder(IUserManager::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- /** @var SyncService | MockObject $syncService */
- $syncService = $this->getMockBuilder(SyncService::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- /** @var Defaults | MockObject $syncService */
- $defaults = $this->getMockBuilder(Defaults::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $defaults->expects($this->once())->method('getColorPrimary')->willReturn('#745bca');
-
- /** @var CalDavBackend | MockObject $cal */
- $cal = $this->getMockBuilder(CalDavBackend::class)
- ->disableOriginalConstructor()
- ->getMock();
- $cal->expects($this->once())->method('getCalendarsForUserCount')->willReturn(0);
- $cal->expects($this->once())->method('createCalendar')->with(
- 'principals/users/newUser',
- 'personal', [
- '{DAV:}displayname' => 'Personal',
- '{http://apple.com/ns/ical/}calendar-color' => '#745bca',
- 'components' => 'VEVENT'
- ]);
-
- /** @var CardDavBackend | MockObject $card */
- $card = $this->getMockBuilder(CardDavBackend::class)
- ->disableOriginalConstructor()
- ->getMock();
- $card->expects($this->once())->method('getAddressBooksForUserCount')->willReturn(0);
- $card->expects($this->once())->method('createAddressBook')->with(
- 'principals/users/newUser',
- 'contacts', ['{DAV:}displayname' => 'Contacts']);
-
- $hm = new HookManager($userManager, $syncService, $cal, $card, $defaults, $this->eventDispatcher);
- $hm->firstLogin($user);
- }
-
- public function testWithExisting() {
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
- $user->expects($this->once())->method('getUID')->willReturn('newUser');
-
- /** @var IUserManager | MockObject $userManager */
- $userManager = $this->getMockBuilder(IUserManager::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- /** @var SyncService | MockObject $syncService */
- $syncService = $this->getMockBuilder(SyncService::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- /** @var Defaults | MockObject $syncService */
- $defaults = $this->getMockBuilder(Defaults::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- /** @var CalDavBackend | MockObject $cal */
- $cal = $this->getMockBuilder(CalDavBackend::class)
- ->disableOriginalConstructor()
- ->getMock();
- $cal->expects($this->once())->method('getCalendarsForUserCount')->willReturn(1);
- $cal->expects($this->never())->method('createCalendar');
-
- /** @var CardDavBackend | MockObject $card */
- $card = $this->getMockBuilder(CardDavBackend::class)
- ->disableOriginalConstructor()
- ->getMock();
- $card->expects($this->once())->method('getAddressBooksForUserCount')->willReturn(1);
- $card->expects($this->never())->method('createAddressBook');
-
- $hm = new HookManager($userManager, $syncService, $cal, $card, $defaults, $this->eventDispatcher);
- $hm->firstLogin($user);
- }
-
- public function testWithBirthdayCalendar() {
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
- $user->expects($this->once())->method('getUID')->willReturn('newUser');
-
- /** @var IUserManager | MockObject $userManager */
- $userManager = $this->getMockBuilder(IUserManager::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- /** @var SyncService | MockObject $syncService */
- $syncService = $this->getMockBuilder(SyncService::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- /** @var Defaults | MockObject $syncService */
- $defaults = $this->getMockBuilder(Defaults::class)
- ->disableOriginalConstructor()
- ->getMock();
- $defaults->expects($this->once())->method('getColorPrimary')->willReturn('#745bca');
-
- /** @var CalDavBackend | MockObject $cal */
- $cal = $this->getMockBuilder(CalDavBackend::class)
- ->disableOriginalConstructor()
- ->getMock();
- $cal->expects($this->once())->method('getCalendarsForUserCount')->willReturn(0);
- $cal->expects($this->once())->method('createCalendar')->with(
- 'principals/users/newUser',
- 'personal', [
- '{DAV:}displayname' => 'Personal',
- '{http://apple.com/ns/ical/}calendar-color' => '#745bca',
- 'components' => 'VEVENT'
- ]);
-
- /** @var CardDavBackend | MockObject $card */
- $card = $this->getMockBuilder(CardDavBackend::class)
- ->disableOriginalConstructor()
- ->getMock();
- $card->expects($this->once())->method('getAddressBooksForUserCount')->willReturn(0);
- $card->expects($this->once())->method('createAddressBook')->with(
- 'principals/users/newUser',
- 'contacts', ['{DAV:}displayname' => 'Contacts']);
-
- $hm = new HookManager($userManager, $syncService, $cal, $card, $defaults, $this->eventDispatcher);
- $hm->firstLogin($user);
- }
-
- public function testDeleteCalendar() {
- $user = $this->getMockBuilder(IUser::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- /** @var IUserManager | MockObject $userManager */
- $userManager = $this->getMockBuilder(IUserManager::class)
- ->disableOriginalConstructor()
- ->getMock();
- $userManager->expects($this->once())->method('get')->willReturn($user);
-
- /** @var SyncService | MockObject $syncService */
- $syncService = $this->getMockBuilder(SyncService::class)
- ->disableOriginalConstructor()
- ->getMock();
- $syncService->expects($this->once())
- ->method('deleteUser');
-
- /** @var Defaults | MockObject $syncService */
- $defaults = $this->getMockBuilder(Defaults::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- /** @var CalDavBackend | MockObject $cal */
- $cal = $this->getMockBuilder(CalDavBackend::class)
- ->disableOriginalConstructor()
- ->getMock();
- $cal->expects($this->once())->method('getUsersOwnCalendars')->willReturn([
- ['id' => 'personal']
- ]);
- $cal->expects($this->once())->method('getSubscriptionsForUser')->willReturn([
- ['id' => 'some-subscription']
- ]);
- $cal->expects($this->once())->method('deleteCalendar')->with('personal');
- $cal->expects($this->once())->method('deleteSubscription')->with('some-subscription');
- $cal->expects($this->once())->method('deleteAllSharesByUser');
-
- /** @var CardDavBackend | MockObject $card */
- $card = $this->getMockBuilder(CardDavBackend::class)
- ->disableOriginalConstructor()
- ->getMock();
- $card->expects($this->once())->method('getUsersOwnAddressBooks')->willReturn([
- ['id' => 'personal']
- ]);
- $card->expects($this->once())->method('deleteAddressBook');
-
- $hm = new HookManager($userManager, $syncService, $cal, $card, $defaults, $this->eventDispatcher);
- $hm->preDeleteUser(['uid' => 'newUser']);
- $hm->postDeleteUser(['uid' => 'newUser']);
- }
-}
diff --git a/apps/dav/tests/unit/DAV/Listener/UserEventsListenerTest.php b/apps/dav/tests/unit/DAV/Listener/UserEventsListenerTest.php
new file mode 100644
index 00000000000..8e410eb0a78
--- /dev/null
+++ b/apps/dav/tests/unit/DAV/Listener/UserEventsListenerTest.php
@@ -0,0 +1,183 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace OCA\DAV\Tests\unit\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\Listener\UserEventsListener;
+use OCA\DAV\Service\ExampleContactService;
+use OCA\DAV\Service\ExampleEventService;
+use OCP\BackgroundJob\IJobList;
+use OCP\Defaults;
+use OCP\IUser;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class UserEventsListenerTest extends TestCase {
+ private IUserManager&MockObject $userManager;
+ private SyncService&MockObject $syncService;
+ private CalDavBackend&MockObject $calDavBackend;
+ private CardDavBackend&MockObject $cardDavBackend;
+ private Defaults&MockObject $defaults;
+ private ExampleContactService&MockObject $exampleContactService;
+ private ExampleEventService&MockObject $exampleEventService;
+ private LoggerInterface&MockObject $logger;
+
+ private UserEventsListener $userEventsListener;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->syncService = $this->createMock(SyncService::class);
+ $this->calDavBackend = $this->createMock(CalDavBackend::class);
+ $this->cardDavBackend = $this->createMock(CardDavBackend::class);
+ $this->defaults = $this->createMock(Defaults::class);
+ $this->exampleContactService = $this->createMock(ExampleContactService::class);
+ $this->exampleEventService = $this->createMock(ExampleEventService::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->jobList = $this->createMock(IJobList::class);
+
+ $this->userEventsListener = new UserEventsListener(
+ $this->userManager,
+ $this->syncService,
+ $this->calDavBackend,
+ $this->cardDavBackend,
+ $this->defaults,
+ $this->exampleContactService,
+ $this->exampleEventService,
+ $this->logger,
+ $this->jobList,
+ );
+ }
+
+ public function test(): void {
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->once())->method('getUID')->willReturn('newUser');
+
+ $this->defaults->expects($this->once())->method('getColorPrimary')->willReturn('#745bca');
+
+ $this->calDavBackend->expects($this->once())->method('getCalendarsForUserCount')->willReturn(0);
+ $this->calDavBackend->expects($this->once())->method('createCalendar')->with(
+ 'principals/users/newUser',
+ 'personal', [
+ '{DAV:}displayname' => 'Personal',
+ '{http://apple.com/ns/ical/}calendar-color' => '#745bca',
+ 'components' => 'VEVENT'
+ ])
+ ->willReturn(1000);
+ $this->calDavBackend->expects(self::never())
+ ->method('getCalendarsForUser');
+ $this->exampleEventService->expects(self::once())
+ ->method('createExampleEvent')
+ ->with(1000);
+
+ $this->cardDavBackend->expects($this->once())->method('getAddressBooksForUserCount')->willReturn(0);
+ $this->cardDavBackend->expects($this->once())->method('createAddressBook')->with(
+ 'principals/users/newUser',
+ 'contacts', ['{DAV:}displayname' => 'Contacts']);
+
+ $this->userEventsListener->firstLogin($user);
+ }
+
+ public function testWithExisting(): void {
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->once())->method('getUID')->willReturn('newUser');
+
+ $this->calDavBackend->expects($this->once())->method('getCalendarsForUserCount')->willReturn(1);
+ $this->calDavBackend->expects($this->never())->method('createCalendar');
+ $this->calDavBackend->expects(self::never())
+ ->method('createCalendar');
+ $this->exampleEventService->expects(self::never())
+ ->method('createExampleEvent');
+
+ $this->cardDavBackend->expects($this->once())->method('getAddressBooksForUserCount')->willReturn(1);
+ $this->cardDavBackend->expects($this->never())->method('createAddressBook');
+
+ $this->userEventsListener->firstLogin($user);
+ }
+
+ public function testWithBirthdayCalendar(): void {
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->once())->method('getUID')->willReturn('newUser');
+
+ $this->defaults->expects($this->once())->method('getColorPrimary')->willReturn('#745bca');
+
+ $this->calDavBackend->expects($this->once())->method('getCalendarsForUserCount')->willReturn(0);
+ $this->calDavBackend->expects($this->once())->method('createCalendar')->with(
+ 'principals/users/newUser',
+ 'personal', [
+ '{DAV:}displayname' => 'Personal',
+ '{http://apple.com/ns/ical/}calendar-color' => '#745bca',
+ 'components' => 'VEVENT'
+ ]);
+
+ $this->cardDavBackend->expects($this->once())->method('getAddressBooksForUserCount')->willReturn(0);
+ $this->cardDavBackend->expects($this->once())->method('createAddressBook')->with(
+ 'principals/users/newUser',
+ 'contacts', ['{DAV:}displayname' => 'Contacts']);
+
+ $this->userEventsListener->firstLogin($user);
+ }
+
+ public function testDeleteCalendar(): void {
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->once())->method('getUID')->willReturn('newUser');
+
+ $this->syncService->expects($this->once())
+ ->method('deleteUser');
+
+ $this->calDavBackend->expects($this->once())->method('getUsersOwnCalendars')->willReturn([
+ ['id' => 'personal']
+ ]);
+ $this->calDavBackend->expects($this->once())->method('getSubscriptionsForUser')->willReturn([
+ ['id' => 'some-subscription']
+ ]);
+ $this->calDavBackend->expects($this->once())->method('deleteCalendar')->with('personal');
+ $this->calDavBackend->expects($this->once())->method('deleteSubscription')->with('some-subscription');
+ $this->calDavBackend->expects($this->once())->method('deleteAllSharesByUser');
+
+ $this->cardDavBackend->expects($this->once())->method('getUsersOwnAddressBooks')->willReturn([
+ ['id' => 'personal']
+ ]);
+ $this->cardDavBackend->expects($this->once())->method('deleteAddressBook');
+
+ $this->userEventsListener->preDeleteUser($user);
+ $this->userEventsListener->postDeleteUser('newUser');
+ }
+
+ public function testDeleteUserAutomationEvent(): void {
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->once())->method('getUID')->willReturn('newUser');
+
+ $this->syncService->expects($this->once())
+ ->method('deleteUser');
+
+ $this->calDavBackend->expects($this->once())->method('getUsersOwnCalendars')->willReturn([
+ ['id' => []]
+ ]);
+ $this->calDavBackend->expects($this->once())->method('getSubscriptionsForUser')->willReturn([
+ ['id' => []]
+ ]);
+ $this->cardDavBackend->expects($this->once())->method('getUsersOwnAddressBooks')->willReturn([
+ ['id' => []]
+ ]);
+
+ $this->jobList->expects(self::once())->method('remove')->with(UserStatusAutomation::class, ['userId' => 'newUser']);
+
+ $this->userEventsListener->preDeleteUser($user);
+ $this->userEventsListener->postDeleteUser('newUser');
+ }
+}
diff --git a/apps/dav/tests/unit/DAV/Sharing/BackendTest.php b/apps/dav/tests/unit/DAV/Sharing/BackendTest.php
new file mode 100644
index 00000000000..556a623a73f
--- /dev/null
+++ b/apps/dav/tests/unit/DAV/Sharing/BackendTest.php
@@ -0,0 +1,399 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\DAV\Sharing;
+
+use OCA\DAV\CalDAV\Sharing\Backend as CalendarSharingBackend;
+use OCA\DAV\CalDAV\Sharing\Service;
+use OCA\DAV\CardDAV\Sharing\Backend as ContactsSharingBackend;
+use OCA\DAV\Connector\Sabre\Principal;
+use OCA\DAV\DAV\Sharing\Backend;
+use OCA\DAV\DAV\Sharing\IShareable;
+use OCP\ICache;
+use OCP\ICacheFactory;
+use OCP\IDBConnection;
+use OCP\IGroupManager;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class BackendTest extends TestCase {
+
+ private IDBConnection&MockObject $db;
+ private IUserManager&MockObject $userManager;
+ private IGroupManager&MockObject $groupManager;
+ private Principal&MockObject $principalBackend;
+ private ICache&MockObject $shareCache;
+ private LoggerInterface&MockObject $logger;
+ private ICacheFactory&MockObject $cacheFactory;
+ private Service&MockObject $calendarService;
+ private CalendarSharingBackend $backend;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->db = $this->createMock(IDBConnection::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->principalBackend = $this->createMock(Principal::class);
+ $this->cacheFactory = $this->createMock(ICacheFactory::class);
+ $this->shareCache = $this->createMock(ICache::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->calendarService = $this->createMock(Service::class);
+ $this->cacheFactory->expects(self::any())
+ ->method('createInMemory')
+ ->willReturn($this->shareCache);
+
+ $this->backend = new CalendarSharingBackend(
+ $this->userManager,
+ $this->groupManager,
+ $this->principalBackend,
+ $this->cacheFactory,
+ $this->calendarService,
+ $this->logger,
+ );
+ }
+
+ public function testUpdateShareCalendarBob(): void {
+ $shareable = $this->createConfiguredMock(IShareable::class, [
+ 'getOwner' => 'principals/users/alice',
+ 'getResourceId' => 42,
+ ]);
+ $add = [
+ [
+ 'href' => 'principal:principals/users/bob',
+ 'readOnly' => true,
+ ]
+ ];
+ $principal = 'principals/users/bob';
+
+ $this->shareCache->expects(self::once())
+ ->method('clear');
+ $this->principalBackend->expects(self::once())
+ ->method('findByUri')
+ ->willReturn($principal);
+ $this->userManager->expects(self::once())
+ ->method('userExists')
+ ->willReturn(true);
+ $this->groupManager->expects(self::never())
+ ->method('groupExists');
+ $this->calendarService->expects(self::once())
+ ->method('shareWith')
+ ->with($shareable->getResourceId(), $principal, Backend::ACCESS_READ);
+
+ $this->backend->updateShares($shareable, $add, []);
+ }
+
+ public function testUpdateShareCalendarGroup(): void {
+ $shareable = $this->createConfiguredMock(IShareable::class, [
+ 'getOwner' => 'principals/users/alice',
+ 'getResourceId' => 42,
+ ]);
+ $add = [
+ [
+ 'href' => 'principal:principals/groups/bob',
+ 'readOnly' => true,
+ ]
+ ];
+ $principal = 'principals/groups/bob';
+
+ $this->shareCache->expects(self::once())
+ ->method('clear');
+ $this->principalBackend->expects(self::once())
+ ->method('findByUri')
+ ->willReturn($principal);
+ $this->userManager->expects(self::never())
+ ->method('userExists');
+ $this->groupManager->expects(self::once())
+ ->method('groupExists')
+ ->willReturn(true);
+ $this->calendarService->expects(self::once())
+ ->method('shareWith')
+ ->with($shareable->getResourceId(), $principal, Backend::ACCESS_READ);
+
+ $this->backend->updateShares($shareable, $add, []);
+ }
+
+ public function testUpdateShareContactsBob(): void {
+ $shareable = $this->createConfiguredMock(IShareable::class, [
+ 'getOwner' => 'principals/users/alice',
+ 'getResourceId' => 42,
+ ]);
+ $add = [
+ [
+ 'href' => 'principal:principals/users/bob',
+ 'readOnly' => true,
+ ]
+ ];
+ $principal = 'principals/users/bob';
+
+ $this->shareCache->expects(self::once())
+ ->method('clear');
+ $this->principalBackend->expects(self::once())
+ ->method('findByUri')
+ ->willReturn($principal);
+ $this->userManager->expects(self::once())
+ ->method('userExists')
+ ->willReturn(true);
+ $this->groupManager->expects(self::never())
+ ->method('groupExists');
+ $this->calendarService->expects(self::once())
+ ->method('shareWith')
+ ->with($shareable->getResourceId(), $principal, Backend::ACCESS_READ);
+
+ $this->backend->updateShares($shareable, $add, []);
+ }
+
+ public function testUpdateShareContactsGroup(): void {
+ $shareable = $this->createConfiguredMock(IShareable::class, [
+ 'getOwner' => 'principals/users/alice',
+ 'getResourceId' => 42,
+ ]);
+ $add = [
+ [
+ 'href' => 'principal:principals/groups/bob',
+ 'readOnly' => true,
+ ]
+ ];
+ $principal = 'principals/groups/bob';
+
+ $this->shareCache->expects(self::once())
+ ->method('clear');
+ $this->principalBackend->expects(self::once())
+ ->method('findByUri')
+ ->willReturn($principal);
+ $this->userManager->expects(self::never())
+ ->method('userExists');
+ $this->groupManager->expects(self::once())
+ ->method('groupExists')
+ ->willReturn(true);
+ $this->calendarService->expects(self::once())
+ ->method('shareWith')
+ ->with($shareable->getResourceId(), $principal, Backend::ACCESS_READ);
+
+ $this->backend->updateShares($shareable, $add, []);
+ }
+
+ public function testUpdateShareCircle(): void {
+ $shareable = $this->createConfiguredMock(IShareable::class, [
+ 'getOwner' => 'principals/users/alice',
+ 'getResourceId' => 42,
+ ]);
+ $add = [
+ [
+ 'href' => 'principal:principals/circles/bob',
+ 'readOnly' => true,
+ ]
+ ];
+ $principal = 'principals/groups/bob';
+
+ $this->shareCache->expects(self::once())
+ ->method('clear');
+ $this->principalBackend->expects(self::once())
+ ->method('findByUri')
+ ->willReturn($principal);
+ $this->userManager->expects(self::never())
+ ->method('userExists');
+ $this->groupManager->expects(self::once())
+ ->method('groupExists')
+ ->willReturn(true);
+ $this->calendarService->expects(self::once())
+ ->method('shareWith')
+ ->with($shareable->getResourceId(), $principal, Backend::ACCESS_READ);
+
+ $this->backend->updateShares($shareable, $add, []);
+ }
+
+ public function testUnshareBob(): void {
+ $shareable = $this->createConfiguredMock(IShareable::class, [
+ 'getOwner' => 'principals/users/alice',
+ 'getResourceId' => 42,
+ ]);
+ $remove = [
+ 'principal:principals/users/bob',
+ ];
+ $principal = 'principals/users/bob';
+
+ $this->shareCache->expects(self::once())
+ ->method('clear');
+ $this->principalBackend->expects(self::once())
+ ->method('findByUri')
+ ->willReturn($principal);
+ $this->calendarService->expects(self::once())
+ ->method('deleteShare')
+ ->with($shareable->getResourceId(), $principal);
+ $this->calendarService->expects(self::never())
+ ->method('unshare');
+
+ $this->backend->updateShares($shareable, [], $remove);
+ }
+
+ public function testUnshareWithBobGroup(): void {
+ $shareable = $this->createConfiguredMock(IShareable::class, [
+ 'getOwner' => 'principals/users/alice',
+ 'getResourceId' => 42,
+ ]);
+ $remove = [
+ 'principal:principals/users/bob',
+ ];
+ $oldShares = [
+ [
+ 'href' => 'principal:principals/groups/bob',
+ 'commonName' => 'bob',
+ 'status' => 1,
+ 'readOnly' => true,
+ '{http://owncloud.org/ns}principal' => 'principals/groups/bob',
+ '{http://owncloud.org/ns}group-share' => true,
+ ]
+ ];
+
+
+ $this->shareCache->expects(self::once())
+ ->method('clear');
+ $this->principalBackend->expects(self::once())
+ ->method('findByUri')
+ ->willReturn('principals/users/bob');
+ $this->calendarService->expects(self::once())
+ ->method('deleteShare')
+ ->with($shareable->getResourceId(), 'principals/users/bob');
+ $this->calendarService->expects(self::never())
+ ->method('unshare');
+
+ $this->backend->updateShares($shareable, [], $remove, $oldShares);
+ }
+
+ public function testGetShares(): void {
+ $resourceId = 42;
+ $principal = 'principals/groups/bob';
+ $rows = [
+ [
+ 'principaluri' => $principal,
+ 'access' => Backend::ACCESS_READ,
+ ]
+ ];
+ $expected = [
+ [
+ 'href' => 'principal:principals/groups/bob',
+ 'commonName' => 'bob',
+ 'status' => 1,
+ 'readOnly' => true,
+ '{http://owncloud.org/ns}principal' => $principal,
+ '{http://owncloud.org/ns}group-share' => true,
+ ]
+ ];
+
+
+ $this->shareCache->expects(self::once())
+ ->method('get')
+ ->with((string)$resourceId)
+ ->willReturn(null);
+ $this->calendarService->expects(self::once())
+ ->method('getShares')
+ ->with($resourceId)
+ ->willReturn($rows);
+ $this->principalBackend->expects(self::once())
+ ->method('getPrincipalByPath')
+ ->with($principal)
+ ->willReturn(['uri' => $principal, '{DAV:}displayname' => 'bob']);
+ $this->shareCache->expects(self::once())
+ ->method('set')
+ ->with((string)$resourceId, $expected);
+
+ $result = $this->backend->getShares($resourceId);
+ $this->assertEquals($expected, $result);
+ }
+
+ public function testGetSharesAddressbooks(): void {
+ $service = $this->createMock(\OCA\DAV\CardDAV\Sharing\Service::class);
+ $backend = new ContactsSharingBackend(
+ $this->userManager,
+ $this->groupManager,
+ $this->principalBackend,
+ $this->cacheFactory,
+ $service,
+ $this->logger);
+ $resourceId = 42;
+ $principal = 'principals/groups/bob';
+ $rows = [
+ [
+ 'principaluri' => $principal,
+ 'access' => Backend::ACCESS_READ,
+ ]
+ ];
+ $expected = [
+ [
+ 'href' => 'principal:principals/groups/bob',
+ 'commonName' => 'bob',
+ 'status' => 1,
+ 'readOnly' => true,
+ '{http://owncloud.org/ns}principal' => $principal,
+ '{http://owncloud.org/ns}group-share' => true,
+ ]
+ ];
+
+ $this->shareCache->expects(self::once())
+ ->method('get')
+ ->with((string)$resourceId)
+ ->willReturn(null);
+ $service->expects(self::once())
+ ->method('getShares')
+ ->with($resourceId)
+ ->willReturn($rows);
+ $this->principalBackend->expects(self::once())
+ ->method('getPrincipalByPath')
+ ->with($principal)
+ ->willReturn(['uri' => $principal, '{DAV:}displayname' => 'bob']);
+ $this->shareCache->expects(self::once())
+ ->method('set')
+ ->with((string)$resourceId, $expected);
+
+ $result = $backend->getShares($resourceId);
+ $this->assertEquals($expected, $result);
+ }
+
+ public function testPreloadShares(): void {
+ $resourceIds = [42, 99];
+ $rows = [
+ [
+ 'resourceid' => 42,
+ 'principaluri' => 'principals/groups/bob',
+ 'access' => Backend::ACCESS_READ,
+ ],
+ [
+ 'resourceid' => 99,
+ 'principaluri' => 'principals/users/carlos',
+ 'access' => Backend::ACCESS_READ_WRITE,
+ ]
+ ];
+ $principalResults = [
+ ['uri' => 'principals/groups/bob', '{DAV:}displayname' => 'bob'],
+ ['uri' => 'principals/users/carlos', '{DAV:}displayname' => 'carlos'],
+ ];
+
+ $this->shareCache->expects(self::exactly(2))
+ ->method('get')
+ ->willReturn(null);
+ $this->calendarService->expects(self::once())
+ ->method('getSharesForIds')
+ ->with($resourceIds)
+ ->willReturn($rows);
+ $this->principalBackend->expects(self::exactly(2))
+ ->method('getPrincipalByPath')
+ ->willReturnCallback(function (string $principal) use ($principalResults) {
+ switch ($principal) {
+ case 'principals/groups/bob':
+ return $principalResults[0];
+ default:
+ return $principalResults[1];
+ }
+ });
+ $this->shareCache->expects(self::exactly(2))
+ ->method('set');
+
+ $this->backend->preloadShares($resourceIds);
+ }
+}
diff --git a/apps/dav/tests/unit/DAV/Sharing/PluginTest.php b/apps/dav/tests/unit/DAV/Sharing/PluginTest.php
index 1c4bb5e12e4..7a88f7cc5dd 100644
--- a/apps/dav/tests/unit/DAV/Sharing/PluginTest.php
+++ b/apps/dav/tests/unit/DAV/Sharing/PluginTest.php
@@ -1,28 +1,10 @@
<?php
+
+declare(strict_types=1);
/**
- * @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 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\Tests\unit\DAV\Sharing;
@@ -31,6 +13,7 @@ use OCA\DAV\DAV\Sharing\IShareable;
use OCA\DAV\DAV\Sharing\Plugin;
use OCP\IConfig;
use OCP\IRequest;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\Server;
use Sabre\DAV\SimpleCollection;
use Sabre\HTTP\Request;
@@ -38,38 +21,30 @@ use Sabre\HTTP\Response;
use Test\TestCase;
class PluginTest extends TestCase {
-
- /** @var Plugin */
- private $plugin;
- /** @var Server */
- private $server;
- /** @var IShareable | \PHPUnit\Framework\MockObject\MockObject */
- private $book;
+ private Plugin $plugin;
+ private Server $server;
+ private IShareable&MockObject $book;
protected function setUp(): void {
parent::setUp();
- /** @var Auth | \PHPUnit\Framework\MockObject\MockObject $authBackend */
- $authBackend = $this->getMockBuilder(Auth::class)->disableOriginalConstructor()->getMock();
+ $authBackend = $this->createMock(Auth::class);
$authBackend->method('isDavAuthenticated')->willReturn(true);
- /** @var IRequest $request */
- $request = $this->getMockBuilder(IRequest::class)->disableOriginalConstructor()->getMock();
+ $request = $this->createMock(IRequest::class);
$config = $this->createMock(IConfig::class);
$this->plugin = new Plugin($authBackend, $request, $config);
$root = new SimpleCollection('root');
$this->server = new \Sabre\DAV\Server($root);
/** @var SimpleCollection $node */
- $this->book = $this->getMockBuilder(IShareable::class)->
- disableOriginalConstructor()->
- getMock();
+ $this->book = $this->createMock(IShareable::class);
$this->book->method('getName')->willReturn('addressbook1.vcf');
$root->addChild($this->book);
$this->plugin->initialize($this->server);
}
- public function testSharing() {
+ public function testSharing(): void {
$this->book->expects($this->once())->method('updateShares')->with([[
'href' => 'principal:principals/admin',
'commonName' => null,
diff --git a/apps/dav/tests/unit/DAV/SystemPrincipalBackendTest.php b/apps/dav/tests/unit/DAV/SystemPrincipalBackendTest.php
index 05e6699cc4c..3df861accf2 100644
--- a/apps/dav/tests/unit/DAV/SystemPrincipalBackendTest.php
+++ b/apps/dav/tests/unit/DAV/SystemPrincipalBackendTest.php
@@ -1,47 +1,27 @@
<?php
+
+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 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\Tests\unit\DAV;
use OCA\DAV\DAV\SystemPrincipalBackend;
+use Sabre\DAV\Exception;
use Test\TestCase;
class SystemPrincipalBackendTest extends TestCase {
- /**
- * @dataProvider providesPrefix
- * @param $expected
- * @param $prefix
- */
- public function testGetPrincipalsByPrefix($expected, $prefix) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesPrefix')]
+ public function testGetPrincipalsByPrefix(array $expected, string $prefix): void {
$backend = new SystemPrincipalBackend();
$result = $backend->getPrincipalsByPrefix($prefix);
$this->assertEquals($expected, $result);
}
- public function providesPrefix() {
+ public static function providesPrefix(): array {
return [
[[], ''],
[[[
@@ -56,18 +36,14 @@ class SystemPrincipalBackendTest extends TestCase {
];
}
- /**
- * @dataProvider providesPath
- * @param $expected
- * @param $path
- */
- public function testGetPrincipalByPath($expected, $path) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesPath')]
+ public function testGetPrincipalByPath(?array $expected, string $path): void {
$backend = new SystemPrincipalBackend();
$result = $backend->getPrincipalByPath($path);
$this->assertEquals($expected, $result);
}
- public function providesPath() {
+ public static function providesPath(): array {
return [
[null, ''],
[null, 'principals'],
@@ -79,60 +55,44 @@ class SystemPrincipalBackendTest extends TestCase {
];
}
- /**
- * @dataProvider providesPrincipalForGetGroupMemberSet
- *
- * @param string $principal
- * @throws \Sabre\DAV\Exception
- */
- public function testGetGroupMemberSetExceptional($principal) {
- $this->expectException(\Sabre\DAV\Exception::class);
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesPrincipalForGetGroupMemberSet')]
+ public function testGetGroupMemberSetExceptional(?string $principal): void {
+ $this->expectException(Exception::class);
$this->expectExceptionMessage('Principal not found');
$backend = new SystemPrincipalBackend();
$backend->getGroupMemberSet($principal);
}
- public function providesPrincipalForGetGroupMemberSet() {
+ public static function providesPrincipalForGetGroupMemberSet(): array {
return [
[null],
['principals/system'],
];
}
- /**
- * @throws \Sabre\DAV\Exception
- */
- public function testGetGroupMemberSet() {
+ public function testGetGroupMemberSet(): void {
$backend = new SystemPrincipalBackend();
$result = $backend->getGroupMemberSet('principals/system/system');
$this->assertEquals(['principals/system/system'], $result);
}
- /**
- * @dataProvider providesPrincipalForGetGroupMembership
- *
- * @param string $principal
- * @throws \Sabre\DAV\Exception
- */
- public function testGetGroupMembershipExceptional($principal) {
- $this->expectException(\Sabre\DAV\Exception::class);
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesPrincipalForGetGroupMembership')]
+ public function testGetGroupMembershipExceptional(string $principal): void {
+ $this->expectException(Exception::class);
$this->expectExceptionMessage('Principal not found');
$backend = new SystemPrincipalBackend();
$backend->getGroupMembership($principal);
}
- public function providesPrincipalForGetGroupMembership() {
+ public static function providesPrincipalForGetGroupMembership(): array {
return [
['principals/system/a'],
];
}
- /**
- * @throws \Sabre\DAV\Exception
- */
- public function testGetGroupMembership() {
+ public function testGetGroupMembership(): void {
$backend = new SystemPrincipalBackend();
$result = $backend->getGroupMembership('principals/system/system');
$this->assertEquals([], $result);
diff --git a/apps/dav/tests/unit/DAV/ViewOnlyPluginTest.php b/apps/dav/tests/unit/DAV/ViewOnlyPluginTest.php
new file mode 100644
index 00000000000..eefbc53fd22
--- /dev/null
+++ b/apps/dav/tests/unit/DAV/ViewOnlyPluginTest.php
@@ -0,0 +1,167 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2019 ownCloud GmbH
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Tests\unit\DAV;
+
+use OCA\DAV\Connector\Sabre\Exception\Forbidden;
+use OCA\DAV\Connector\Sabre\File as DavFile;
+use OCA\DAV\DAV\ViewOnlyPlugin;
+use OCA\Files_Sharing\SharedStorage;
+use OCA\Files_Versions\Sabre\VersionFile;
+use OCA\Files_Versions\Versions\IVersion;
+use OCP\Files\File;
+use OCP\Files\Folder;
+use OCP\Files\Storage\ISharedStorage;
+use OCP\Files\Storage\IStorage;
+use OCP\IUser;
+use OCP\Share\IAttributes;
+use OCP\Share\IShare;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Server;
+use Sabre\DAV\Tree;
+use Sabre\HTTP\RequestInterface;
+use Test\TestCase;
+
+class ViewOnlyPluginTest extends TestCase {
+ private Tree&MockObject $tree;
+ private RequestInterface&MockObject $request;
+ private Folder&MockObject $userFolder;
+ private ViewOnlyPlugin $plugin;
+
+ public function setUp(): void {
+ parent::setUp();
+
+ $this->userFolder = $this->createMock(Folder::class);
+ $this->request = $this->createMock(RequestInterface::class);
+ $this->tree = $this->createMock(Tree::class);
+ $server = $this->createMock(Server::class);
+
+ $this->plugin = new ViewOnlyPlugin(
+ $this->userFolder,
+ );
+ $server->tree = $this->tree;
+
+ $this->plugin->initialize($server);
+ }
+
+ public function testCanGetNonDav(): void {
+ $this->request->expects($this->once())->method('getPath')->willReturn('files/test/target');
+ $this->tree->method('getNodeForPath')->willReturn(null);
+
+ $this->assertTrue($this->plugin->checkViewOnly($this->request));
+ }
+
+ public function testCanGetNonShared(): void {
+ $this->request->expects($this->once())->method('getPath')->willReturn('files/test/target');
+ $davNode = $this->createMock(DavFile::class);
+ $this->tree->method('getNodeForPath')->willReturn($davNode);
+
+ $file = $this->createMock(File::class);
+ $davNode->method('getNode')->willReturn($file);
+
+ $storage = $this->createMock(IStorage::class);
+ $file->method('getStorage')->willReturn($storage);
+ $storage->method('instanceOfStorage')->with(ISharedStorage::class)->willReturn(false);
+
+ $this->assertTrue($this->plugin->checkViewOnly($this->request));
+ }
+
+ public static function providesDataForCanGet(): array {
+ return [
+ // has attribute permissions-download enabled - can get file
+ [false, true, true, true],
+ // has no attribute permissions-download - can get file
+ [false, null, true, true],
+ // has attribute permissions-download enabled - can get file version
+ [true, true, true, true],
+ // has no attribute permissions-download - can get file version
+ [true, null, true, true],
+ // has attribute permissions-download disabled - cannot get the file
+ [false, false, false, false],
+ // has attribute permissions-download disabled - cannot get the file version
+ [true, false, false, false],
+
+ // Has global allowViewWithoutDownload option enabled
+ // has attribute permissions-download disabled - can get file
+ [false, false, false, true],
+ // has attribute permissions-download disabled - can get file version
+ [true, false, false, true],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesDataForCanGet')]
+ public function testCanGet(bool $isVersion, ?bool $attrEnabled, bool $expectCanDownloadFile, bool $allowViewWithoutDownload): void {
+ $nodeInfo = $this->createMock(File::class);
+ if ($isVersion) {
+ $davPath = 'versions/alice/versions/117/123456';
+ $version = $this->createMock(IVersion::class);
+ $version->expects($this->once())
+ ->method('getSourceFile')
+ ->willReturn($nodeInfo);
+ $davNode = $this->createMock(VersionFile::class);
+ $davNode->expects($this->once())
+ ->method('getVersion')
+ ->willReturn($version);
+
+ $currentUser = $this->createMock(IUser::class);
+ $currentUser->expects($this->once())
+ ->method('getUID')
+ ->willReturn('alice');
+ $nodeInfo->expects($this->once())
+ ->method('getOwner')
+ ->willReturn($currentUser);
+
+ $nodeInfo = $this->createMock(File::class);
+ $owner = $this->createMock(IUser::class);
+ $owner->expects($this->once())
+ ->method('getUID')
+ ->willReturn('bob');
+ $this->userFolder->expects($this->once())
+ ->method('getById')
+ ->willReturn([$nodeInfo]);
+ $this->userFolder->expects($this->once())
+ ->method('getOwner')
+ ->willReturn($owner);
+ } else {
+ $davPath = 'files/path/to/file.odt';
+ $davNode = $this->createMock(DavFile::class);
+ $davNode->method('getNode')->willReturn($nodeInfo);
+ }
+
+ $this->request->expects($this->once())->method('getPath')->willReturn($davPath);
+
+ $this->tree->expects($this->once())
+ ->method('getNodeForPath')
+ ->with($davPath)
+ ->willReturn($davNode);
+
+ $storage = $this->createMock(SharedStorage::class);
+ $share = $this->createMock(IShare::class);
+ $nodeInfo->expects($this->once())
+ ->method('getStorage')
+ ->willReturn($storage);
+ $storage->method('instanceOfStorage')->with(ISharedStorage::class)->willReturn(true);
+ $storage->method('getShare')->willReturn($share);
+
+ $extAttr = $this->createMock(IAttributes::class);
+ $share->method('getAttributes')->willReturn($extAttr);
+ $extAttr->expects($this->once())
+ ->method('getAttribute')
+ ->with('permissions', 'download')
+ ->willReturn($attrEnabled);
+
+ $share->expects($this->once())
+ ->method('canSeeContent')
+ ->willReturn($allowViewWithoutDownload);
+
+ if (!$expectCanDownloadFile) {
+ $this->expectException(Forbidden::class);
+ }
+ $this->plugin->checkViewOnly($this->request);
+ }
+}
diff --git a/apps/dav/tests/unit/Direct/DirectFileTest.php b/apps/dav/tests/unit/Direct/DirectFileTest.php
index 5899b23d0b9..f6f0f49fa8c 100644
--- a/apps/dav/tests/unit/Direct/DirectFileTest.php
+++ b/apps/dav/tests/unit/Direct/DirectFileTest.php
@@ -3,29 +3,10 @@
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\Tests\Unit\Direct;
+namespace OCA\DAV\Tests\unit\Direct;
use OCA\DAV\Db\Direct;
use OCA\DAV\Direct\DirectFile;
@@ -33,28 +14,17 @@ use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\Exception\Forbidden;
use Test\TestCase;
class DirectFileTest extends TestCase {
-
- /** @var Direct */
- private $direct;
-
- /** @var IRootFolder|\PHPUnit\Framework\MockObject\MockObject */
- private $rootFolder;
-
- /** @var Folder|\PHPUnit\Framework\MockObject\MockObject */
- private $userFolder;
-
- /** @var File|\PHPUnit\Framework\MockObject\MockObject */
- private $file;
-
- /** @var DirectFile */
- private $directFile;
-
- /** @var IEventDispatcher */
- private $eventDispatcher;
+ private Direct $direct;
+ private IRootFolder&MockObject $rootFolder;
+ private Folder&MockObject $userFolder;
+ private File&MockObject $file;
+ private IEventDispatcher&MockObject $eventDispatcher;
+ private DirectFile $directFile;
protected function setUp(): void {
parent::setUp();
@@ -73,66 +43,66 @@ class DirectFileTest extends TestCase {
->willReturn($this->userFolder);
$this->file = $this->createMock(File::class);
- $this->userFolder->method('getById')
+ $this->userFolder->method('getFirstNodeById')
->with(42)
- ->willReturn([$this->file]);
+ ->willReturn($this->file);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->directFile = new DirectFile($this->direct, $this->rootFolder, $this->eventDispatcher);
}
- public function testPut() {
+ public function testPut(): void {
$this->expectException(Forbidden::class);
$this->directFile->put('foo');
}
- public function testGet() {
+ public function testGet(): void {
$this->file->expects($this->once())
->method('fopen')
->with('rb');
$this->directFile->get();
}
- public function testGetContentType() {
+ public function testGetContentType(): void {
$this->file->method('getMimeType')
->willReturn('direct/type');
$this->assertSame('direct/type', $this->directFile->getContentType());
}
- public function testGetETag() {
+ public function testGetETag(): void {
$this->file->method('getEtag')
->willReturn('directEtag');
$this->assertSame('directEtag', $this->directFile->getETag());
}
- public function testGetSize() {
+ public function testGetSize(): void {
$this->file->method('getSize')
->willReturn(42);
$this->assertSame(42, $this->directFile->getSize());
}
- public function testDelete() {
+ public function testDelete(): void {
$this->expectException(Forbidden::class);
$this->directFile->delete();
}
- public function testGetName() {
+ public function testGetName(): void {
$this->assertSame('directToken', $this->directFile->getName());
}
- public function testSetName() {
+ public function testSetName(): void {
$this->expectException(Forbidden::class);
$this->directFile->setName('foobar');
}
- public function testGetLastModified() {
+ public function testGetLastModified(): void {
$this->file->method('getMTime')
->willReturn(42);
diff --git a/apps/dav/tests/unit/Direct/DirectHomeTest.php b/apps/dav/tests/unit/Direct/DirectHomeTest.php
index 073aa28f121..94c82c2b7c5 100644
--- a/apps/dav/tests/unit/Direct/DirectHomeTest.php
+++ b/apps/dav/tests/unit/Direct/DirectHomeTest.php
@@ -3,31 +3,11 @@
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\Tests\Unit\Direct;
+namespace OCA\DAV\Tests\unit\Direct;
-use OC\Security\Bruteforce\Throttler;
use OCA\DAV\Db\Direct;
use OCA\DAV\Db\DirectMapper;
use OCA\DAV\Direct\DirectFile;
@@ -37,33 +17,21 @@ use OCP\AppFramework\Utility\ITimeFactory;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\IRootFolder;
use OCP\IRequest;
+use OCP\Security\Bruteforce\IThrottler;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\MethodNotAllowed;
use Sabre\DAV\Exception\NotFound;
use Test\TestCase;
class DirectHomeTest extends TestCase {
-
- /** @var DirectMapper|\PHPUnit\Framework\MockObject\MockObject */
- private $directMapper;
-
- /** @var IRootFolder|\PHPUnit\Framework\MockObject\MockObject */
- private $rootFolder;
-
- /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */
- private $timeFactory;
-
- /** @var Throttler|\PHPUnit\Framework\MockObject\MockObject */
- private $throttler;
-
- /** @var IRequest */
- private $request;
-
- /** @var DirectHome */
- private $directHome;
-
- /** @var IEventDispatcher */
- private $eventDispatcher;
+ private DirectMapper&MockObject $directMapper;
+ private IRootFolder&MockObject $rootFolder;
+ private ITimeFactory&MockObject $timeFactory;
+ private IThrottler&MockObject $throttler;
+ private IRequest&MockObject $request;
+ private IEventDispatcher&MockObject $eventDispatcher;
+ private DirectHome $directHome;
protected function setUp(): void {
parent::setUp();
@@ -71,7 +39,7 @@ class DirectHomeTest extends TestCase {
$this->directMapper = $this->createMock(DirectMapper::class);
$this->rootFolder = $this->createMock(IRootFolder::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
- $this->throttler = $this->createMock(Throttler::class);
+ $this->throttler = $this->createMock(IThrottler::class);
$this->request = $this->createMock(IRequest::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
@@ -92,49 +60,49 @@ class DirectHomeTest extends TestCase {
);
}
- public function testCreateFile() {
+ public function testCreateFile(): void {
$this->expectException(Forbidden::class);
$this->directHome->createFile('foo', 'bar');
}
- public function testCreateDirectory() {
+ public function testCreateDirectory(): void {
$this->expectException(Forbidden::class);
$this->directHome->createDirectory('foo');
}
- public function testGetChildren() {
+ public function testGetChildren(): void {
$this->expectException(MethodNotAllowed::class);
$this->directHome->getChildren();
}
- public function testChildExists() {
+ public function testChildExists(): void {
$this->assertFalse($this->directHome->childExists('foo'));
}
- public function testDelete() {
+ public function testDelete(): void {
$this->expectException(Forbidden::class);
$this->directHome->delete();
}
- public function testGetName() {
+ public function testGetName(): void {
$this->assertSame('direct', $this->directHome->getName());
}
- public function testSetName() {
+ public function testSetName(): void {
$this->expectException(Forbidden::class);
$this->directHome->setName('foo');
}
- public function testGetLastModified() {
+ public function testGetLastModified(): void {
$this->assertSame(0, $this->directHome->getLastModified());
}
- public function testGetChildValid() {
+ public function testGetChildValid(): void {
$direct = Direct::fromParams([
'expiration' => 100,
]);
@@ -150,7 +118,7 @@ class DirectHomeTest extends TestCase {
$this->assertInstanceOf(DirectFile::class, $result);
}
- public function testGetChildExpired() {
+ public function testGetChildExpired(): void {
$direct = Direct::fromParams([
'expiration' => 41,
]);
@@ -167,7 +135,7 @@ class DirectHomeTest extends TestCase {
$this->directHome->getChild('longtoken');
}
- public function testGetChildInvalid() {
+ public function testGetChildInvalid(): void {
$this->directMapper->method('getByToken')
->with('longtoken')
->willThrowException(new DoesNotExistException('not found'));
@@ -179,7 +147,7 @@ class DirectHomeTest extends TestCase {
'1.2.3.4'
);
$this->throttler->expects($this->once())
- ->method('sleepDelay')
+ ->method('sleepDelayOrThrowOnMax')
->with(
'1.2.3.4',
'directlink'
diff --git a/apps/dav/tests/unit/Files/FileSearchBackendTest.php b/apps/dav/tests/unit/Files/FileSearchBackendTest.php
index dec5db1b1b0..c6d6f85347b 100644
--- a/apps/dav/tests/unit/Files/FileSearchBackendTest.php
+++ b/apps/dav/tests/unit/Files/FileSearchBackendTest.php
@@ -1,38 +1,21 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @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: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-namespace OCA\DAV\Tests\Files;
+
+namespace OCA\DAV\Tests\unit\Files;
use OC\Files\Search\SearchComparison;
use OC\Files\Search\SearchQuery;
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\ObjectTree;
+use OCA\DAV\Connector\Sabre\Server;
use OCA\DAV\Files\FileSearchBackend;
use OCP\Files\FileInfo;
use OCP\Files\Folder;
@@ -40,37 +23,28 @@ use OCP\Files\IRootFolder;
use OCP\Files\Search\ISearchBinaryOperator;
use OCP\Files\Search\ISearchComparison;
use OCP\Files\Search\ISearchQuery;
+use OCP\FilesMetadata\IFilesMetadataManager;
use OCP\IUser;
use OCP\Share\IManager;
+use PHPUnit\Framework\MockObject\MockObject;
use SearchDAV\Backend\SearchPropertyDefinition;
use SearchDAV\Query\Limit;
+use SearchDAV\Query\Literal;
+use SearchDAV\Query\Operator;
use SearchDAV\Query\Query;
+use SearchDAV\Query\Scope;
use Test\TestCase;
class FileSearchBackendTest extends TestCase {
- /** @var CachingTree|\PHPUnit\Framework\MockObject\MockObject */
- private $tree;
-
- /** @var IUser */
- private $user;
-
- /** @var IRootFolder|\PHPUnit\Framework\MockObject\MockObject */
- private $rootFolder;
-
- /** @var IManager|\PHPUnit\Framework\MockObject\MockObject */
- private $shareManager;
-
- /** @var View|\PHPUnit\Framework\MockObject\MockObject */
- private $view;
-
- /** @var Folder|\PHPUnit\Framework\MockObject\MockObject */
- private $searchFolder;
-
- /** @var FileSearchBackend */
- private $search;
-
- /** @var Directory|\PHPUnit\Framework\MockObject\MockObject */
- private $davFolder;
+ private ObjectTree&MockObject $tree;
+ private Server&MockObject $server;
+ private IUser&MockObject $user;
+ private IRootFolder&MockObject $rootFolder;
+ private IManager&MockObject $shareManager;
+ private View&MockObject $view;
+ private Folder&MockObject $searchFolder;
+ private Directory&MockObject $davFolder;
+ private FileSearchBackend $search;
protected function setUp(): void {
parent::setUp();
@@ -80,28 +54,23 @@ class FileSearchBackendTest extends TestCase {
->method('getUID')
->willReturn('test');
- $this->tree = $this->getMockBuilder(CachingTree::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->view = $this->getMockBuilder(View::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->view->expects($this->any())
- ->method('getRelativePath')
- ->willReturnArgument(0);
-
+ $this->tree = $this->createMock(ObjectTree::class);
+ $this->server = $this->createMock(Server::class);
+ $this->view = $this->createMock(View::class);
$this->rootFolder = $this->createMock(IRootFolder::class);
-
$this->shareManager = $this->createMock(IManager::class);
-
$this->searchFolder = $this->createMock(Folder::class);
-
$fileInfo = $this->createMock(FileInfo::class);
-
$this->davFolder = $this->createMock(Directory::class);
+ $this->view->expects($this->any())
+ ->method('getRoot')
+ ->willReturn('');
+
+ $this->view->expects($this->any())
+ ->method('getRelativePath')
+ ->willReturnArgument(0);
+
$this->davFolder->expects($this->any())
->method('getFileInfo')
->willReturn($fileInfo);
@@ -110,10 +79,12 @@ class FileSearchBackendTest extends TestCase {
->method('get')
->willReturn($this->searchFolder);
- $this->search = new FileSearchBackend($this->tree, $this->user, $this->rootFolder, $this->shareManager, $this->view);
+ $filesMetadataManager = $this->createMock(IFilesMetadataManager::class);
+
+ $this->search = new FileSearchBackend($this->server, $this->tree, $this->user, $this->rootFolder, $this->shareManager, $this->view, $filesMetadataManager);
}
- public function testSearchFilename() {
+ public function testSearchFilename(): void {
$this->tree->expects($this->any())
->method('getNodeForPath')
->willReturn($this->davFolder);
@@ -132,17 +103,17 @@ class FileSearchBackendTest extends TestCase {
$this->user
))
->willReturn([
- new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path')
+ new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
]);
- $query = $this->getBasicQuery(\SearchDAV\Query\Operator::OPERATION_EQUAL, '{DAV:}displayname', 'foo');
+ $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}displayname', 'foo');
$result = $this->search->search($query);
$this->assertCount(1, $result);
$this->assertEquals('/files/test/test/path', $result[0]->href);
}
- public function testSearchMimetype() {
+ public function testSearchMimetype(): void {
$this->tree->expects($this->any())
->method('getNodeForPath')
->willReturn($this->davFolder);
@@ -161,17 +132,17 @@ class FileSearchBackendTest extends TestCase {
$this->user
))
->willReturn([
- new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path')
+ new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
]);
- $query = $this->getBasicQuery(\SearchDAV\Query\Operator::OPERATION_EQUAL, '{DAV:}getcontenttype', 'foo');
+ $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}getcontenttype', 'foo');
$result = $this->search->search($query);
$this->assertCount(1, $result);
$this->assertEquals('/files/test/test/path', $result[0]->href);
}
- public function testSearchSize() {
+ public function testSearchSize(): void {
$this->tree->expects($this->any())
->method('getNodeForPath')
->willReturn($this->davFolder);
@@ -190,17 +161,17 @@ class FileSearchBackendTest extends TestCase {
$this->user
))
->willReturn([
- new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path')
+ new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
]);
- $query = $this->getBasicQuery(\SearchDAV\Query\Operator::OPERATION_GREATER_THAN, FilesPlugin::SIZE_PROPERTYNAME, 10);
+ $query = $this->getBasicQuery(Operator::OPERATION_GREATER_THAN, FilesPlugin::SIZE_PROPERTYNAME, 10);
$result = $this->search->search($query);
$this->assertCount(1, $result);
$this->assertEquals('/files/test/test/path', $result[0]->href);
}
- public function testSearchMtime() {
+ public function testSearchMtime(): void {
$this->tree->expects($this->any())
->method('getNodeForPath')
->willReturn($this->davFolder);
@@ -219,17 +190,17 @@ class FileSearchBackendTest extends TestCase {
$this->user
))
->willReturn([
- new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path')
+ new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
]);
- $query = $this->getBasicQuery(\SearchDAV\Query\Operator::OPERATION_GREATER_THAN, '{DAV:}getlastmodified', 10);
+ $query = $this->getBasicQuery(Operator::OPERATION_GREATER_THAN, '{DAV:}getlastmodified', 10);
$result = $this->search->search($query);
$this->assertCount(1, $result);
$this->assertEquals('/files/test/test/path', $result[0]->href);
}
- public function testSearchIsCollection() {
+ public function testSearchIsCollection(): void {
$this->tree->expects($this->any())
->method('getNodeForPath')
->willReturn($this->davFolder);
@@ -248,10 +219,10 @@ class FileSearchBackendTest extends TestCase {
$this->user
))
->willReturn([
- new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path')
+ new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
]);
- $query = $this->getBasicQuery(\SearchDAV\Query\Operator::OPERATION_IS_COLLECTION, 'yes');
+ $query = $this->getBasicQuery(Operator::OPERATION_IS_COLLECTION, 'yes');
$result = $this->search->search($query);
$this->assertCount(1, $result);
@@ -259,7 +230,7 @@ class FileSearchBackendTest extends TestCase {
}
- public function testSearchInvalidProp() {
+ public function testSearchInvalidProp(): void {
$this->expectException(\InvalidArgumentException::class);
$this->tree->expects($this->any())
@@ -269,25 +240,25 @@ class FileSearchBackendTest extends TestCase {
$this->searchFolder->expects($this->never())
->method('search');
- $query = $this->getBasicQuery(\SearchDAV\Query\Operator::OPERATION_EQUAL, '{DAV:}getetag', 'foo');
+ $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}getetag', 'foo');
$this->search->search($query);
}
- private function getBasicQuery($type, $property, $value = null) {
- $scope = new \SearchDAV\Query\Scope('/', 'infinite');
+ private function getBasicQuery(string $type, string $property, int|string|null $value = null) {
+ $scope = new Scope('/', 'infinite');
$scope->path = '/';
$from = [$scope];
$orderBy = [];
$select = [];
if (is_null($value)) {
- $where = new \SearchDAV\Query\Operator(
+ $where = new Operator(
$type,
- [new \SearchDAV\Query\Literal($property)]
+ [new Literal($property)]
);
} else {
- $where = new \SearchDAV\Query\Operator(
+ $where = new Operator(
$type,
- [new SearchPropertyDefinition($property, true, true, true), new \SearchDAV\Query\Literal($value)]
+ [new SearchPropertyDefinition($property, true, true, true), new Literal($value)]
);
}
$limit = new Limit();
@@ -296,7 +267,7 @@ class FileSearchBackendTest extends TestCase {
}
- public function testSearchNonFolder() {
+ public function testSearchNonFolder(): void {
$this->expectException(\InvalidArgumentException::class);
$davNode = $this->createMock(File::class);
@@ -305,11 +276,11 @@ class FileSearchBackendTest extends TestCase {
->method('getNodeForPath')
->willReturn($davNode);
- $query = $this->getBasicQuery(\SearchDAV\Query\Operator::OPERATION_EQUAL, '{DAV:}displayname', 'foo');
+ $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}displayname', 'foo');
$this->search->search($query);
}
- public function testSearchLimitOwnerBasic() {
+ public function testSearchLimitOwnerBasic(): void {
$this->tree->expects($this->any())
->method('getNodeForPath')
->willReturn($this->davFolder);
@@ -321,11 +292,11 @@ class FileSearchBackendTest extends TestCase {
->willReturnCallback(function ($query) use (&$receivedQuery) {
$receivedQuery = $query;
return [
- new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path')
+ new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
];
});
- $query = $this->getBasicQuery(\SearchDAV\Query\Operator::OPERATION_EQUAL, FilesPlugin::OWNER_ID_PROPERTYNAME, $this->user->getUID());
+ $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, FilesPlugin::OWNER_ID_PROPERTYNAME, $this->user->getUID());
$this->search->search($query);
$this->assertNotNull($receivedQuery);
@@ -338,7 +309,7 @@ class FileSearchBackendTest extends TestCase {
$this->assertEmpty($operator->getArguments());
}
- public function testSearchLimitOwnerNested() {
+ public function testSearchLimitOwnerNested(): void {
$this->tree->expects($this->any())
->method('getNodeForPath')
->willReturn($this->davFolder);
@@ -350,22 +321,22 @@ class FileSearchBackendTest extends TestCase {
->willReturnCallback(function ($query) use (&$receivedQuery) {
$receivedQuery = $query;
return [
- new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path')
+ new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
];
});
- $query = $this->getBasicQuery(\SearchDAV\Query\Operator::OPERATION_EQUAL, FilesPlugin::OWNER_ID_PROPERTYNAME, $this->user->getUID());
- $query->where = new \SearchDAV\Query\Operator(
- \SearchDAV\Query\Operator::OPERATION_AND,
+ $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, FilesPlugin::OWNER_ID_PROPERTYNAME, $this->user->getUID());
+ $query->where = new Operator(
+ Operator::OPERATION_AND,
[
- new \SearchDAV\Query\Operator(
- \SearchDAV\Query\Operator::OPERATION_EQUAL,
- [new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true), new \SearchDAV\Query\Literal('image/png')]
+ new Operator(
+ Operator::OPERATION_EQUAL,
+ [new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true), new Literal('image/png')]
+ ),
+ new Operator(
+ Operator::OPERATION_EQUAL,
+ [new SearchPropertyDefinition(FilesPlugin::OWNER_ID_PROPERTYNAME, true, true, true), new Literal($this->user->getUID())]
),
- new \SearchDAV\Query\Operator(
- \SearchDAV\Query\Operator::OPERATION_EQUAL,
- [new SearchPropertyDefinition(FilesPlugin::OWNER_ID_PROPERTYNAME, true, true, true), new \SearchDAV\Query\Literal($this->user->getUID())]
- )
]
);
$this->search->search($query);
@@ -385,4 +356,66 @@ class FileSearchBackendTest extends TestCase {
$this->assertEquals(ISearchBinaryOperator::OPERATOR_AND, $operator->getType());
$this->assertEmpty($operator->getArguments());
}
+
+ public function testSearchOperatorLimit(): void {
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->willReturn($this->davFolder);
+
+ $innerOperator = new Operator(
+ Operator::OPERATION_EQUAL,
+ [new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true), new Literal('image/png')]
+ );
+ // 5 child operators
+ $level1Operator = new Operator(
+ Operator::OPERATION_AND,
+ [
+ $innerOperator,
+ $innerOperator,
+ $innerOperator,
+ $innerOperator,
+ $innerOperator,
+ ]
+ );
+ // 5^2 = 25 child operators
+ $level2Operator = new Operator(
+ Operator::OPERATION_AND,
+ [
+ $level1Operator,
+ $level1Operator,
+ $level1Operator,
+ $level1Operator,
+ $level1Operator,
+ ]
+ );
+ // 5^3 = 125 child operators
+ $level3Operator = new Operator(
+ Operator::OPERATION_AND,
+ [
+ $level2Operator,
+ $level2Operator,
+ $level2Operator,
+ $level2Operator,
+ $level2Operator,
+ ]
+ );
+
+ $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, FilesPlugin::OWNER_ID_PROPERTYNAME, $this->user->getUID());
+ $query->where = $level3Operator;
+ $this->expectException(\InvalidArgumentException::class);
+ $this->search->search($query);
+ }
+
+ public function testPreloadPropertyFor(): void {
+ $node1 = $this->createMock(File::class);
+ $node2 = $this->createMock(Directory::class);
+ $nodes = [$node1, $node2];
+ $requestProperties = ['{DAV:}getcontenttype', '{DAV:}getlastmodified'];
+
+ $this->server->expects($this->once())
+ ->method('emit')
+ ->with('preloadProperties', [$nodes, $requestProperties]);
+
+ $this->search->preloadPropertyFor($nodes, $requestProperties);
+ }
}
diff --git a/apps/dav/tests/unit/Files/MultipartRequestParserTest.php b/apps/dav/tests/unit/Files/MultipartRequestParserTest.php
index ec9e2d0a383..dc0e884f07c 100644
--- a/apps/dav/tests/unit/Files/MultipartRequestParserTest.php
+++ b/apps/dav/tests/unit/Files/MultipartRequestParserTest.php
@@ -1,71 +1,70 @@
<?php
+
+declare(strict_types=1);
/**
- * @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\Tests\unit\DAV;
+namespace OCA\DAV\Tests\unit\Files;
+use OCA\DAV\BulkUpload\MultipartRequestParser;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\HTTP\RequestInterface;
use Test\TestCase;
-use \OCA\DAV\BulkUpload\MultipartRequestParser;
class MultipartRequestParserTest extends TestCase {
- private function getValidBodyObject() {
+
+ protected LoggerInterface&MockObject $logger;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->logger = $this->createMock(LoggerInterface::class);
+ }
+
+ private static function getValidBodyObject(): array {
return [
[
- "headers" => [
- "Content-Length" => 7,
- "X-File-MD5" => "4f2377b4d911f7ec46325fe603c3af03",
- "X-File-Path" => "/coucou.txt"
+ 'headers' => [
+ 'Content-Length' => 7,
+ 'X-File-MD5' => '4f2377b4d911f7ec46325fe603c3af03',
+ 'OC-Checksum' => 'md5:4f2377b4d911f7ec46325fe603c3af03',
+ 'X-File-Path' => '/coucou.txt'
],
- "content" => "Coucou\n"
+ 'content' => "Coucou\n"
]
];
}
- private function getMultipartParser(array $parts, array $headers = [], string $boundary = "boundary_azertyuiop"): MultipartRequestParser {
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
+ private function getMultipartParser(array $parts, array $headers = [], string $boundary = 'boundary_azertyuiop'): MultipartRequestParser {
+ /** @var RequestInterface&MockObject $request */
+ $request = $this->getMockBuilder(RequestInterface::class)
->disableOriginalConstructor()
->getMock();
- $headers = array_merge(['Content-Type' => 'multipart/related; boundary='.$boundary], $headers);
+ $headers = array_merge(['Content-Type' => 'multipart/related; boundary=' . $boundary], $headers);
$request->expects($this->any())
->method('getHeader')
->willReturnCallback(function (string $key) use (&$headers) {
return $headers[$key];
});
- $body = "";
+ $body = '';
foreach ($parts as $part) {
- $body .= '--'.$boundary."\r\n";
+ $body .= '--' . $boundary . "\r\n";
foreach ($part['headers'] as $headerKey => $headerPart) {
- $body .= $headerKey.": ".$headerPart."\r\n";
+ $body .= $headerKey . ': ' . $headerPart . "\r\n";
}
$body .= "\r\n";
- $body .= $part['content']."\r\n";
+ $body .= $part['content'] . "\r\n";
}
- $body .= '--'.$boundary."--";
+ $body .= '--' . $boundary . '--';
- $stream = fopen('php://temp','r+');
+ $stream = fopen('php://temp', 'r+');
fwrite($stream, $body);
rewind($stream);
@@ -73,16 +72,17 @@ class MultipartRequestParserTest extends TestCase {
->method('getBody')
->willReturn($stream);
- return new MultipartRequestParser($request);
+ return new MultipartRequestParser($request, $this->logger);
}
/**
* Test validation of the request's body type
*/
- public function testBodyTypeValidation() {
- $bodyStream = "I am not a stream, but pretend to be";
- $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
+ public function testBodyTypeValidation(): void {
+ $bodyStream = 'I am not a stream, but pretend to be';
+ /** @var RequestInterface&MockObject $request */
+ $request = $this->getMockBuilder(RequestInterface::class)
->disableOriginalConstructor()
->getMock();
$request->expects($this->any())
@@ -90,7 +90,30 @@ class MultipartRequestParserTest extends TestCase {
->willReturn($bodyStream);
$this->expectExceptionMessage('Body should be of type resource');
- new MultipartRequestParser($request);
+ new MultipartRequestParser($request, $this->logger);
+ }
+
+ /**
+ * Test with valid request.
+ * - valid boundary
+ * - valid hash
+ * - valid content-length
+ * - valid file content
+ * - valid file path
+ */
+ public function testValidRequest(): void {
+ $bodyObject = self::getValidBodyObject();
+ unset($bodyObject['0']['headers']['X-File-MD5']);
+
+ $multipartParser = $this->getMultipartParser($bodyObject);
+
+ [$headers, $content] = $multipartParser->parseNextPart();
+
+ $this->assertSame((int)$headers['content-length'], 7, 'Content-Length header should be the same as provided.');
+ $this->assertSame($headers['oc-checksum'], 'md5:4f2377b4d911f7ec46325fe603c3af03', 'OC-Checksum header should be the same as provided.');
+ $this->assertSame($headers['x-file-path'], '/coucou.txt', 'X-File-Path header should be the same as provided.');
+
+ $this->assertSame($content, "Coucou\n", 'Content should be the same');
}
/**
@@ -101,54 +124,72 @@ class MultipartRequestParserTest extends TestCase {
* - valid file content
* - valid file path
*/
- public function testValidRequest() {
- $multipartParser = $this->getMultipartParser(
- $this->getValidBodyObject()
- );
+ public function testValidRequestWithMd5(): void {
+ $bodyObject = self::getValidBodyObject();
+ unset($bodyObject['0']['headers']['OC-Checksum']);
+
+ $multipartParser = $this->getMultipartParser($bodyObject);
[$headers, $content] = $multipartParser->parseNextPart();
- $this->assertSame((int)$headers["content-length"], 7, "Content-Length header should be the same as provided.");
- $this->assertSame($headers["x-file-md5"], "4f2377b4d911f7ec46325fe603c3af03", "X-File-MD5 header should be the same as provided.");
- $this->assertSame($headers["x-file-path"], "/coucou.txt", "X-File-Path header should be the same as provided.");
+ $this->assertSame((int)$headers['content-length'], 7, 'Content-Length header should be the same as provided.');
+ $this->assertSame($headers['x-file-md5'], '4f2377b4d911f7ec46325fe603c3af03', 'X-File-MD5 header should be the same as provided.');
+ $this->assertSame($headers['x-file-path'], '/coucou.txt', 'X-File-Path header should be the same as provided.');
+
+ $this->assertSame($content, "Coucou\n", 'Content should be the same');
+ }
- $this->assertSame($content, "Coucou\n", "Content should be the same");
+ /**
+ * Test with invalid hash.
+ */
+ public function testInvalidHash(): void {
+ $bodyObject = self::getValidBodyObject();
+ $bodyObject['0']['headers']['OC-Checksum'] = 'md5:f2377b4d911f7ec46325fe603c3af03';
+ unset($bodyObject['0']['headers']['X-File-MD5']);
+ $multipartParser = $this->getMultipartParser(
+ $bodyObject
+ );
+
+ $this->expectExceptionMessage('Computed md5 hash is incorrect (4f2377b4d911f7ec46325fe603c3af03).');
+ $multipartParser->parseNextPart();
}
/**
* Test with invalid md5 hash.
*/
- public function testInvalidMd5Hash() {
- $bodyObject = $this->getValidBodyObject();
- $bodyObject["0"]["headers"]["X-File-MD5"] = "f2377b4d911f7ec46325fe603c3af03";
+ public function testInvalidMd5Hash(): void {
+ $bodyObject = self::getValidBodyObject();
+ unset($bodyObject['0']['headers']['OC-Checksum']);
+ $bodyObject['0']['headers']['X-File-MD5'] = 'f2377b4d911f7ec46325fe603c3af03';
$multipartParser = $this->getMultipartParser(
$bodyObject
);
- $this->expectExceptionMessage('Computed md5 hash is incorrect.');
+ $this->expectExceptionMessage('Computed md5 hash is incorrect (4f2377b4d911f7ec46325fe603c3af03).');
$multipartParser->parseNextPart();
}
/**
- * Test with a null md5 hash.
+ * Test with a null hash headers.
*/
- public function testNullMd5Hash() {
- $bodyObject = $this->getValidBodyObject();
- unset($bodyObject["0"]["headers"]["X-File-MD5"]);
+ public function testNullHash(): void {
+ $bodyObject = self::getValidBodyObject();
+ unset($bodyObject['0']['headers']['OC-Checksum']);
+ unset($bodyObject['0']['headers']['X-File-MD5']);
$multipartParser = $this->getMultipartParser(
$bodyObject
);
- $this->expectExceptionMessage('The X-File-MD5 header must not be null.');
+ $this->expectExceptionMessage('The hash headers must not be null.');
$multipartParser->parseNextPart();
}
/**
* Test with a null Content-Length.
*/
- public function testNullContentLength() {
- $bodyObject = $this->getValidBodyObject();
- unset($bodyObject["0"]["headers"]["Content-Length"]);
+ public function testNullContentLength(): void {
+ $bodyObject = self::getValidBodyObject();
+ unset($bodyObject['0']['headers']['Content-Length']);
$multipartParser = $this->getMultipartParser(
$bodyObject
);
@@ -160,36 +201,36 @@ class MultipartRequestParserTest extends TestCase {
/**
* Test with a lower Content-Length.
*/
- public function testLowerContentLength() {
- $bodyObject = $this->getValidBodyObject();
- $bodyObject["0"]["headers"]["Content-Length"] = 6;
+ public function testLowerContentLength(): void {
+ $bodyObject = self::getValidBodyObject();
+ $bodyObject['0']['headers']['Content-Length'] = 6;
$multipartParser = $this->getMultipartParser(
$bodyObject
);
- $this->expectExceptionMessage('Computed md5 hash is incorrect.');
+ $this->expectExceptionMessage('Computed md5 hash is incorrect (41060d3ddfdf63e68fc2bf196f652ee9).');
$multipartParser->parseNextPart();
}
/**
* Test with a higher Content-Length.
*/
- public function testHigherContentLength() {
- $bodyObject = $this->getValidBodyObject();
- $bodyObject["0"]["headers"]["Content-Length"] = 8;
+ public function testHigherContentLength(): void {
+ $bodyObject = self::getValidBodyObject();
+ $bodyObject['0']['headers']['Content-Length'] = 8;
$multipartParser = $this->getMultipartParser(
$bodyObject
);
- $this->expectExceptionMessage('Computed md5 hash is incorrect.');
+ $this->expectExceptionMessage('Computed md5 hash is incorrect (0161002bbee6a744f18741b8a914e413).');
$multipartParser->parseNextPart();
}
/**
* Test with wrong boundary in body.
*/
- public function testWrongBoundary() {
- $bodyObject = $this->getValidBodyObject();
+ public function testWrongBoundary(): void {
+ $bodyObject = self::getValidBodyObject();
$multipartParser = $this->getMultipartParser(
$bodyObject,
['Content-Type' => 'multipart/related; boundary=boundary_poiuytreza']
@@ -202,8 +243,8 @@ class MultipartRequestParserTest extends TestCase {
/**
* Test with no boundary in request headers.
*/
- public function testNoBoundaryInHeader() {
- $bodyObject = $this->getValidBodyObject();
+ public function testNoBoundaryInHeader(): void {
+ $bodyObject = self::getValidBodyObject();
$this->expectExceptionMessage('Error while parsing boundary in Content-Type header.');
$this->getMultipartParser(
$bodyObject,
@@ -214,8 +255,8 @@ class MultipartRequestParserTest extends TestCase {
/**
* Test with no boundary in the request's headers.
*/
- public function testNoBoundaryInBody() {
- $bodyObject = $this->getValidBodyObject();
+ public function testNoBoundaryInBody(): void {
+ $bodyObject = self::getValidBodyObject();
$multipartParser = $this->getMultipartParser(
$bodyObject,
['Content-Type' => 'multipart/related; boundary=boundary_azertyuiop'],
@@ -229,8 +270,8 @@ class MultipartRequestParserTest extends TestCase {
/**
* Test with a boundary with quotes in the request's headers.
*/
- public function testBoundaryWithQuotes() {
- $bodyObject = $this->getValidBodyObject();
+ public function testBoundaryWithQuotes(): void {
+ $bodyObject = self::getValidBodyObject();
$multipartParser = $this->getMultipartParser(
$bodyObject,
['Content-Type' => 'multipart/related; boundary="boundary_azertyuiop"'],
@@ -245,8 +286,8 @@ class MultipartRequestParserTest extends TestCase {
/**
* Test with a wrong Content-Type in the request's headers.
*/
- public function testWrongContentType() {
- $bodyObject = $this->getValidBodyObject();
+ public function testWrongContentType(): void {
+ $bodyObject = self::getValidBodyObject();
$this->expectExceptionMessage('Content-Type must be multipart/related');
$this->getMultipartParser(
$bodyObject,
@@ -257,8 +298,8 @@ class MultipartRequestParserTest extends TestCase {
/**
* Test with a wrong key after the content type in the request's headers.
*/
- public function testWrongKeyInContentType() {
- $bodyObject = $this->getValidBodyObject();
+ public function testWrongKeyInContentType(): void {
+ $bodyObject = self::getValidBodyObject();
$this->expectExceptionMessage('Boundary is invalid');
$this->getMultipartParser(
$bodyObject,
@@ -269,8 +310,8 @@ class MultipartRequestParserTest extends TestCase {
/**
* Test with a null Content-Type in the request's headers.
*/
- public function testNullContentType() {
- $bodyObject = $this->getValidBodyObject();
+ public function testNullContentType(): void {
+ $bodyObject = self::getValidBodyObject();
$this->expectExceptionMessage('Content-Type can not be null');
$this->getMultipartParser(
$bodyObject,
diff --git a/apps/dav/tests/unit/Files/Sharing/FilesDropPluginTest.php b/apps/dav/tests/unit/Files/Sharing/FilesDropPluginTest.php
index a24125b6804..1a7ab7179e1 100644
--- a/apps/dav/tests/unit/Files/Sharing/FilesDropPluginTest.php
+++ b/apps/dav/tests/unit/Files/Sharing/FilesDropPluginTest.php
@@ -1,33 +1,19 @@
<?php
+
+declare(strict_types=1);
/**
- * @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 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\Tests\Files\Sharing;
+namespace OCA\DAV\Tests\unit\Files\Sharing;
-use OC\Files\View;
use OCA\DAV\Files\Sharing\FilesDropPlugin;
-use Sabre\DAV\Exception\MethodNotAllowed;
+use OCP\Files\Folder;
+use OCP\Files\NotFoundException;
+use OCP\Share\IAttributes;
+use OCP\Share\IShare;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Server;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
@@ -35,148 +21,238 @@ use Test\TestCase;
class FilesDropPluginTest extends TestCase {
- /** @var View|\PHPUnit\Framework\MockObject\MockObject */
- private $view;
-
- /** @var Server|\PHPUnit\Framework\MockObject\MockObject */
- private $server;
-
- /** @var FilesDropPlugin */
- private $plugin;
+ private FilesDropPlugin $plugin;
- /** @var RequestInterface|\PHPUnit\Framework\MockObject\MockObject */
- private $request;
-
- /** @var ResponseInterface|\PHPUnit\Framework\MockObject\MockObject */
- private $response;
+ private Folder&MockObject $node;
+ private IShare&MockObject $share;
+ private Server&MockObject $server;
+ private RequestInterface&MockObject $request;
+ private ResponseInterface&MockObject $response;
protected function setUp(): void {
parent::setUp();
- $this->view = $this->createMock(View::class);
+ $this->node = $this->createMock(Folder::class);
+ $this->node->method('getPath')
+ ->willReturn('/files/token');
+
+ $this->share = $this->createMock(IShare::class);
+ $this->share->expects(self::any())
+ ->method('getNode')
+ ->willReturn($this->node);
$this->server = $this->createMock(Server::class);
$this->plugin = new FilesDropPlugin();
$this->request = $this->createMock(RequestInterface::class);
$this->response = $this->createMock(ResponseInterface::class);
- $this->response->expects($this->never())
- ->method($this->anything());
- }
-
- public function testInitialize() {
- $this->server->expects($this->once())
- ->method('on')
- ->with(
- $this->equalTo('beforeMethod:*'),
- $this->equalTo([$this->plugin, 'beforeMethod']),
- $this->equalTo(999)
- );
+ $attributes = $this->createMock(IAttributes::class);
+ $this->share->expects($this->any())
+ ->method('getAttributes')
+ ->willReturn($attributes);
- $this->plugin->initialize($this->server);
+ $this->share
+ ->method('getToken')
+ ->willReturn('token');
}
- public function testNotEnabled() {
- $this->view->expects($this->never())
- ->method($this->anything());
-
+ public function testNotEnabled(): void {
$this->request->expects($this->never())
->method($this->anything());
$this->plugin->beforeMethod($this->request, $this->response);
}
- public function testValid() {
+ public function testValid(): void {
$this->plugin->enable();
- $this->plugin->setView($this->view);
+ $this->plugin->setShare($this->share);
$this->request->method('getMethod')
->willReturn('PUT');
$this->request->method('getPath')
- ->willReturn('file.txt');
+ ->willReturn('/files/token/file.txt');
$this->request->method('getBaseUrl')
->willReturn('https://example.com');
- $this->view->method('file_exists')
- ->with('/file.txt')
- ->willReturn(false);
+ $this->node->expects(self::once())
+ ->method('getNonExistingName')
+ ->with('file.txt')
+ ->willReturn('file.txt');
$this->request->expects($this->once())
->method('setUrl')
- ->with('https://example.com/file.txt');
+ ->with('https://example.com/files/token/file.txt');
$this->plugin->beforeMethod($this->request, $this->response);
}
- public function testFileAlreadyExistsValid() {
+ public function testFileAlreadyExistsValid(): void {
$this->plugin->enable();
- $this->plugin->setView($this->view);
+ $this->plugin->setShare($this->share);
$this->request->method('getMethod')
->willReturn('PUT');
$this->request->method('getPath')
- ->willReturn('file.txt');
+ ->willReturn('/files/token/file.txt');
$this->request->method('getBaseUrl')
->willReturn('https://example.com');
- $this->view->method('file_exists')
- ->willReturnCallback(function ($path) {
- if ($path === 'file.txt' || $path === '/file.txt') {
- return true;
- } else {
- return false;
- }
- });
+ $this->node->method('getNonExistingName')
+ ->with('file.txt')
+ ->willReturn('file (2).txt');
$this->request->expects($this->once())
->method('setUrl')
- ->with($this->equalTo('https://example.com/file (2).txt'));
+ ->with($this->equalTo('https://example.com/files/token/file (2).txt'));
$this->plugin->beforeMethod($this->request, $this->response);
}
- public function testNoMKCOL() {
+ public function testNoMKCOLWithoutNickname(): void {
$this->plugin->enable();
- $this->plugin->setView($this->view);
+ $this->plugin->setShare($this->share);
$this->request->method('getMethod')
->willReturn('MKCOL');
- $this->expectException(MethodNotAllowed::class);
+ $this->expectException(BadRequest::class);
+
+ $this->plugin->beforeMethod($this->request, $this->response);
+ }
+
+ public function testMKCOLWithNickname(): void {
+ $this->plugin->enable();
+ $this->plugin->setShare($this->share);
+
+ $this->request->method('getMethod')
+ ->willReturn('MKCOL');
+
+ $this->request->method('hasHeader')
+ ->with('X-NC-Nickname')
+ ->willReturn(true);
+ $this->request->method('getHeader')
+ ->with('X-NC-Nickname')
+ ->willReturn('nickname');
+
+ $this->expectNotToPerformAssertions();
$this->plugin->beforeMethod($this->request, $this->response);
}
- public function testNoSubdirPut() {
+ public function testSubdirPut(): void {
$this->plugin->enable();
- $this->plugin->setView($this->view);
+ $this->plugin->setShare($this->share);
$this->request->method('getMethod')
->willReturn('PUT');
+ $this->request->method('hasHeader')
+ ->with('X-NC-Nickname')
+ ->willReturn(true);
+ $this->request->method('getHeader')
+ ->with('X-NC-Nickname')
+ ->willReturn('nickname');
+
$this->request->method('getPath')
- ->willReturn('folder/file.txt');
+ ->willReturn('/files/token/folder/file.txt');
$this->request->method('getBaseUrl')
->willReturn('https://example.com');
- $this->view->method('file_exists')
- ->willReturnCallback(function ($path) {
- if ($path === 'file.txt' || $path === '/file.txt') {
- return true;
- } else {
- return false;
- }
- });
+ $nodeName = $this->createMock(Folder::class);
+ $nodeFolder = $this->createMock(Folder::class);
+ $nodeFolder->expects(self::once())
+ ->method('getPath')
+ ->willReturn('/files/token/nickname/folder');
+ $nodeFolder->method('getNonExistingName')
+ ->with('file.txt')
+ ->willReturn('file.txt');
+ $nodeName->expects(self::once())
+ ->method('get')
+ ->with('folder')
+ ->willThrowException(new NotFoundException());
+ $nodeName->expects(self::once())
+ ->method('newFolder')
+ ->with('folder')
+ ->willReturn($nodeFolder);
+
+ $this->node->expects(self::once())
+ ->method('get')
+ ->willThrowException(new NotFoundException());
+ $this->node->expects(self::once())
+ ->method('newFolder')
+ ->with('nickname')
+ ->willReturn($nodeName);
$this->request->expects($this->once())
->method('setUrl')
- ->with($this->equalTo('https://example.com/file (2).txt'));
+ ->with($this->equalTo('https://example.com/files/token/nickname/folder/file.txt'));
$this->plugin->beforeMethod($this->request, $this->response);
}
+
+ public function testRecursiveFolderCreation(): void {
+ $this->plugin->enable();
+ $this->plugin->setShare($this->share);
+
+ $this->request->method('getMethod')
+ ->willReturn('PUT');
+ $this->request->method('hasHeader')
+ ->with('X-NC-Nickname')
+ ->willReturn(true);
+ $this->request->method('getHeader')
+ ->with('X-NC-Nickname')
+ ->willReturn('nickname');
+
+ $this->request->method('getPath')
+ ->willReturn('/files/token/folder/subfolder/file.txt');
+ $this->request->method('getBaseUrl')
+ ->willReturn('https://example.com');
+
+ $this->request->expects($this->once())
+ ->method('setUrl')
+ ->with($this->equalTo('https://example.com/files/token/nickname/folder/subfolder/file.txt'));
+
+ $subfolder = $this->createMock(Folder::class);
+ $subfolder->expects(self::once())
+ ->method('getNonExistingName')
+ ->with('file.txt')
+ ->willReturn('file.txt');
+ $subfolder->expects(self::once())
+ ->method('getPath')
+ ->willReturn('/files/token/nickname/folder/subfolder');
+
+ $folder = $this->createMock(Folder::class);
+ $folder->expects(self::once())
+ ->method('get')
+ ->with('subfolder')
+ ->willReturn($subfolder);
+
+ $nickname = $this->createMock(Folder::class);
+ $nickname->expects(self::once())
+ ->method('get')
+ ->with('folder')
+ ->willReturn($folder);
+
+ $this->node->method('get')
+ ->with('nickname')
+ ->willReturn($nickname);
+ $this->plugin->beforeMethod($this->request, $this->response);
+ }
+
+ public function testOnMkcol(): void {
+ $this->plugin->enable();
+ $this->plugin->setShare($this->share);
+
+ $this->response->expects($this->once())
+ ->method('setStatus')
+ ->with(201);
+
+ $response = $this->plugin->onMkcol($this->request, $this->response);
+ $this->assertFalse($response);
+ }
}
diff --git a/apps/dav/tests/unit/Listener/ActivityUpdaterListenerTest.php b/apps/dav/tests/unit/Listener/ActivityUpdaterListenerTest.php
new file mode 100644
index 00000000000..8519dca7126
--- /dev/null
+++ b/apps/dav/tests/unit/Listener/ActivityUpdaterListenerTest.php
@@ -0,0 +1,80 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\Listener;
+
+use OCA\DAV\CalDAV\Activity\Backend as ActivityBackend;
+use OCA\DAV\CalDAV\Activity\Provider\Event;
+use OCA\DAV\DAV\Sharing\Plugin as SharingPlugin;
+use OCA\DAV\Events\CalendarDeletedEvent;
+use OCA\DAV\Listener\ActivityUpdaterListener;
+use OCP\Calendar\Events\CalendarObjectDeletedEvent;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class ActivityUpdaterListenerTest extends TestCase {
+
+ private ActivityBackend&MockObject $activityBackend;
+ private LoggerInterface&MockObject $logger;
+ private ActivityUpdaterListener $listener;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->activityBackend = $this->createMock(ActivityBackend::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+
+ $this->listener = new ActivityUpdaterListener(
+ $this->activityBackend,
+ $this->logger
+ );
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataForTestHandleCalendarObjectDeletedEvent')]
+ public function testHandleCalendarObjectDeletedEvent(int $calendarId, array $calendarData, array $shares, array $objectData, bool $createsActivity): void {
+ $event = new CalendarObjectDeletedEvent($calendarId, $calendarData, $shares, $objectData);
+ $this->logger->expects($this->once())->method('debug')->with(
+ $createsActivity ? "Activity generated for deleted calendar object in calendar $calendarId" : "Calendar object in calendar $calendarId was already in trashbin, skipping deletion activity"
+ );
+ $this->activityBackend->expects($createsActivity ? $this->once() : $this->never())->method('onTouchCalendarObject')->with(
+ Event::SUBJECT_OBJECT_DELETE,
+ $calendarData,
+ $shares,
+ $objectData
+ );
+ $this->listener->handle($event);
+ }
+
+ public static function dataForTestHandleCalendarObjectDeletedEvent(): array {
+ return [
+ [1, [], [], [], true],
+ [1, [], [], ['{' . SharingPlugin::NS_NEXTCLOUD . '}deleted-at' => 120], false],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataForTestHandleCalendarDeletedEvent')]
+ public function testHandleCalendarDeletedEvent(int $calendarId, array $calendarData, array $shares, bool $createsActivity): void {
+ $event = new CalendarDeletedEvent($calendarId, $calendarData, $shares);
+ $this->logger->expects($this->once())->method('debug')->with(
+ $createsActivity ? "Activity generated for deleted calendar $calendarId" : "Calendar $calendarId was already in trashbin, skipping deletion activity"
+ );
+ $this->activityBackend->expects($createsActivity ? $this->once() : $this->never())->method('onCalendarDelete')->with(
+ $calendarData,
+ $shares
+ );
+ $this->listener->handle($event);
+ }
+
+ public static function dataForTestHandleCalendarDeletedEvent(): array {
+ return [
+ [1, [], [], true],
+ [1, ['{' . SharingPlugin::NS_NEXTCLOUD . '}deleted-at' => 120], [], false],
+ ];
+ }
+}
diff --git a/apps/dav/tests/unit/Listener/CalendarContactInteractionListenerTest.php b/apps/dav/tests/unit/Listener/CalendarContactInteractionListenerTest.php
index 29058653a47..dc3dce8a62f 100644
--- a/apps/dav/tests/unit/Listener/CalendarContactInteractionListenerTest.php
+++ b/apps/dav/tests/unit/Listener/CalendarContactInteractionListenerTest.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\Tests\Unit\Listener;
+namespace OCA\DAV\Tests\unit\Listener;
use OCA\DAV\Connector\Sabre\Principal;
-use OCA\DAV\Events\CalendarObjectCreatedEvent;
use OCA\DAV\Events\CalendarShareUpdatedEvent;
use OCA\DAV\Listener\CalendarContactInteractionListener;
+use OCP\Calendar\Events\CalendarObjectCreatedEvent;
use OCP\Contacts\Events\ContactInteractedWithEvent;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventDispatcher;
@@ -40,24 +23,12 @@ use Psr\Log\LoggerInterface;
use Test\TestCase;
class CalendarContactInteractionListenerTest extends TestCase {
-
- /** @var IEventDispatcher|MockObject */
- private $eventDispatcher;
-
- /** @var IUserSession|MockObject */
- private $userSession;
-
- /** @var Principal|MockObject */
- private $principalConnector;
-
- /** @var LoggerInterface|MockObject */
- private $logger;
-
- /** @var IMailer|MockObject */
- private $mailer;
-
- /** @var CalendarContactInteractionListener */
- private $listener;
+ private IEventDispatcher&MockObject $eventDispatcher;
+ private IUserSession&MockObject $userSession;
+ private Principal&MockObject $principalConnector;
+ private LoggerInterface&MockObject $logger;
+ private IMailer&MockObject $mailer;
+ private CalendarContactInteractionListener $listener;
protected function setUp(): void {
parent::setUp();
diff --git a/apps/dav/tests/unit/Listener/OutOfOfficeListenerTest.php b/apps/dav/tests/unit/Listener/OutOfOfficeListenerTest.php
new file mode 100644
index 00000000000..971d113b742
--- /dev/null
+++ b/apps/dav/tests/unit/Listener/OutOfOfficeListenerTest.php
@@ -0,0 +1,606 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\Listener;
+
+use DateTimeImmutable;
+use InvalidArgumentException;
+use OCA\DAV\CalDAV\Calendar;
+use OCA\DAV\CalDAV\CalendarHome;
+use OCA\DAV\CalDAV\CalendarObject;
+use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
+use OCA\DAV\CalDAV\Plugin;
+use OCA\DAV\CalDAV\TimezoneService;
+use OCA\DAV\Connector\Sabre\Server;
+use OCA\DAV\Listener\OutOfOfficeListener;
+use OCA\DAV\ServerFactory;
+use OCP\EventDispatcher\Event;
+use OCP\IConfig;
+use OCP\IUser;
+use OCP\User\Events\OutOfOfficeChangedEvent;
+use OCP\User\Events\OutOfOfficeClearedEvent;
+use OCP\User\Events\OutOfOfficeScheduledEvent;
+use OCP\User\IOutOfOfficeData;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\Tree;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VEvent;
+use Sabre\VObject\Reader;
+use Test\TestCase;
+
+/**
+ * @covers \OCA\DAV\Listener\OutOfOfficeListener
+ */
+class OutOfOfficeListenerTest extends TestCase {
+
+ private ServerFactory&MockObject $serverFactory;
+ private IConfig&MockObject $appConfig;
+ private LoggerInterface&MockObject $loggerInterface;
+ private TimezoneService&MockObject $timezoneService;
+ private OutOfOfficeListener $listener;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->serverFactory = $this->createMock(ServerFactory::class);
+ $this->appConfig = $this->createMock(IConfig::class);
+ $this->timezoneService = $this->createMock(TimezoneService::class);
+ $this->loggerInterface = $this->createMock(LoggerInterface::class);
+
+ $this->listener = new OutOfOfficeListener(
+ $this->serverFactory,
+ $this->appConfig,
+ $this->timezoneService,
+ $this->loggerInterface,
+ );
+ }
+
+ public function testHandleUnrelated(): void {
+ $event = new Event();
+
+ $this->listener->handle($event);
+
+ $this->addToAssertionCount(1);
+ }
+
+ public function testHandleSchedulingNoCalendarHome(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $event = new OutOfOfficeScheduledEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleSchedulingNoCalendarHomeNode(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $caldavPlugin->expects(self::once())
+ ->method('getCalendarHomeForPrincipal')
+ ->willReturn('/home/calendar');
+ $tree = $this->createMock(Tree::class);
+ $davServer->tree = $tree;
+ $tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/home/calendar')
+ ->willThrowException(new NotFound('nope'));
+ $event = new OutOfOfficeScheduledEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleSchedulingPersonalCalendarNotFound(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $caldavPlugin->expects(self::once())
+ ->method('getCalendarHomeForPrincipal')
+ ->willReturn('/home/calendar');
+ $tree = $this->createMock(Tree::class);
+ $davServer->tree = $tree;
+ $calendarHome = $this->createMock(CalendarHome::class);
+ $tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/home/calendar')
+ ->willReturn($calendarHome);
+ $this->appConfig->expects(self::once())
+ ->method('getUserValue')
+ ->with('user123', 'dav', 'defaultCalendar', 'personal')
+ ->willReturn('personal-1');
+ $calendarHome->expects(self::once())
+ ->method('getChild')
+ ->with('personal-1')
+ ->willThrowException(new NotFound('nope'));
+ $event = new OutOfOfficeScheduledEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleSchedulingWithDefaultTimezone(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $data->method('getStartDate')
+ ->willReturn((new DateTimeImmutable('2023-12-12T00:00:00Z'))->getTimestamp());
+ $data->method('getEndDate')
+ ->willReturn((new DateTimeImmutable('2023-12-13T00:00:00Z'))->getTimestamp());
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $caldavPlugin->expects(self::once())
+ ->method('getCalendarHomeForPrincipal')
+ ->willReturn('/home/calendar');
+ $tree = $this->createMock(Tree::class);
+ $davServer->tree = $tree;
+ $calendarHome = $this->createMock(CalendarHome::class);
+ $tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/home/calendar')
+ ->willReturn($calendarHome);
+ $this->appConfig->expects(self::once())
+ ->method('getUserValue')
+ ->with('user123', 'dav', 'defaultCalendar', 'personal')
+ ->willReturn('personal-1');
+ $calendar = $this->createMock(Calendar::class);
+ $this->timezoneService->expects(self::once())
+ ->method('getUserTimezone')
+ ->with('user123')
+ ->willReturn('Europe/Prague');
+ $calendarHome->expects(self::once())
+ ->method('getChild')
+ ->with('personal-1')
+ ->willReturn($calendar);
+ $calendar->expects(self::once())
+ ->method('createFile')
+ ->willReturnCallback(function ($name, $data): void {
+ $vcalendar = Reader::read($data);
+ if (!($vcalendar instanceof VCalendar)) {
+ throw new InvalidArgumentException('Calendar data should be a VCALENDAR');
+ }
+ $vevent = $vcalendar->VEVENT;
+ if ($vevent === null || !($vevent instanceof VEvent)) {
+ throw new InvalidArgumentException('Calendar data should contain a VEVENT');
+ }
+ self::assertSame('Europe/Prague', $vevent->DTSTART['TZID']?->getValue());
+ self::assertSame('Europe/Prague', $vevent->DTEND['TZID']?->getValue());
+ });
+ $event = new OutOfOfficeScheduledEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleChangeNoCalendarHome(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $event = new OutOfOfficeChangedEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleChangeNoCalendarHomeNode(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $caldavPlugin->expects(self::once())
+ ->method('getCalendarHomeForPrincipal')
+ ->willReturn('/home/calendar');
+ $tree = $this->createMock(Tree::class);
+ $davServer->tree = $tree;
+ $tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/home/calendar')
+ ->willThrowException(new NotFound('nope'));
+ $event = new OutOfOfficeChangedEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleChangePersonalCalendarNotFound(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $caldavPlugin->expects(self::once())
+ ->method('getCalendarHomeForPrincipal')
+ ->willReturn('/home/calendar');
+ $tree = $this->createMock(Tree::class);
+ $davServer->tree = $tree;
+ $calendarHome = $this->createMock(CalendarHome::class);
+ $tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/home/calendar')
+ ->willReturn($calendarHome);
+ $this->appConfig->expects(self::once())
+ ->method('getUserValue')
+ ->with('user123', 'dav', 'defaultCalendar', 'personal')
+ ->willReturn('personal-1');
+ $calendarHome->expects(self::once())
+ ->method('getChild')
+ ->with('personal-1')
+ ->willThrowException(new NotFound('nope'));
+ $event = new OutOfOfficeChangedEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleChangeRecreate(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $data->method('getStartDate')
+ ->willReturn((new DateTimeImmutable('2023-12-12T00:00:00Z'))->getTimestamp());
+ $data->method('getEndDate')
+ ->willReturn((new DateTimeImmutable('2023-12-14T00:00:00Z'))->getTimestamp());
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $caldavPlugin->expects(self::once())
+ ->method('getCalendarHomeForPrincipal')
+ ->willReturn('/home/calendar');
+ $tree = $this->createMock(Tree::class);
+ $davServer->tree = $tree;
+ $calendarHome = $this->createMock(CalendarHome::class);
+ $tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/home/calendar')
+ ->willReturn($calendarHome);
+ $this->appConfig->expects(self::once())
+ ->method('getUserValue')
+ ->with('user123', 'dav', 'defaultCalendar', 'personal')
+ ->willReturn('personal-1');
+ $calendar = $this->createMock(Calendar::class);
+ $this->timezoneService->expects(self::once())
+ ->method('getUserTimezone')
+ ->with('user123')
+ ->willReturn(null);
+ $this->timezoneService->expects(self::once())
+ ->method('getDefaultTimezone')
+ ->willReturn('Europe/Berlin');
+ $calendarHome->expects(self::once())
+ ->method('getChild')
+ ->with('personal-1')
+ ->willReturn($calendar);
+ $calendar->expects(self::once())
+ ->method('getChild')
+ ->willThrowException(new NotFound());
+ $calendar->expects(self::once())
+ ->method('createFile')
+ ->willReturnCallback(function ($name, $data): void {
+ $vcalendar = Reader::read($data);
+ if (!($vcalendar instanceof VCalendar)) {
+ throw new InvalidArgumentException('Calendar data should be a VCALENDAR');
+ }
+ $vevent = $vcalendar->VEVENT;
+ if ($vevent === null || !($vevent instanceof VEvent)) {
+ throw new InvalidArgumentException('Calendar data should contain a VEVENT');
+ }
+ self::assertSame('Europe/Berlin', $vevent->DTSTART['TZID']?->getValue());
+ self::assertSame('Europe/Berlin', $vevent->DTEND['TZID']?->getValue());
+ });
+ $event = new OutOfOfficeChangedEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleChangeWithoutTimezone(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $data->method('getStartDate')
+ ->willReturn((new DateTimeImmutable('2023-01-12T00:00:00Z'))->getTimestamp());
+ $data->method('getEndDate')
+ ->willReturn((new DateTimeImmutable('2023-12-14T00:00:00Z'))->getTimestamp());
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $caldavPlugin->expects(self::once())
+ ->method('getCalendarHomeForPrincipal')
+ ->willReturn('/home/calendar');
+ $tree = $this->createMock(Tree::class);
+ $davServer->tree = $tree;
+ $calendarHome = $this->createMock(CalendarHome::class);
+ $tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/home/calendar')
+ ->willReturn($calendarHome);
+ $this->appConfig->expects(self::once())
+ ->method('getUserValue')
+ ->with('user123', 'dav', 'defaultCalendar', 'personal')
+ ->willReturn('personal-1');
+ $calendar = $this->createMock(Calendar::class);
+ $calendarHome->expects(self::once())
+ ->method('getChild')
+ ->with('personal-1')
+ ->willReturn($calendar);
+ $eventNode = $this->createMock(CalendarObject::class);
+ $this->timezoneService->expects(self::once())
+ ->method('getUserTimezone')
+ ->with('user123')
+ ->willReturn(null);
+ $this->timezoneService->expects(self::once())
+ ->method('getDefaultTimezone')
+ ->willReturn('UTC');
+ $calendar->expects(self::once())
+ ->method('getChild')
+ ->willReturn($eventNode);
+ $eventNode->expects(self::once())
+ ->method('put')
+ ->willReturnCallback(function ($data): void {
+ $vcalendar = Reader::read($data);
+ if (!($vcalendar instanceof VCalendar)) {
+ throw new InvalidArgumentException('Calendar data should be a VCALENDAR');
+ }
+ $vevent = $vcalendar->VEVENT;
+ if ($vevent === null || !($vevent instanceof VEvent)) {
+ throw new InvalidArgumentException('Calendar data should contain a VEVENT');
+ }
+ // UTC datetimes are stored without a TZID
+ self::assertSame(null, $vevent->DTSTART['TZID']?->getValue());
+ self::assertSame(null, $vevent->DTEND['TZID']?->getValue());
+ });
+ $calendar->expects(self::never())
+ ->method('createFile');
+ $event = new OutOfOfficeChangedEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleClearNoCalendarHome(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $event = new OutOfOfficeClearedEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleClearNoCalendarHomeNode(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $caldavPlugin->expects(self::once())
+ ->method('getCalendarHomeForPrincipal')
+ ->willReturn('/home/calendar');
+ $tree = $this->createMock(Tree::class);
+ $davServer->tree = $tree;
+ $tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/home/calendar')
+ ->willThrowException(new NotFound('nope'));
+ $event = new OutOfOfficeClearedEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleClearPersonalCalendarNotFound(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $caldavPlugin->expects(self::once())
+ ->method('getCalendarHomeForPrincipal')
+ ->willReturn('/home/calendar');
+ $tree = $this->createMock(Tree::class);
+ $davServer->tree = $tree;
+ $calendarHome = $this->createMock(CalendarHome::class);
+ $tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/home/calendar')
+ ->willReturn($calendarHome);
+ $this->appConfig->expects(self::once())
+ ->method('getUserValue')
+ ->with('user123', 'dav', 'defaultCalendar', 'personal')
+ ->willReturn('personal-1');
+ $calendarHome->expects(self::once())
+ ->method('getChild')
+ ->with('personal-1')
+ ->willThrowException(new NotFound('nope'));
+ $event = new OutOfOfficeClearedEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleClearRecreate(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $caldavPlugin->expects(self::once())
+ ->method('getCalendarHomeForPrincipal')
+ ->willReturn('/home/calendar');
+ $tree = $this->createMock(Tree::class);
+ $davServer->tree = $tree;
+ $calendarHome = $this->createMock(CalendarHome::class);
+ $tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/home/calendar')
+ ->willReturn($calendarHome);
+ $this->appConfig->expects(self::once())
+ ->method('getUserValue')
+ ->with('user123', 'dav', 'defaultCalendar', 'personal')
+ ->willReturn('personal-1');
+ $calendar = $this->createMock(Calendar::class);
+ $calendarHome->expects(self::once())
+ ->method('getChild')
+ ->with('personal-1')
+ ->willReturn($calendar);
+ $calendar->expects(self::once())
+ ->method('getChild')
+ ->willThrowException(new NotFound());
+ $event = new OutOfOfficeClearedEvent($data);
+
+ $this->listener->handle($event);
+ }
+
+ public function testHandleClear(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('user123');
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getUser')->willReturn($user);
+ $davServer = $this->createMock(Server::class);
+ $invitationServer = $this->createMock(InvitationResponseServer::class);
+ $invitationServer->method('getServer')->willReturn($davServer);
+ $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer);
+ $caldavPlugin = $this->createMock(Plugin::class);
+ $davServer->expects(self::once())
+ ->method('getPlugin')
+ ->with('caldav')
+ ->willReturn($caldavPlugin);
+ $caldavPlugin->expects(self::once())
+ ->method('getCalendarHomeForPrincipal')
+ ->willReturn('/home/calendar');
+ $tree = $this->createMock(Tree::class);
+ $davServer->tree = $tree;
+ $calendarHome = $this->createMock(CalendarHome::class);
+ $tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/home/calendar')
+ ->willReturn($calendarHome);
+ $this->appConfig->expects(self::once())
+ ->method('getUserValue')
+ ->with('user123', 'dav', 'defaultCalendar', 'personal')
+ ->willReturn('personal-1');
+ $calendar = $this->createMock(Calendar::class);
+ $calendarHome->expects(self::once())
+ ->method('getChild')
+ ->with('personal-1')
+ ->willReturn($calendar);
+ $eventNode = $this->createMock(CalendarObject::class);
+ $calendar->expects(self::once())
+ ->method('getChild')
+ ->willReturn($eventNode);
+ $eventNode->expects(self::once())
+ ->method('delete');
+ $event = new OutOfOfficeClearedEvent($data);
+
+ $this->listener->handle($event);
+ }
+}
diff --git a/apps/dav/tests/unit/Migration/CalDAVRemoveEmptyValueTest.php b/apps/dav/tests/unit/Migration/CalDAVRemoveEmptyValueTest.php
index 478be5b4294..1852d2709c1 100644
--- a/apps/dav/tests/unit/Migration/CalDAVRemoveEmptyValueTest.php
+++ b/apps/dav/tests/unit/Migration/CalDAVRemoveEmptyValueTest.php
@@ -1,34 +1,19 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright 2016, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have 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\Tests\Unit\DAV\Migration;
+namespace OCA\DAV\Tests\unit\DAV\Migration;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\Migration\CalDAVRemoveEmptyValue;
-use OCP\ILogger;
+use OCP\IDBConnection;
use OCP\Migration\IOutput;
+use OCP\Server;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
use Sabre\VObject\InvalidDataException;
use Test\TestCase;
@@ -39,18 +24,10 @@ use Test\TestCase;
* @group DB
*/
class CalDAVRemoveEmptyValueTest extends TestCase {
-
- /** @var ILogger|\PHPUnit\Framework\MockObject\MockObject */
- private $logger;
-
- /** @var CalDavBackend|\PHPUnit\Framework\MockObject\MockObject */
- private $backend;
-
- /** @var IOutput|\PHPUnit\Framework\MockObject\MockObject */
- private $output;
-
- /** @var string */
- private $invalid = 'BEGIN:VCALENDAR
+ private LoggerInterface&MockObject $logger;
+ private CalDavBackend&MockObject $backend;
+ private IOutput&MockObject $output;
+ private string $invalid = 'BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//Mac OS X 10.11.2//EN
CALSCALE:GREGORIAN
@@ -70,8 +47,7 @@ CREATED;VALUE=:20151214T091032Z
END:VEVENT
END:VCALENDAR';
- /** @var string */
- private $valid = 'BEGIN:VCALENDAR
+ private string $valid = 'BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//Mac OS X 10.11.2//EN
CALSCALE:GREGORIAN
@@ -94,20 +70,20 @@ END:VCALENDAR';
protected function setUp(): void {
parent::setUp();
- $this->logger = $this->createMock(ILogger::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
$this->backend = $this->createMock(CalDavBackend::class);
$this->output = $this->createMock(IOutput::class);
}
- public function testRunAllValid() {
- /** @var CalDAVRemoveEmptyValue|\PHPUnit\Framework\MockObject\MockObject $step */
+ public function testRunAllValid(): void {
+ /** @var CalDAVRemoveEmptyValue&MockObject $step */
$step = $this->getMockBuilder(CalDAVRemoveEmptyValue::class)
->setConstructorArgs([
- \OC::$server->getDatabaseConnection(),
+ Server::get(IDBConnection::class),
$this->backend,
$this->logger
])
- ->setMethods(['getInvalidObjects'])
+ ->onlyMethods(['getInvalidObjects'])
->getMock();
$step->expects($this->once())
@@ -123,15 +99,15 @@ END:VCALENDAR';
$step->run($this->output);
}
- public function testRunInvalid() {
- /** @var CalDAVRemoveEmptyValue|\PHPUnit\Framework\MockObject\MockObject $step */
+ public function testRunInvalid(): void {
+ /** @var CalDAVRemoveEmptyValue&MockObject $step */
$step = $this->getMockBuilder(CalDAVRemoveEmptyValue::class)
->setConstructorArgs([
- \OC::$server->getDatabaseConnection(),
+ Server::get(IDBConnection::class),
$this->backend,
$this->logger
])
- ->setMethods(['getInvalidObjects'])
+ ->onlyMethods(['getInvalidObjects'])
->getMock();
$step->expects($this->once())
@@ -166,15 +142,15 @@ END:VCALENDAR';
$step->run($this->output);
}
- public function testRunValid() {
- /** @var CalDAVRemoveEmptyValue|\PHPUnit\Framework\MockObject\MockObject $step */
+ public function testRunValid(): void {
+ /** @var CalDAVRemoveEmptyValue&MockObject $step */
$step = $this->getMockBuilder(CalDAVRemoveEmptyValue::class)
->setConstructorArgs([
- \OC::$server->getDatabaseConnection(),
+ Server::get(IDBConnection::class),
$this->backend,
$this->logger
])
- ->setMethods(['getInvalidObjects'])
+ ->onlyMethods(['getInvalidObjects'])
->getMock();
$step->expects($this->once())
@@ -208,15 +184,15 @@ END:VCALENDAR';
$step->run($this->output);
}
- public function testRunStillInvalid() {
- /** @var CalDAVRemoveEmptyValue|\PHPUnit\Framework\MockObject\MockObject $step */
+ public function testRunStillInvalid(): void {
+ /** @var CalDAVRemoveEmptyValue&MockObject $step */
$step = $this->getMockBuilder(CalDAVRemoveEmptyValue::class)
->setConstructorArgs([
- \OC::$server->getDatabaseConnection(),
+ Server::get(IDBConnection::class),
$this->backend,
$this->logger
])
- ->setMethods(['getInvalidObjects'])
+ ->onlyMethods(['getInvalidObjects'])
->getMock();
$step->expects($this->once())
diff --git a/apps/dav/tests/unit/Migration/CreateSystemAddressBookStepTest.php b/apps/dav/tests/unit/Migration/CreateSystemAddressBookStepTest.php
new file mode 100644
index 00000000000..667d2e39d3a
--- /dev/null
+++ b/apps/dav/tests/unit/Migration/CreateSystemAddressBookStepTest.php
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\Migration;
+
+use OCA\DAV\CardDAV\SyncService;
+use OCA\DAV\Migration\CreateSystemAddressBookStep;
+use OCP\Migration\IOutput;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+class CreateSystemAddressBookStepTest extends TestCase {
+
+ private SyncService&MockObject $syncService;
+ private CreateSystemAddressBookStep $step;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->syncService = $this->createMock(SyncService::class);
+
+ $this->step = new CreateSystemAddressBookStep(
+ $this->syncService,
+ );
+ }
+
+ public function testGetName(): void {
+ $name = $this->step->getName();
+
+ self::assertEquals('Create system address book', $name);
+ }
+
+ public function testRun(): void {
+ $output = $this->createMock(IOutput::class);
+
+ $this->step->run($output);
+
+ $this->addToAssertionCount(1);
+ }
+
+}
diff --git a/apps/dav/tests/unit/Migration/RefreshWebcalJobRegistrarTest.php b/apps/dav/tests/unit/Migration/RefreshWebcalJobRegistrarTest.php
index 427bf6da145..8e7bf366cbf 100644
--- a/apps/dav/tests/unit/Migration/RefreshWebcalJobRegistrarTest.php
+++ b/apps/dav/tests/unit/Migration/RefreshWebcalJobRegistrarTest.php
@@ -1,28 +1,9 @@
<?php
+
+declare(strict_types=1);
/**
- * @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 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: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Tests\unit\DAV\Migration;
@@ -33,18 +14,13 @@ use OCP\DB\IResult;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
+use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class RefreshWebcalJobRegistrarTest extends TestCase {
-
- /** @var IDBConnection | \PHPUnit\Framework\MockObject\MockObject */
- private $db;
-
- /** @var IJobList | \PHPUnit\Framework\MockObject\MockObject */
- private $jobList;
-
- /** @var RefreshWebcalJobRegistrar */
- private $migration;
+ private IDBConnection&MockObject $db;
+ private IJobList&MockObject $jobList;
+ private RefreshWebcalJobRegistrar $migration;
protected function setUp(): void {
parent::setUp();
@@ -55,11 +31,11 @@ class RefreshWebcalJobRegistrarTest extends TestCase {
$this->migration = new RefreshWebcalJobRegistrar($this->db, $this->jobList);
}
- public function testGetName() {
+ public function testGetName(): void {
$this->assertEquals($this->migration->getName(), 'Registering background jobs to update cache for webcal calendars');
}
- public function testRun() {
+ public function testRun(): void {
$output = $this->createMock(IOutput::class);
$queryBuilder = $this->createMock(IQueryBuilder::class);
@@ -69,77 +45,70 @@ class RefreshWebcalJobRegistrarTest extends TestCase {
->method('getQueryBuilder')
->willReturn($queryBuilder);
- $queryBuilder->expects($this->at(0))
+ $queryBuilder->expects($this->once())
->method('select')
->with(['principaluri', 'uri'])
->willReturn($queryBuilder);
- $queryBuilder->expects($this->at(1))
+ $queryBuilder->expects($this->once())
->method('from')
->with('calendarsubscriptions')
->willReturn($queryBuilder);
- $queryBuilder->expects($this->at(2))
+ $queryBuilder->expects($this->once())
->method('execute')
->willReturn($statement);
- $statement->expects($this->at(0))
- ->method('fetch')
- ->with(\PDO::FETCH_ASSOC)
- ->willReturn([
- 'principaluri' => 'foo1',
- 'uri' => 'bar1',
- ]);
- $statement->expects($this->at(1))
+ $statement->expects($this->exactly(4))
->method('fetch')
->with(\PDO::FETCH_ASSOC)
- ->willReturn([
- 'principaluri' => 'foo2',
- 'uri' => 'bar2',
- ]);
- $statement->expects($this->at(2))
- ->method('fetch')
- ->with(\PDO::FETCH_ASSOC)
- ->willReturn([
- 'principaluri' => 'foo3',
- 'uri' => 'bar3',
+ ->willReturnOnConsecutiveCalls(
+ [
+ 'principaluri' => 'foo1',
+ 'uri' => 'bar1',
+ ],
+ [
+ 'principaluri' => 'foo2',
+ 'uri' => 'bar2',
+ ],
+ [
+ 'principaluri' => 'foo3',
+ 'uri' => 'bar3',
+ ],
+ null
+ );
+
+ $this->jobList->expects($this->exactly(3))
+ ->method('has')
+ ->willReturnMap([
+ [RefreshWebcalJob::class, [
+ 'principaluri' => 'foo1',
+ 'uri' => 'bar1',
+ ], false],
+ [RefreshWebcalJob::class, [
+ 'principaluri' => 'foo2',
+ 'uri' => 'bar2',
+ ], true ],
+ [RefreshWebcalJob::class, [
+ 'principaluri' => 'foo3',
+ 'uri' => 'bar3',
+ ], false],
]);
- $statement->expects($this->at(0))
- ->method('fetch')
- ->with(\PDO::FETCH_ASSOC)
- ->willReturn(null);
- $this->jobList->expects($this->at(0))
- ->method('has')
- ->with(RefreshWebcalJob::class, [
- 'principaluri' => 'foo1',
- 'uri' => 'bar1',
- ])
- ->willReturn(false);
- $this->jobList->expects($this->at(1))
- ->method('add')
- ->with(RefreshWebcalJob::class, [
+ $calls = [
+ [RefreshWebcalJob::class, [
'principaluri' => 'foo1',
'uri' => 'bar1',
- ]);
- $this->jobList->expects($this->at(2))
- ->method('has')
- ->with(RefreshWebcalJob::class, [
- 'principaluri' => 'foo2',
- 'uri' => 'bar2',
- ])
- ->willReturn(true);
- $this->jobList->expects($this->at(3))
- ->method('has')
- ->with(RefreshWebcalJob::class, [
+ ]],
+ [RefreshWebcalJob::class, [
'principaluri' => 'foo3',
'uri' => 'bar3',
- ])
- ->willReturn(false);
- $this->jobList->expects($this->at(4))
+ ]]
+ ];
+ $this->jobList->expects($this->exactly(2))
->method('add')
- ->with(RefreshWebcalJob::class, [
- 'principaluri' => 'foo3',
- 'uri' => 'bar3',
- ]);
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
$output->expects($this->once())
->method('info')
diff --git a/apps/dav/tests/unit/Migration/RegenerateBirthdayCalendarsTest.php b/apps/dav/tests/unit/Migration/RegenerateBirthdayCalendarsTest.php
index bb5210f18fb..6f681badb8b 100644
--- a/apps/dav/tests/unit/Migration/RegenerateBirthdayCalendarsTest.php
+++ b/apps/dav/tests/unit/Migration/RegenerateBirthdayCalendarsTest.php
@@ -1,28 +1,9 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018, Georg Ehrke
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author François Freitag <mail@franek.fr>
- * @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: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Tests\unit\DAV\Migration;
@@ -31,18 +12,13 @@ use OCA\DAV\Migration\RegenerateBirthdayCalendars;
use OCP\BackgroundJob\IJobList;
use OCP\IConfig;
use OCP\Migration\IOutput;
+use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class RegenerateBirthdayCalendarsTest extends TestCase {
-
- /** @var IJobList | \PHPUnit\Framework\MockObject\MockObject */
- private $jobList;
-
- /** @var IConfig | \PHPUnit\Framework\MockObject\MockObject */
- private $config;
-
- /** @var RegenerateBirthdayCalendars */
- private $migration;
+ private IJobList&MockObject $jobList;
+ private IConfig&MockObject $config;
+ private RegenerateBirthdayCalendars $migration;
protected function setUp(): void {
parent::setUp();
@@ -54,14 +30,14 @@ class RegenerateBirthdayCalendarsTest extends TestCase {
$this->config);
}
- public function testGetName() {
+ public function testGetName(): void {
$this->assertEquals(
'Regenerating birthday calendars to use new icons and fix old birthday events without year',
$this->migration->getName()
);
}
- public function testRun() {
+ public function testRun(): void {
$this->config->expects($this->once())
->method('getAppValue')
->with('dav', 'regeneratedBirthdayCalendarsForYearFix')
@@ -83,7 +59,7 @@ class RegenerateBirthdayCalendarsTest extends TestCase {
$this->migration->run($output);
}
- public function testRunSecondTime() {
+ public function testRunSecondTime(): void {
$this->config->expects($this->once())
->method('getAppValue')
->with('dav', 'regeneratedBirthdayCalendarsForYearFix')
diff --git a/apps/dav/tests/unit/Migration/RemoveDeletedUsersCalendarSubscriptionsTest.php b/apps/dav/tests/unit/Migration/RemoveDeletedUsersCalendarSubscriptionsTest.php
index c0d6518d205..a9758470573 100644
--- a/apps/dav/tests/unit/Migration/RemoveDeletedUsersCalendarSubscriptionsTest.php
+++ b/apps/dav/tests/unit/Migration/RemoveDeletedUsersCalendarSubscriptionsTest.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\Tests\unit\DAV\Migration;
@@ -39,23 +22,10 @@ use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class RemoveDeletedUsersCalendarSubscriptionsTest extends TestCase {
- /**
- * @var IDBConnection|MockObject
- */
- private $dbConnection;
- /**
- * @var IUserManager|MockObject
- */
- private $userManager;
-
- /**
- * @var IOutput|MockObject
- */
- private $output;
- /**
- * @var RemoveDeletedUsersCalendarSubscriptions
- */
- private $migration;
+ private IDBConnection&MockObject $dbConnection;
+ private IUserManager&MockObject $userManager;
+ private IOutput&MockObject $output;
+ private RemoveDeletedUsersCalendarSubscriptions $migration;
protected function setUp(): void {
@@ -75,13 +45,7 @@ class RemoveDeletedUsersCalendarSubscriptionsTest extends TestCase {
);
}
- /**
- * @dataProvider dataTestRun
- * @param array $subscriptions
- * @param array $userExists
- * @param int $deletions
- * @throws \Exception
- */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTestRun')]
public function testRun(array $subscriptions, array $userExists, int $deletions): void {
$qb = $this->createMock(IQueryBuilder::class);
@@ -111,7 +75,7 @@ class RemoveDeletedUsersCalendarSubscriptionsTest extends TestCase {
$qb->method('execute')
->willReturn($result);
- $result->expects($this->at(0))
+ $result->expects($this->once())
->method('fetchOne')
->willReturn(count($subscriptions));
@@ -149,21 +113,28 @@ class RemoveDeletedUsersCalendarSubscriptionsTest extends TestCase {
$this->migration->run($this->output);
}
- public function dataTestRun(): array {
+ public static function dataTestRun(): array {
return [
[[], [], 0],
- [[[
- 'id' => 1,
- 'principaluri' => 'users/principals/foo1',
- ],
+ [
[
- 'id' => 2,
- 'principaluri' => 'users/principals/bar1',
+ [
+ 'id' => 1,
+ 'principaluri' => 'users/principals/foo1',
+ ],
+ [
+ 'id' => 2,
+ 'principaluri' => 'users/principals/bar1',
+ ],
+ [
+ 'id' => 3,
+ 'principaluri' => 'users/principals/bar1',
+ ],
+ [],
],
- [
- 'id' => 3,
- 'principaluri' => 'users/principals/bar1',
- ]], ['foo1' => true, 'bar1' => false], 2]
+ ['foo1' => true, 'bar1' => false],
+ 2
+ ],
];
}
}
diff --git a/apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningNodeTest.php b/apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningNodeTest.php
index 2f4728f1966..4f04aebb3e8 100644
--- a/apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningNodeTest.php
+++ b/apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningNodeTest.php
@@ -1,41 +1,21 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @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: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Tests\unit\Provisioning\Apple;
use OCA\DAV\Provisioning\Apple\AppleProvisioningNode;
use OCP\AppFramework\Utility\ITimeFactory;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\PropPatch;
use Test\TestCase;
class AppleProvisioningNodeTest extends TestCase {
-
- /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */
- private $timeFactory;
-
- /** @var AppleProvisioningNode */
- private $node;
+ private ITimeFactory&MockObject $timeFactory;
+ private AppleProvisioningNode $node;
protected function setUp(): void {
parent::setUp();
@@ -44,43 +24,42 @@ class AppleProvisioningNodeTest extends TestCase {
$this->node = new AppleProvisioningNode($this->timeFactory);
}
- public function testGetName() {
+ public function testGetName(): void {
$this->assertEquals('apple-provisioning.mobileconfig', $this->node->getName());
}
-
- public function testSetName() {
+ public function testSetName(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$this->expectExceptionMessage('Renaming apple-provisioning.mobileconfig is forbidden');
$this->node->setName('foo');
}
- public function testGetLastModified() {
+ public function testGetLastModified(): void {
$this->assertEquals(null, $this->node->getLastModified());
}
- public function testDelete() {
+ public function testDelete(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$this->expectExceptionMessage('apple-provisioning.mobileconfig may not be deleted');
$this->node->delete();
}
- public function testGetProperties() {
- $this->timeFactory->expects($this->at(0))
+ public function testGetProperties(): void {
+ $this->timeFactory->expects($this->once())
->method('getDateTime')
->willReturn(new \DateTime('2000-01-01'));
$this->assertEquals([
'{DAV:}getcontentlength' => 42,
- '{DAV:}getlastmodified' => 'Sat, 01 Jan 2000 00:00:00 +0000',
+ '{DAV:}getlastmodified' => 'Sat, 01 Jan 2000 00:00:00 GMT',
], $this->node->getProperties([]));
}
- public function testGetPropPatch() {
+ public function testGetPropPatch(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$this->expectExceptionMessage('apple-provisioning.mobileconfig\'s properties may not be altered.');
diff --git a/apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningPluginTest.php b/apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningPluginTest.php
index 5bff59cbc7c..58e588aa68d 100644
--- a/apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningPluginTest.php
+++ b/apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningPluginTest.php
@@ -1,28 +1,9 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 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\Tests\unit\Provisioning\Apple;
@@ -33,41 +14,27 @@ use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\Server;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
use Test\TestCase;
class AppleProvisioningPluginTest extends TestCase {
-
- /** @var \Sabre\DAV\Server|\PHPUnit\Framework\MockObject\MockObject */
- protected $server;
-
- /** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */
- protected $userSession;
-
- /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
- protected $urlGenerator;
-
- /** @var ThemingDefaults|\PHPUnit\Framework\MockObject\MockObject */
- protected $themingDefaults;
-
- /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
- protected $request;
-
- /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
- protected $l10n;
-
- /** @var \Sabre\HTTP\RequestInterface|\PHPUnit\Framework\MockObject\MockObject */
- protected $sabreRequest;
-
- /** @var \Sabre\HTTP\ResponseInterface|\PHPUnit\Framework\MockObject\MockObject */
- protected $sabreResponse;
-
- /** @var AppleProvisioningPlugin */
- protected $plugin;
+ protected Server&MockObject $server;
+ protected IUserSession&MockObject $userSession;
+ protected IURLGenerator&MockObject $urlGenerator;
+ protected ThemingDefaults&MockObject $themingDefaults;
+ protected IRequest&MockObject $request;
+ protected IL10N&MockObject $l10n;
+ protected RequestInterface&MockObject $sabreRequest;
+ protected ResponseInterface&MockObject $sabreResponse;
+ protected AppleProvisioningPlugin $plugin;
protected function setUp(): void {
parent::setUp();
- $this->server = $this->createMock(\Sabre\DAV\Server::class);
+ $this->server = $this->createMock(Server::class);
$this->userSession = $this->createMock(IUserSession::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->themingDefaults = $this->createMock(ThemingDefaults::class);
@@ -84,56 +51,56 @@ class AppleProvisioningPluginTest extends TestCase {
}
);
- $this->sabreRequest = $this->createMock(\Sabre\HTTP\RequestInterface::class);
- $this->sabreResponse = $this->createMock(\Sabre\HTTP\ResponseInterface::class);
+ $this->sabreRequest = $this->createMock(RequestInterface::class);
+ $this->sabreResponse = $this->createMock(ResponseInterface::class);
}
- public function testInitialize() {
- $server = $this->createMock(\Sabre\DAV\Server::class);
+ public function testInitialize(): void {
+ $server = $this->createMock(Server::class);
$plugin = new AppleProvisioningPlugin($this->userSession,
$this->urlGenerator, $this->themingDefaults, $this->request, $this->l10n,
- function () {
+ function (): void {
});
- $server->expects($this->at(0))
+ $server->expects($this->once())
->method('on')
->with('method:GET', [$plugin, 'httpGet'], 90);
$plugin->initialize($server);
}
- public function testHttpGetOnHttp() {
- $this->sabreRequest->expects($this->at(0))
+ public function testHttpGetOnHttp(): void {
+ $this->sabreRequest->expects($this->once())
->method('getPath')
->with()
->willReturn('provisioning/apple-provisioning.mobileconfig');
$user = $this->createMock(IUser::class);
- $this->userSession->expects($this->at(0))
+ $this->userSession->expects($this->once())
->method('getUser')
->willReturn($user);
- $this->request->expects($this->at(0))
+ $this->request->expects($this->once())
->method('getServerProtocol')
->wilLReturn('http');
- $this->themingDefaults->expects($this->at(0))
+ $this->themingDefaults->expects($this->once())
->method('getName')
->willReturn('InstanceName');
- $this->l10n->expects($this->at(0))
+ $this->l10n->expects($this->once())
->method('t')
->with('Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS.', ['InstanceName'])
->willReturn('LocalizedErrorMessage');
- $this->sabreResponse->expects($this->at(0))
+ $this->sabreResponse->expects($this->once())
->method('setStatus')
->with(200);
- $this->sabreResponse->expects($this->at(1))
+ $this->sabreResponse->expects($this->once())
->method('setHeader')
->with('Content-Type', 'text/plain; charset=utf-8');
- $this->sabreResponse->expects($this->at(2))
+ $this->sabreResponse->expects($this->once())
->method('setBody')
->with('LocalizedErrorMessage');
@@ -142,22 +109,22 @@ class AppleProvisioningPluginTest extends TestCase {
$this->assertFalse($returnValue);
}
- public function testHttpGetOnHttps() {
- $this->sabreRequest->expects($this->at(0))
+ public function testHttpGetOnHttps(): void {
+ $this->sabreRequest->expects($this->once())
->method('getPath')
->with()
->willReturn('provisioning/apple-provisioning.mobileconfig');
$user = $this->createMock(IUser::class);
- $user->expects($this->at(0))
+ $user->expects($this->once())
->method('getUID')
->willReturn('userName');
- $this->userSession->expects($this->at(0))
+ $this->userSession->expects($this->once())
->method('getUser')
->willReturn($user);
- $this->request->expects($this->at(0))
+ $this->request->expects($this->once())
->method('getServerProtocol')
->wilLReturn('https');
@@ -165,30 +132,32 @@ class AppleProvisioningPluginTest extends TestCase {
->method('getBaseUrl')
->willReturn('https://nextcloud.tld/nextcloud');
- $this->themingDefaults->expects($this->at(0))
+ $this->themingDefaults->expects($this->once())
->method('getName')
->willReturn('InstanceName');
- $this->l10n->expects($this->at(0))
- ->method('t')
- ->with('Configures a CalDAV account')
- ->willReturn('LocalizedConfiguresCalDAV');
-
- $this->l10n->expects($this->at(1))
+ $this->l10n->expects($this->exactly(2))
->method('t')
- ->with('Configures a CardDAV account')
- ->willReturn('LocalizedConfiguresCardDAV');
+ ->willReturnMap([
+ ['Configures a CalDAV account', [], 'LocalizedConfiguresCalDAV'],
+ ['Configures a CardDAV account', [], 'LocalizedConfiguresCardDAV'],
+ ]);
- $this->sabreResponse->expects($this->at(0))
+ $this->sabreResponse->expects($this->once())
->method('setStatus')
->with(200);
- $this->sabreResponse->expects($this->at(1))
- ->method('setHeader')
- ->with('Content-Disposition', 'attachment; filename="userName-apple-provisioning.mobileconfig"');
- $this->sabreResponse->expects($this->at(2))
+
+ $calls = [
+ ['Content-Disposition', 'attachment; filename="userName-apple-provisioning.mobileconfig"'],
+ ['Content-Type', 'application/xml; charset=utf-8'],
+ ];
+ $this->sabreResponse->expects($this->exactly(2))
->method('setHeader')
- ->with('Content-Type', 'application/xml; charset=utf-8');
- $this->sabreResponse->expects($this->at(3))
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
+ $this->sabreResponse->expects($this->once())
->method('setBody')
->with(<<<EOF
<?xml version="1.0" encoding="UTF-8"?>
@@ -262,7 +231,7 @@ class AppleProvisioningPluginTest extends TestCase {
</plist>
EOF
-);
+ );
$returnValue = $this->plugin->httpGet($this->sabreRequest, $this->sabreResponse);
diff --git a/apps/dav/tests/unit/Search/ContactsSearchProviderTest.php b/apps/dav/tests/unit/Search/ContactsSearchProviderTest.php
index b91612ae5be..f4dc13a5c06 100644
--- a/apps/dav/tests/unit/Search/ContactsSearchProviderTest.php
+++ b/apps/dav/tests/unit/Search/ContactsSearchProviderTest.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.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\Tests\unit;
@@ -35,43 +17,34 @@ use OCP\IUser;
use OCP\Search\ISearchQuery;
use OCP\Search\SearchResult;
use OCP\Search\SearchResultEntry;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\VObject\Reader;
use Test\TestCase;
class ContactsSearchProviderTest extends TestCase {
-
- /** @var IAppManager|\PHPUnit\Framework\MockObject\MockObject */
- private $appManager;
-
- /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
- private $l10n;
-
- /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
- private $urlGenerator;
-
- /** @var CardDavBackend|\PHPUnit\Framework\MockObject\MockObject */
- private $backend;
-
- /** @var ContactsSearchProvider */
- private $provider;
-
- private $vcardTest0 = 'BEGIN:VCARD'.PHP_EOL.
- 'VERSION:3.0'.PHP_EOL.
- 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN'.PHP_EOL.
- 'UID:Test'.PHP_EOL.
- 'FN:FN of Test'.PHP_EOL.
- 'N:Test;;;;'.PHP_EOL.
- 'EMAIL:forrestgump@example.com'.PHP_EOL.
- 'END:VCARD';
-
- private $vcardTest1 = 'BEGIN:VCARD'.PHP_EOL.
- 'VERSION:3.0'.PHP_EOL.
- 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN'.PHP_EOL.
- 'PHOTO;ENCODING=b;TYPE=image/jpeg:'.PHP_EOL.
- 'UID:Test2'.PHP_EOL.
- 'FN:FN of Test2'.PHP_EOL.
- 'N:Test2;;;;'.PHP_EOL.
- 'END:VCARD';
+ private IAppManager&MockObject $appManager;
+ private IL10N&MockObject $l10n;
+ private IURLGenerator&MockObject $urlGenerator;
+ private CardDavBackend&MockObject $backend;
+ private ContactsSearchProvider $provider;
+
+ private string $vcardTest0 = 'BEGIN:VCARD' . PHP_EOL
+ . 'VERSION:3.0' . PHP_EOL
+ . 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN' . PHP_EOL
+ . 'UID:Test' . PHP_EOL
+ . 'FN:FN of Test' . PHP_EOL
+ . 'N:Test;;;;' . PHP_EOL
+ . 'EMAIL:forrestgump@example.com' . PHP_EOL
+ . 'END:VCARD';
+
+ private string $vcardTest1 = 'BEGIN:VCARD' . PHP_EOL
+ . 'VERSION:3.0' . PHP_EOL
+ . 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN' . PHP_EOL
+ . 'PHOTO;ENCODING=b;TYPE=image/jpeg:' . PHP_EOL
+ . 'UID:Test2' . PHP_EOL
+ . 'FN:FN of Test2' . PHP_EOL
+ . 'N:Test2;;;;' . PHP_EOL
+ . 'END:VCARD';
protected function setUp(): void {
parent::setUp();
@@ -159,7 +132,7 @@ class ContactsSearchProviderTest extends TestCase {
]);
$this->backend->expects($this->once())
->method('searchPrincipalUri')
- ->with('principals/users/john.doe', 'search term',
+ ->with('principals/users/john.doe', '',
[
'N',
'FN',
@@ -171,7 +144,7 @@ class ContactsSearchProviderTest extends TestCase {
'ORG',
'NOTE',
],
- ['limit' => 5, 'offset' => 20])
+ ['limit' => 5, 'offset' => 20, 'since' => null, 'until' => null, 'person' => null, 'company' => null])
->willReturn([
[
'addressbookid' => 99,
@@ -192,7 +165,7 @@ class ContactsSearchProviderTest extends TestCase {
$this->urlGenerator,
$this->backend,
])
- ->setMethods([
+ ->onlyMethods([
'getDavUrlForContact',
'getDeepLinkToContactsApp',
'generateSubline',
@@ -209,11 +182,10 @@ class ContactsSearchProviderTest extends TestCase {
->willReturn('subline');
$provider->expects($this->exactly(2))
->method('getDeepLinkToContactsApp')
- ->withConsecutive(
- ['addressbook-uri-99', 'Test'],
- ['addressbook-uri-123', 'Test2']
- )
- ->willReturn('deep-link-to-contacts');
+ ->willReturnMap([
+ ['addressbook-uri-99', 'Test', 'deep-link-to-contacts'],
+ ['addressbook-uri-123', 'Test2', 'deep-link-to-contacts'],
+ ]);
$actual = $provider->search($user, $query);
$data = $actual->jsonSerialize();
diff --git a/apps/dav/tests/unit/Search/EventsSearchProviderTest.php b/apps/dav/tests/unit/Search/EventsSearchProviderTest.php
index a2f4e66fbdb..d5d536fd201 100644
--- a/apps/dav/tests/unit/Search/EventsSearchProviderTest.php
+++ b/apps/dav/tests/unit/Search/EventsSearchProviderTest.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.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\Tests\unit\Search;
@@ -32,208 +14,200 @@ use OCP\App\IAppManager;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUser;
+use OCP\Search\IFilter;
use OCP\Search\ISearchQuery;
use OCP\Search\SearchResult;
use OCP\Search\SearchResultEntry;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\VObject\Reader;
use Test\TestCase;
class EventsSearchProviderTest extends TestCase {
-
- /** @var IAppManager|\PHPUnit\Framework\MockObject\MockObject */
- private $appManager;
-
- /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
- private $l10n;
-
- /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
- private $urlGenerator;
-
- /** @var CalDavBackend|\PHPUnit\Framework\MockObject\MockObject */
- private $backend;
-
- /** @var EventsSearchProvider */
- private $provider;
+ private IAppManager&MockObject $appManager;
+ private IL10N&MockObject $l10n;
+ private IURLGenerator&MockObject $urlGenerator;
+ private CalDavBackend&MockObject $backend;
+ private EventsSearchProvider $provider;
// NO SUMMARY
- private $vEvent0 = 'BEGIN:VCALENDAR'.PHP_EOL.
- 'VERSION:2.0'.PHP_EOL.
- 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN'.PHP_EOL.
- 'CALSCALE:GREGORIAN'.PHP_EOL.
- 'BEGIN:VEVENT'.PHP_EOL.
- 'CREATED:20161004T144433Z'.PHP_EOL.
- 'UID:85560E76-1B0D-47E1-A735-21625767FCA4'.PHP_EOL.
- 'DTEND;VALUE=DATE:20161008'.PHP_EOL.
- 'TRANSP:TRANSPARENT'.PHP_EOL.
- 'DTSTART;VALUE=DATE:20161005'.PHP_EOL.
- 'DTSTAMP:20161004T144437Z'.PHP_EOL.
- 'SEQUENCE:0'.PHP_EOL.
- 'END:VEVENT'.PHP_EOL.
- 'END:VCALENDAR';
+ private static string $vEvent0 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN' . PHP_EOL
+ . 'CALSCALE:GREGORIAN' . PHP_EOL
+ . 'BEGIN:VEVENT' . PHP_EOL
+ . 'CREATED:20161004T144433Z' . PHP_EOL
+ . 'UID:85560E76-1B0D-47E1-A735-21625767FCA4' . PHP_EOL
+ . 'DTEND;VALUE=DATE:20161008' . PHP_EOL
+ . 'TRANSP:TRANSPARENT' . PHP_EOL
+ . 'DTSTART;VALUE=DATE:20161005' . PHP_EOL
+ . 'DTSTAMP:20161004T144437Z' . PHP_EOL
+ . 'SEQUENCE:0' . PHP_EOL
+ . 'END:VEVENT' . PHP_EOL
+ . 'END:VCALENDAR';
// TIMED SAME DAY
- private $vEvent1 = 'BEGIN:VCALENDAR'.PHP_EOL.
- 'VERSION:2.0'.PHP_EOL.
- 'PRODID:-//Tests//'.PHP_EOL.
- 'CALSCALE:GREGORIAN'.PHP_EOL.
- 'BEGIN:VTIMEZONE'.PHP_EOL.
- 'TZID:Europe/Berlin'.PHP_EOL.
- 'BEGIN:DAYLIGHT'.PHP_EOL.
- 'TZOFFSETFROM:+0100'.PHP_EOL.
- 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU'.PHP_EOL.
- 'DTSTART:19810329T020000'.PHP_EOL.
- 'TZNAME:GMT+2'.PHP_EOL.
- 'TZOFFSETTO:+0200'.PHP_EOL.
- 'END:DAYLIGHT'.PHP_EOL.
- 'BEGIN:STANDARD'.PHP_EOL.
- 'TZOFFSETFROM:+0200'.PHP_EOL.
- 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU'.PHP_EOL.
- 'DTSTART:19961027T030000'.PHP_EOL.
- 'TZNAME:GMT+1'.PHP_EOL.
- 'TZOFFSETTO:+0100'.PHP_EOL.
- 'END:STANDARD'.PHP_EOL.
- 'END:VTIMEZONE'.PHP_EOL.
- 'BEGIN:VEVENT'.PHP_EOL.
- 'CREATED:20160809T163629Z'.PHP_EOL.
- 'UID:0AD16F58-01B3-463B-A215-FD09FC729A02'.PHP_EOL.
- 'DTEND;TZID=Europe/Berlin:20160816T100000'.PHP_EOL.
- 'TRANSP:OPAQUE'.PHP_EOL.
- 'SUMMARY:Test Europe Berlin'.PHP_EOL.
- 'DTSTART;TZID=Europe/Berlin:20160816T090000'.PHP_EOL.
- 'DTSTAMP:20160809T163632Z'.PHP_EOL.
- 'SEQUENCE:0'.PHP_EOL.
- 'END:VEVENT'.PHP_EOL.
- 'END:VCALENDAR';
+ private static string $vEvent1 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'PRODID:-//Tests//' . PHP_EOL
+ . 'CALSCALE:GREGORIAN' . PHP_EOL
+ . 'BEGIN:VTIMEZONE' . PHP_EOL
+ . 'TZID:Europe/Berlin' . PHP_EOL
+ . 'BEGIN:DAYLIGHT' . PHP_EOL
+ . 'TZOFFSETFROM:+0100' . PHP_EOL
+ . 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU' . PHP_EOL
+ . 'DTSTART:19810329T020000' . PHP_EOL
+ . 'TZNAME:GMT+2' . PHP_EOL
+ . 'TZOFFSETTO:+0200' . PHP_EOL
+ . 'END:DAYLIGHT' . PHP_EOL
+ . 'BEGIN:STANDARD' . PHP_EOL
+ . 'TZOFFSETFROM:+0200' . PHP_EOL
+ . 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU' . PHP_EOL
+ . 'DTSTART:19961027T030000' . PHP_EOL
+ . 'TZNAME:GMT+1' . PHP_EOL
+ . 'TZOFFSETTO:+0100' . PHP_EOL
+ . 'END:STANDARD' . PHP_EOL
+ . 'END:VTIMEZONE' . PHP_EOL
+ . 'BEGIN:VEVENT' . PHP_EOL
+ . 'CREATED:20160809T163629Z' . PHP_EOL
+ . 'UID:0AD16F58-01B3-463B-A215-FD09FC729A02' . PHP_EOL
+ . 'DTEND;TZID=Europe/Berlin:20160816T100000' . PHP_EOL
+ . 'TRANSP:OPAQUE' . PHP_EOL
+ . 'SUMMARY:Test Europe Berlin' . PHP_EOL
+ . 'DTSTART;TZID=Europe/Berlin:20160816T090000' . PHP_EOL
+ . 'DTSTAMP:20160809T163632Z' . PHP_EOL
+ . 'SEQUENCE:0' . PHP_EOL
+ . 'END:VEVENT' . PHP_EOL
+ . 'END:VCALENDAR';
// TIMED DIFFERENT DAY
- private $vEvent2 = 'BEGIN:VCALENDAR'.PHP_EOL.
- 'VERSION:2.0'.PHP_EOL.
- 'PRODID:-//Tests//'.PHP_EOL.
- 'CALSCALE:GREGORIAN'.PHP_EOL.
- 'BEGIN:VTIMEZONE'.PHP_EOL.
- 'TZID:Europe/Berlin'.PHP_EOL.
- 'BEGIN:DAYLIGHT'.PHP_EOL.
- 'TZOFFSETFROM:+0100'.PHP_EOL.
- 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU'.PHP_EOL.
- 'DTSTART:19810329T020000'.PHP_EOL.
- 'TZNAME:GMT+2'.PHP_EOL.
- 'TZOFFSETTO:+0200'.PHP_EOL.
- 'END:DAYLIGHT'.PHP_EOL.
- 'BEGIN:STANDARD'.PHP_EOL.
- 'TZOFFSETFROM:+0200'.PHP_EOL.
- 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU'.PHP_EOL.
- 'DTSTART:19961027T030000'.PHP_EOL.
- 'TZNAME:GMT+1'.PHP_EOL.
- 'TZOFFSETTO:+0100'.PHP_EOL.
- 'END:STANDARD'.PHP_EOL.
- 'END:VTIMEZONE'.PHP_EOL.
- 'BEGIN:VEVENT'.PHP_EOL.
- 'CREATED:20160809T163629Z'.PHP_EOL.
- 'UID:0AD16F58-01B3-463B-A215-FD09FC729A02'.PHP_EOL.
- 'DTEND;TZID=Europe/Berlin:20160817T100000'.PHP_EOL.
- 'TRANSP:OPAQUE'.PHP_EOL.
- 'SUMMARY:Test Europe Berlin'.PHP_EOL.
- 'DTSTART;TZID=Europe/Berlin:20160816T090000'.PHP_EOL.
- 'DTSTAMP:20160809T163632Z'.PHP_EOL.
- 'SEQUENCE:0'.PHP_EOL.
- 'END:VEVENT'.PHP_EOL.
- 'END:VCALENDAR';
+ private static string $vEvent2 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'PRODID:-//Tests//' . PHP_EOL
+ . 'CALSCALE:GREGORIAN' . PHP_EOL
+ . 'BEGIN:VTIMEZONE' . PHP_EOL
+ . 'TZID:Europe/Berlin' . PHP_EOL
+ . 'BEGIN:DAYLIGHT' . PHP_EOL
+ . 'TZOFFSETFROM:+0100' . PHP_EOL
+ . 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU' . PHP_EOL
+ . 'DTSTART:19810329T020000' . PHP_EOL
+ . 'TZNAME:GMT+2' . PHP_EOL
+ . 'TZOFFSETTO:+0200' . PHP_EOL
+ . 'END:DAYLIGHT' . PHP_EOL
+ . 'BEGIN:STANDARD' . PHP_EOL
+ . 'TZOFFSETFROM:+0200' . PHP_EOL
+ . 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU' . PHP_EOL
+ . 'DTSTART:19961027T030000' . PHP_EOL
+ . 'TZNAME:GMT+1' . PHP_EOL
+ . 'TZOFFSETTO:+0100' . PHP_EOL
+ . 'END:STANDARD' . PHP_EOL
+ . 'END:VTIMEZONE' . PHP_EOL
+ . 'BEGIN:VEVENT' . PHP_EOL
+ . 'CREATED:20160809T163629Z' . PHP_EOL
+ . 'UID:0AD16F58-01B3-463B-A215-FD09FC729A02' . PHP_EOL
+ . 'DTEND;TZID=Europe/Berlin:20160817T100000' . PHP_EOL
+ . 'TRANSP:OPAQUE' . PHP_EOL
+ . 'SUMMARY:Test Europe Berlin' . PHP_EOL
+ . 'DTSTART;TZID=Europe/Berlin:20160816T090000' . PHP_EOL
+ . 'DTSTAMP:20160809T163632Z' . PHP_EOL
+ . 'SEQUENCE:0' . PHP_EOL
+ . 'END:VEVENT' . PHP_EOL
+ . 'END:VCALENDAR';
// ALL-DAY ONE-DAY
- private $vEvent3 = 'BEGIN:VCALENDAR'.PHP_EOL.
- 'VERSION:2.0'.PHP_EOL.
- 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN'.PHP_EOL.
- 'CALSCALE:GREGORIAN'.PHP_EOL.
- 'BEGIN:VEVENT'.PHP_EOL.
- 'CREATED:20161004T144433Z'.PHP_EOL.
- 'UID:85560E76-1B0D-47E1-A735-21625767FCA4'.PHP_EOL.
- 'DTEND;VALUE=DATE:20161006'.PHP_EOL.
- 'TRANSP:TRANSPARENT'.PHP_EOL.
- 'DTSTART;VALUE=DATE:20161005'.PHP_EOL.
- 'DTSTAMP:20161004T144437Z'.PHP_EOL.
- 'SEQUENCE:0'.PHP_EOL.
- 'END:VEVENT'.PHP_EOL.
- 'END:VCALENDAR';
+ private static string $vEvent3 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN' . PHP_EOL
+ . 'CALSCALE:GREGORIAN' . PHP_EOL
+ . 'BEGIN:VEVENT' . PHP_EOL
+ . 'CREATED:20161004T144433Z' . PHP_EOL
+ . 'UID:85560E76-1B0D-47E1-A735-21625767FCA4' . PHP_EOL
+ . 'DTEND;VALUE=DATE:20161006' . PHP_EOL
+ . 'TRANSP:TRANSPARENT' . PHP_EOL
+ . 'DTSTART;VALUE=DATE:20161005' . PHP_EOL
+ . 'DTSTAMP:20161004T144437Z' . PHP_EOL
+ . 'SEQUENCE:0' . PHP_EOL
+ . 'END:VEVENT' . PHP_EOL
+ . 'END:VCALENDAR';
// ALL-DAY MULTIPLE DAYS
- private $vEvent4 = 'BEGIN:VCALENDAR'.PHP_EOL.
- 'VERSION:2.0'.PHP_EOL.
- 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN'.PHP_EOL.
- 'CALSCALE:GREGORIAN'.PHP_EOL.
- 'BEGIN:VEVENT'.PHP_EOL.
- 'CREATED:20161004T144433Z'.PHP_EOL.
- 'UID:85560E76-1B0D-47E1-A735-21625767FCA4'.PHP_EOL.
- 'DTEND;VALUE=DATE:20161008'.PHP_EOL.
- 'TRANSP:TRANSPARENT'.PHP_EOL.
- 'DTSTART;VALUE=DATE:20161005'.PHP_EOL.
- 'DTSTAMP:20161004T144437Z'.PHP_EOL.
- 'SEQUENCE:0'.PHP_EOL.
- 'END:VEVENT'.PHP_EOL.
- 'END:VCALENDAR';
+ private static string $vEvent4 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN' . PHP_EOL
+ . 'CALSCALE:GREGORIAN' . PHP_EOL
+ . 'BEGIN:VEVENT' . PHP_EOL
+ . 'CREATED:20161004T144433Z' . PHP_EOL
+ . 'UID:85560E76-1B0D-47E1-A735-21625767FCA4' . PHP_EOL
+ . 'DTEND;VALUE=DATE:20161008' . PHP_EOL
+ . 'TRANSP:TRANSPARENT' . PHP_EOL
+ . 'DTSTART;VALUE=DATE:20161005' . PHP_EOL
+ . 'DTSTAMP:20161004T144437Z' . PHP_EOL
+ . 'SEQUENCE:0' . PHP_EOL
+ . 'END:VEVENT' . PHP_EOL
+ . 'END:VCALENDAR';
// DURATION
- private $vEvent5 = 'BEGIN:VCALENDAR'.PHP_EOL.
- 'VERSION:2.0'.PHP_EOL.
- 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN'.PHP_EOL.
- 'CALSCALE:GREGORIAN'.PHP_EOL.
- 'BEGIN:VEVENT'.PHP_EOL.
- 'CREATED:20161004T144433Z'.PHP_EOL.
- 'UID:85560E76-1B0D-47E1-A735-21625767FCA4'.PHP_EOL.
- 'DURATION:P5D'.PHP_EOL.
- 'TRANSP:TRANSPARENT'.PHP_EOL.
- 'DTSTART;VALUE=DATE:20161005'.PHP_EOL.
- 'DTSTAMP:20161004T144437Z'.PHP_EOL.
- 'SEQUENCE:0'.PHP_EOL.
- 'END:VEVENT'.PHP_EOL.
- 'END:VCALENDAR';
+ private static string $vEvent5 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN' . PHP_EOL
+ . 'CALSCALE:GREGORIAN' . PHP_EOL
+ . 'BEGIN:VEVENT' . PHP_EOL
+ . 'CREATED:20161004T144433Z' . PHP_EOL
+ . 'UID:85560E76-1B0D-47E1-A735-21625767FCA4' . PHP_EOL
+ . 'DURATION:P5D' . PHP_EOL
+ . 'TRANSP:TRANSPARENT' . PHP_EOL
+ . 'DTSTART;VALUE=DATE:20161005' . PHP_EOL
+ . 'DTSTAMP:20161004T144437Z' . PHP_EOL
+ . 'SEQUENCE:0' . PHP_EOL
+ . 'END:VEVENT' . PHP_EOL
+ . 'END:VCALENDAR';
// NO DTEND - DATE
- private $vEvent6 = 'BEGIN:VCALENDAR'.PHP_EOL.
- 'VERSION:2.0'.PHP_EOL.
- 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN'.PHP_EOL.
- 'CALSCALE:GREGORIAN'.PHP_EOL.
- 'BEGIN:VEVENT'.PHP_EOL.
- 'CREATED:20161004T144433Z'.PHP_EOL.
- 'UID:85560E76-1B0D-47E1-A735-21625767FCA4'.PHP_EOL.
- 'TRANSP:TRANSPARENT'.PHP_EOL.
- 'DTSTART;VALUE=DATE:20161005'.PHP_EOL.
- 'DTSTAMP:20161004T144437Z'.PHP_EOL.
- 'SEQUENCE:0'.PHP_EOL.
- 'END:VEVENT'.PHP_EOL.
- 'END:VCALENDAR';
+ private static string $vEvent6 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN' . PHP_EOL
+ . 'CALSCALE:GREGORIAN' . PHP_EOL
+ . 'BEGIN:VEVENT' . PHP_EOL
+ . 'CREATED:20161004T144433Z' . PHP_EOL
+ . 'UID:85560E76-1B0D-47E1-A735-21625767FCA4' . PHP_EOL
+ . 'TRANSP:TRANSPARENT' . PHP_EOL
+ . 'DTSTART;VALUE=DATE:20161005' . PHP_EOL
+ . 'DTSTAMP:20161004T144437Z' . PHP_EOL
+ . 'SEQUENCE:0' . PHP_EOL
+ . 'END:VEVENT' . PHP_EOL
+ . 'END:VCALENDAR';
// NO DTEND - DATE-TIME
- private $vEvent7 = 'BEGIN:VCALENDAR'.PHP_EOL.
- 'VERSION:2.0'.PHP_EOL.
- 'PRODID:-//Tests//'.PHP_EOL.
- 'CALSCALE:GREGORIAN'.PHP_EOL.
- 'BEGIN:VTIMEZONE'.PHP_EOL.
- 'TZID:Europe/Berlin'.PHP_EOL.
- 'BEGIN:DAYLIGHT'.PHP_EOL.
- 'TZOFFSETFROM:+0100'.PHP_EOL.
- 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU'.PHP_EOL.
- 'DTSTART:19810329T020000'.PHP_EOL.
- 'TZNAME:GMT+2'.PHP_EOL.
- 'TZOFFSETTO:+0200'.PHP_EOL.
- 'END:DAYLIGHT'.PHP_EOL.
- 'BEGIN:STANDARD'.PHP_EOL.
- 'TZOFFSETFROM:+0200'.PHP_EOL.
- 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU'.PHP_EOL.
- 'DTSTART:19961027T030000'.PHP_EOL.
- 'TZNAME:GMT+1'.PHP_EOL.
- 'TZOFFSETTO:+0100'.PHP_EOL.
- 'END:STANDARD'.PHP_EOL.
- 'END:VTIMEZONE'.PHP_EOL.
- 'BEGIN:VEVENT'.PHP_EOL.
- 'CREATED:20160809T163629Z'.PHP_EOL.
- 'UID:0AD16F58-01B3-463B-A215-FD09FC729A02'.PHP_EOL.
- 'TRANSP:OPAQUE'.PHP_EOL.
- 'SUMMARY:Test Europe Berlin'.PHP_EOL.
- 'DTSTART;TZID=Europe/Berlin:20160816T090000'.PHP_EOL.
- 'DTSTAMP:20160809T163632Z'.PHP_EOL.
- 'SEQUENCE:0'.PHP_EOL.
- 'END:VEVENT'.PHP_EOL.
- 'END:VCALENDAR';
+ private static string $vEvent7 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'PRODID:-//Tests//' . PHP_EOL
+ . 'CALSCALE:GREGORIAN' . PHP_EOL
+ . 'BEGIN:VTIMEZONE' . PHP_EOL
+ . 'TZID:Europe/Berlin' . PHP_EOL
+ . 'BEGIN:DAYLIGHT' . PHP_EOL
+ . 'TZOFFSETFROM:+0100' . PHP_EOL
+ . 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU' . PHP_EOL
+ . 'DTSTART:19810329T020000' . PHP_EOL
+ . 'TZNAME:GMT+2' . PHP_EOL
+ . 'TZOFFSETTO:+0200' . PHP_EOL
+ . 'END:DAYLIGHT' . PHP_EOL
+ . 'BEGIN:STANDARD' . PHP_EOL
+ . 'TZOFFSETFROM:+0200' . PHP_EOL
+ . 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU' . PHP_EOL
+ . 'DTSTART:19961027T030000' . PHP_EOL
+ . 'TZNAME:GMT+1' . PHP_EOL
+ . 'TZOFFSETTO:+0100' . PHP_EOL
+ . 'END:STANDARD' . PHP_EOL
+ . 'END:VTIMEZONE' . PHP_EOL
+ . 'BEGIN:VEVENT' . PHP_EOL
+ . 'CREATED:20160809T163629Z' . PHP_EOL
+ . 'UID:0AD16F58-01B3-463B-A215-FD09FC729A02' . PHP_EOL
+ . 'TRANSP:OPAQUE' . PHP_EOL
+ . 'SUMMARY:Test Europe Berlin' . PHP_EOL
+ . 'DTSTART;TZID=Europe/Berlin:20160816T090000' . PHP_EOL
+ . 'DTSTAMP:20160809T163632Z' . PHP_EOL
+ . 'SEQUENCE:0' . PHP_EOL
+ . 'END:VEVENT' . PHP_EOL
+ . 'END:VCALENDAR';
protected function setUp(): void {
parent::setUp();
@@ -294,7 +268,14 @@ class EventsSearchProviderTest extends TestCase {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('john.doe');
$query = $this->createMock(ISearchQuery::class);
- $query->method('getTerm')->willReturn('search term');
+ $seachTermFilter = $this->createMock(IFilter::class);
+ $query->method('getFilter')->willReturnCallback(function ($name) use ($seachTermFilter) {
+ return match ($name) {
+ 'term' => $seachTermFilter,
+ default => null,
+ };
+ });
+ $seachTermFilter->method('get')->willReturn('search term');
$query->method('getLimit')->willReturn(5);
$query->method('getCursor')->willReturn(20);
$this->appManager->expects($this->once())
@@ -332,25 +313,25 @@ class EventsSearchProviderTest extends TestCase {
->with('principals/users/john.doe', 'search term', ['VEVENT'],
['SUMMARY', 'LOCATION', 'DESCRIPTION', 'ATTENDEE', 'ORGANIZER', 'CATEGORIES'],
['ATTENDEE' => ['CN'], 'ORGANIZER' => ['CN']],
- ['limit' => 5, 'offset' => 20])
+ ['limit' => 5, 'offset' => 20, 'timerange' => ['start' => null, 'end' => null]])
->willReturn([
[
'calendarid' => 99,
'calendartype' => CalDavBackend::CALENDAR_TYPE_CALENDAR,
'uri' => 'event0.ics',
- 'calendardata' => $this->vEvent0,
+ 'calendardata' => self::$vEvent0,
],
[
'calendarid' => 123,
'calendartype' => CalDavBackend::CALENDAR_TYPE_CALENDAR,
'uri' => 'event1.ics',
- 'calendardata' => $this->vEvent1,
+ 'calendardata' => self::$vEvent1,
],
[
'calendarid' => 1337,
'calendartype' => CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION,
'uri' => 'event2.ics',
- 'calendardata' => $this->vEvent2,
+ 'calendardata' => self::$vEvent2,
]
]);
@@ -361,7 +342,7 @@ class EventsSearchProviderTest extends TestCase {
$this->urlGenerator,
$this->backend,
])
- ->setMethods([
+ ->onlyMethods([
'getDeepLinkToCalendarApp',
'generateSubline',
])
@@ -372,12 +353,11 @@ class EventsSearchProviderTest extends TestCase {
->willReturn('subline');
$provider->expects($this->exactly(3))
->method('getDeepLinkToCalendarApp')
- ->withConsecutive(
- ['principals/users/john.doe', 'calendar-uri-99', 'event0.ics'],
- ['principals/users/john.doe', 'calendar-uri-123', 'event1.ics'],
- ['principals/users/john.doe', 'subscription-uri-1337', 'event2.ics']
- )
- ->willReturn('deep-link-to-calendar');
+ ->willReturnMap([
+ ['principals/users/john.doe', 'calendar-uri-99', 'event0.ics', 'deep-link-to-calendar'],
+ ['principals/users/john.doe', 'calendar-uri-123', 'event1.ics', 'deep-link-to-calendar'],
+ ['principals/users/john.doe', 'subscription-uri-1337', 'event2.ics', 'deep-link-to-calendar']
+ ]);
$actual = $provider->search($user, $query);
$data = $actual->jsonSerialize();
@@ -420,15 +400,15 @@ class EventsSearchProviderTest extends TestCase {
}
public function testGetDeepLinkToCalendarApp(): void {
- $this->urlGenerator->expects($this->at(0))
+ $this->urlGenerator->expects($this->once())
->method('linkTo')
->with('', 'remote.php')
->willReturn('link-to-remote.php');
- $this->urlGenerator->expects($this->at(1))
+ $this->urlGenerator->expects($this->once())
->method('linkToRoute')
->with('calendar.view.index')
->willReturn('link-to-route-calendar/');
- $this->urlGenerator->expects($this->at(2))
+ $this->urlGenerator->expects($this->once())
->method('getAbsoluteURL')
->with('link-to-route-calendar/edit/bGluay10by1yZW1vdGUucGhwL2Rhdi9jYWxlbmRhcnMvam9obi5kb2UvZm9vL2Jhci5pY3M=')
->willReturn('absolute-url-to-route');
@@ -438,12 +418,7 @@ class EventsSearchProviderTest extends TestCase {
$this->assertEquals('absolute-url-to-route', $actual);
}
- /**
- * @param string $ics
- * @param string $expectedSubline
- *
- * @dataProvider generateSublineDataProvider
- */
+ #[\PHPUnit\Framework\Attributes\DataProvider('generateSublineDataProvider')]
public function testGenerateSubline(string $ics, string $expectedSubline): void {
$vCalendar = Reader::read($ics, Reader::OPTION_FORGIVING);
$eventComponent = $vCalendar->VEVENT;
@@ -461,15 +436,15 @@ class EventsSearchProviderTest extends TestCase {
$this->assertEquals($expectedSubline, $actual);
}
- public function generateSublineDataProvider(): array {
+ public static function generateSublineDataProvider(): array {
return [
- [$this->vEvent1, '08-16 09:00 - 10:00'],
- [$this->vEvent2, '08-16 09:00 - 08-17 10:00'],
- [$this->vEvent3, '10-05'],
- [$this->vEvent4, '10-05 - 10-07'],
- [$this->vEvent5, '10-05 - 10-09'],
- [$this->vEvent6, '10-05'],
- [$this->vEvent7, '08-16 09:00 - 09:00'],
+ [self::$vEvent1, '08-16 09:00 - 10:00'],
+ [self::$vEvent2, '08-16 09:00 - 08-17 10:00'],
+ [self::$vEvent3, '10-05'],
+ [self::$vEvent4, '10-05 - 10-07'],
+ [self::$vEvent5, '10-05 - 10-09'],
+ [self::$vEvent6, '10-05'],
+ [self::$vEvent7, '08-16 09:00 - 09:00'],
];
}
}
diff --git a/apps/dav/tests/unit/Search/TasksSearchProviderTest.php b/apps/dav/tests/unit/Search/TasksSearchProviderTest.php
index 13dc02bb278..7f9a2842de9 100644
--- a/apps/dav/tests/unit/Search/TasksSearchProviderTest.php
+++ b/apps/dav/tests/unit/Search/TasksSearchProviderTest.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.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\Tests\unit\Search;
@@ -35,89 +17,80 @@ use OCP\IUser;
use OCP\Search\ISearchQuery;
use OCP\Search\SearchResult;
use OCP\Search\SearchResultEntry;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\VObject\Reader;
use Test\TestCase;
class TasksSearchProviderTest extends TestCase {
-
- /** @var IAppManager|\PHPUnit\Framework\MockObject\MockObject */
- private $appManager;
-
- /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
- private $l10n;
-
- /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
- private $urlGenerator;
-
- /** @var CalDavBackend|\PHPUnit\Framework\MockObject\MockObject */
- private $backend;
-
- /** @var TasksSearchProvider */
- private $provider;
+ private IAppManager&MockObject $appManager;
+ private IL10N&MockObject $l10n;
+ private IURLGenerator&MockObject $urlGenerator;
+ private CalDavBackend&MockObject $backend;
+ private TasksSearchProvider $provider;
// NO DUE NOR COMPLETED NOR SUMMARY
- private $vTodo0 = 'BEGIN:VCALENDAR'.PHP_EOL.
- 'PRODID:TEST'.PHP_EOL.
- 'VERSION:2.0'.PHP_EOL.
- 'BEGIN:VTODO'.PHP_EOL.
- 'UID:20070313T123432Z-456553@example.com'.PHP_EOL.
- 'DTSTAMP:20070313T123432Z'.PHP_EOL.
- 'STATUS:NEEDS-ACTION'.PHP_EOL.
- 'END:VTODO'.PHP_EOL.
- 'END:VCALENDAR';
+ private static string $vTodo0 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'PRODID:TEST' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'BEGIN:VTODO' . PHP_EOL
+ . 'UID:20070313T123432Z-456553@example.com' . PHP_EOL
+ . 'DTSTAMP:20070313T123432Z' . PHP_EOL
+ . 'STATUS:NEEDS-ACTION' . PHP_EOL
+ . 'END:VTODO' . PHP_EOL
+ . 'END:VCALENDAR';
// DUE AND COMPLETED
- private $vTodo1 = 'BEGIN:VCALENDAR'.PHP_EOL.
- 'PRODID:TEST'.PHP_EOL.
- 'VERSION:2.0'.PHP_EOL.
- 'BEGIN:VTODO'.PHP_EOL.
- 'UID:20070313T123432Z-456553@example.com'.PHP_EOL.
- 'DTSTAMP:20070313T123432Z'.PHP_EOL.
- 'COMPLETED:20070707T100000Z'.PHP_EOL.
- 'DUE;VALUE=DATE:20070501'.PHP_EOL.
- 'SUMMARY:Task title'.PHP_EOL.
- 'STATUS:NEEDS-ACTION'.PHP_EOL.
- 'END:VTODO'.PHP_EOL.
- 'END:VCALENDAR';
+ private static string $vTodo1 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'PRODID:TEST' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'BEGIN:VTODO' . PHP_EOL
+ . 'UID:20070313T123432Z-456553@example.com' . PHP_EOL
+ . 'DTSTAMP:20070313T123432Z' . PHP_EOL
+ . 'COMPLETED:20070707T100000Z' . PHP_EOL
+ . 'DUE;VALUE=DATE:20070501' . PHP_EOL
+ . 'SUMMARY:Task title' . PHP_EOL
+ . 'STATUS:NEEDS-ACTION' . PHP_EOL
+ . 'END:VTODO' . PHP_EOL
+ . 'END:VCALENDAR';
// COMPLETED ONLY
- private $vTodo2 = 'BEGIN:VCALENDAR'.PHP_EOL.
- 'PRODID:TEST'.PHP_EOL.
- 'VERSION:2.0'.PHP_EOL.
- 'BEGIN:VTODO'.PHP_EOL.
- 'UID:20070313T123432Z-456553@example.com'.PHP_EOL.
- 'DTSTAMP:20070313T123432Z'.PHP_EOL.
- 'COMPLETED:20070707T100000Z'.PHP_EOL.
- 'SUMMARY:Task title'.PHP_EOL.
- 'STATUS:NEEDS-ACTION'.PHP_EOL.
- 'END:VTODO'.PHP_EOL.
- 'END:VCALENDAR';
+ private static string $vTodo2 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'PRODID:TEST' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'BEGIN:VTODO' . PHP_EOL
+ . 'UID:20070313T123432Z-456553@example.com' . PHP_EOL
+ . 'DTSTAMP:20070313T123432Z' . PHP_EOL
+ . 'COMPLETED:20070707T100000Z' . PHP_EOL
+ . 'SUMMARY:Task title' . PHP_EOL
+ . 'STATUS:NEEDS-ACTION' . PHP_EOL
+ . 'END:VTODO' . PHP_EOL
+ . 'END:VCALENDAR';
// DUE DATE
- private $vTodo3 = 'BEGIN:VCALENDAR'.PHP_EOL.
- 'PRODID:TEST'.PHP_EOL.
- 'VERSION:2.0'.PHP_EOL.
- 'BEGIN:VTODO'.PHP_EOL.
- 'UID:20070313T123432Z-456553@example.com'.PHP_EOL.
- 'DTSTAMP:20070313T123432Z'.PHP_EOL.
- 'DUE;VALUE=DATE:20070501'.PHP_EOL.
- 'SUMMARY:Task title'.PHP_EOL.
- 'STATUS:NEEDS-ACTION'.PHP_EOL.
- 'END:VTODO'.PHP_EOL.
- 'END:VCALENDAR';
+ private static string $vTodo3 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'PRODID:TEST' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'BEGIN:VTODO' . PHP_EOL
+ . 'UID:20070313T123432Z-456553@example.com' . PHP_EOL
+ . 'DTSTAMP:20070313T123432Z' . PHP_EOL
+ . 'DUE;VALUE=DATE:20070501' . PHP_EOL
+ . 'SUMMARY:Task title' . PHP_EOL
+ . 'STATUS:NEEDS-ACTION' . PHP_EOL
+ . 'END:VTODO' . PHP_EOL
+ . 'END:VCALENDAR';
// DUE DATETIME
- private $vTodo4 = 'BEGIN:VCALENDAR'.PHP_EOL.
- 'PRODID:TEST'.PHP_EOL.
- 'VERSION:2.0'.PHP_EOL.
- 'BEGIN:VTODO'.PHP_EOL.
- 'UID:20070313T123432Z-456553@example.com'.PHP_EOL.
- 'DTSTAMP:20070313T123432Z'.PHP_EOL.
- 'DUE:20070709T130000Z'.PHP_EOL.
- 'SUMMARY:Task title'.PHP_EOL.
- 'STATUS:NEEDS-ACTION'.PHP_EOL.
- 'END:VTODO'.PHP_EOL.
- 'END:VCALENDAR';
+ private static string $vTodo4 = 'BEGIN:VCALENDAR' . PHP_EOL
+ . 'PRODID:TEST' . PHP_EOL
+ . 'VERSION:2.0' . PHP_EOL
+ . 'BEGIN:VTODO' . PHP_EOL
+ . 'UID:20070313T123432Z-456553@example.com' . PHP_EOL
+ . 'DTSTAMP:20070313T123432Z' . PHP_EOL
+ . 'DUE:20070709T130000Z' . PHP_EOL
+ . 'SUMMARY:Task title' . PHP_EOL
+ . 'STATUS:NEEDS-ACTION' . PHP_EOL
+ . 'END:VTODO' . PHP_EOL
+ . 'END:VCALENDAR';
protected function setUp(): void {
parent::setUp();
@@ -213,28 +186,28 @@ class TasksSearchProviderTest extends TestCase {
]);
$this->backend->expects($this->once())
->method('searchPrincipalUri')
- ->with('principals/users/john.doe', 'search term', ['VTODO'],
+ ->with('principals/users/john.doe', '', ['VTODO'],
['SUMMARY', 'DESCRIPTION', 'CATEGORIES'],
[],
- ['limit' => 5, 'offset' => 20])
+ ['limit' => 5, 'offset' => 20, 'since' => null, 'until' => null])
->willReturn([
[
'calendarid' => 99,
'calendartype' => CalDavBackend::CALENDAR_TYPE_CALENDAR,
'uri' => 'todo0.ics',
- 'calendardata' => $this->vTodo0,
+ 'calendardata' => self::$vTodo0,
],
[
'calendarid' => 123,
'calendartype' => CalDavBackend::CALENDAR_TYPE_CALENDAR,
'uri' => 'todo1.ics',
- 'calendardata' => $this->vTodo1,
+ 'calendardata' => self::$vTodo1,
],
[
'calendarid' => 1337,
'calendartype' => CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION,
'uri' => 'todo2.ics',
- 'calendardata' => $this->vTodo2,
+ 'calendardata' => self::$vTodo2,
]
]);
@@ -245,7 +218,7 @@ class TasksSearchProviderTest extends TestCase {
$this->urlGenerator,
$this->backend,
])
- ->setMethods([
+ ->onlyMethods([
'getDeepLinkToTasksApp',
'generateSubline',
])
@@ -256,12 +229,11 @@ class TasksSearchProviderTest extends TestCase {
->willReturn('subline');
$provider->expects($this->exactly(3))
->method('getDeepLinkToTasksApp')
- ->withConsecutive(
- ['calendar-uri-99', 'todo0.ics'],
- ['calendar-uri-123', 'todo1.ics'],
- ['subscription-uri-1337', 'todo2.ics']
- )
- ->willReturn('deep-link-to-tasks');
+ ->willReturnMap([
+ ['calendar-uri-99', 'todo0.ics', 'deep-link-to-tasks'],
+ ['calendar-uri-123', 'todo1.ics', 'deep-link-to-tasks'],
+ ['subscription-uri-1337', 'todo2.ics', 'deep-link-to-tasks']
+ ]);
$actual = $provider->search($user, $query);
$data = $actual->jsonSerialize();
@@ -310,37 +282,32 @@ class TasksSearchProviderTest extends TestCase {
->willReturn('link-to-route-tasks.index');
$this->urlGenerator->expects($this->once())
->method('getAbsoluteURL')
- ->with('link-to-route-tasks.index#/calendars/uri-john.doe/tasks/task-uri.ics')
- ->willReturn('absolute-url-link-to-route-tasks.index#/calendars/uri-john.doe/tasks/task-uri.ics');
+ ->with('link-to-route-tasks.indexcalendars/uri-john.doe/tasks/task-uri.ics')
+ ->willReturn('absolute-url-link-to-route-tasks.indexcalendars/uri-john.doe/tasks/task-uri.ics');
$actual = self::invokePrivate($this->provider, 'getDeepLinkToTasksApp', ['uri-john.doe', 'task-uri.ics']);
- $this->assertEquals('absolute-url-link-to-route-tasks.index#/calendars/uri-john.doe/tasks/task-uri.ics', $actual);
+ $this->assertEquals('absolute-url-link-to-route-tasks.indexcalendars/uri-john.doe/tasks/task-uri.ics', $actual);
}
- /**
- * @param string $ics
- * @param string $expectedSubline
- *
- * @dataProvider generateSublineDataProvider
- */
+ #[\PHPUnit\Framework\Attributes\DataProvider('generateSublineDataProvider')]
public function testGenerateSubline(string $ics, string $expectedSubline): void {
$vCalendar = Reader::read($ics, Reader::OPTION_FORGIVING);
$taskComponent = $vCalendar->VTODO;
$this->l10n->method('t')->willReturnArgument(0);
- $this->l10n->method('l')->willReturnArgument('');
+ $this->l10n->method('l')->willReturnArgument(0);
$actual = self::invokePrivate($this->provider, 'generateSubline', [$taskComponent]);
$this->assertEquals($expectedSubline, $actual);
}
- public function generateSublineDataProvider(): array {
+ public static function generateSublineDataProvider(): array {
return [
- [$this->vTodo0, ''],
- [$this->vTodo1, 'Completed on %s'],
- [$this->vTodo2, 'Completed on %s'],
- [$this->vTodo3, 'Due on %s'],
- [$this->vTodo4, 'Due on %s by %s'],
+ [self::$vTodo0, ''],
+ [self::$vTodo1, 'Completed on %s'],
+ [self::$vTodo2, 'Completed on %s'],
+ [self::$vTodo3, 'Due on %s'],
+ [self::$vTodo4, 'Due on %s by %s'],
];
}
}
diff --git a/apps/dav/tests/unit/ServerTest.php b/apps/dav/tests/unit/ServerTest.php
index 8cdd52f5745..9ffe86d3053 100644
--- a/apps/dav/tests/unit/ServerTest.php
+++ b/apps/dav/tests/unit/ServerTest.php
@@ -1,28 +1,10 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @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 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\Tests\unit;
@@ -38,20 +20,19 @@ use OCP\IRequest;
*/
class ServerTest extends \Test\TestCase {
- /**
- * @dataProvider providesUris
- */
- public function test($uri, array $plugins) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesUris')]
+ public function test(string $uri, array $plugins): void {
/** @var IRequest | \PHPUnit\Framework\MockObject\MockObject $r */
$r = $this->createMock(IRequest::class);
$r->expects($this->any())->method('getRequestUri')->willReturn($uri);
+ $this->loginAsUser('admin');
$s = new Server($r, '/');
$this->assertNotNull($s->server);
foreach ($plugins as $plugin) {
$this->assertNotNull($s->server->getPlugin($plugin));
}
}
- public function providesUris() {
+ public static function providesUris(): array {
return [
'principals' => ['principals/users/admin', ['caldav', 'oc-resource-sharing', 'carddav']],
'calendars' => ['calendars/admin', ['caldav', 'oc-resource-sharing']],
diff --git a/apps/dav/tests/unit/Service/AbsenceServiceTest.php b/apps/dav/tests/unit/Service/AbsenceServiceTest.php
new file mode 100644
index 00000000000..c16c715d5c2
--- /dev/null
+++ b/apps/dav/tests/unit/Service/AbsenceServiceTest.php
@@ -0,0 +1,445 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\Service;
+
+use DateTimeImmutable;
+use DateTimeZone;
+use OCA\DAV\BackgroundJob\OutOfOfficeEventDispatcherJob;
+use OCA\DAV\CalDAV\TimezoneService;
+use OCA\DAV\Db\Absence;
+use OCA\DAV\Db\AbsenceMapper;
+use OCA\DAV\Service\AbsenceService;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IUser;
+use OCP\User\Events\OutOfOfficeChangedEvent;
+use OCP\User\Events\OutOfOfficeScheduledEvent;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+class AbsenceServiceTest extends TestCase {
+ private AbsenceService $absenceService;
+ private AbsenceMapper&MockObject $absenceMapper;
+ private IEventDispatcher&MockObject $eventDispatcher;
+ private IJobList&MockObject $jobList;
+ private TimezoneService&MockObject $timezoneService;
+ private ITimeFactory&MockObject $timeFactory;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->absenceMapper = $this->createMock(AbsenceMapper::class);
+ $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
+ $this->jobList = $this->createMock(IJobList::class);
+ $this->timezoneService = $this->createMock(TimezoneService::class);
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+
+ $this->absenceService = new AbsenceService(
+ $this->absenceMapper,
+ $this->eventDispatcher,
+ $this->jobList,
+ $this->timezoneService,
+ $this->timeFactory,
+ );
+ }
+
+ public function testCreateAbsenceEmitsScheduledEvent(): void {
+ $tz = new DateTimeZone('Europe/Berlin');
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('user');
+
+ $this->absenceMapper->expects(self::once())
+ ->method('findByUserId')
+ ->with('user')
+ ->willThrowException(new DoesNotExistException('foo bar'));
+ $this->absenceMapper->expects(self::once())
+ ->method('insert')
+ ->willReturnCallback(function (Absence $absence): Absence {
+ $absence->setId(1);
+ return $absence;
+ });
+ $this->timezoneService->expects(self::once())
+ ->method('getUserTimezone')
+ ->with('user')
+ ->willReturn('Europe/Berlin');
+ $this->eventDispatcher->expects(self::once())
+ ->method('dispatchTyped')
+ ->with(self::callback(static function (Event $event) use ($user, $tz): bool {
+ self::assertInstanceOf(OutOfOfficeScheduledEvent::class, $event);
+ /** @var OutOfOfficeScheduledEvent $event */
+ $data = $event->getData();
+ self::assertEquals('1', $data->getId());
+ self::assertEquals($user, $data->getUser());
+ self::assertEquals(
+ (new DateTimeImmutable('2023-01-05', $tz))->getTimeStamp(),
+ $data->getStartDate(),
+ );
+ self::assertEquals(
+ (new DateTimeImmutable('2023-01-10', $tz))->getTimeStamp() + 3600 * 23 + 59 * 60,
+ $data->getEndDate(),
+ );
+ self::assertEquals('status', $data->getShortMessage());
+ self::assertEquals('message', $data->getMessage());
+ return true;
+ }));
+ $this->timeFactory->expects(self::once())
+ ->method('getTime')
+ ->willReturn(PHP_INT_MAX);
+ $this->jobList->expects(self::never())
+ ->method('scheduleAfter');
+
+ $this->absenceService->createOrUpdateAbsence(
+ $user,
+ '2023-01-05',
+ '2023-01-10',
+ 'status',
+ 'message',
+ );
+ }
+
+ public function testUpdateAbsenceEmitsChangedEvent(): void {
+ $tz = new DateTimeZone('Europe/Berlin');
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('user');
+ $absence = new Absence();
+ $absence->setId(1);
+ $absence->setFirstDay('1970-01-01');
+ $absence->setLastDay('1970-01-10');
+ $absence->setStatus('old status');
+ $absence->setMessage('old message');
+
+ $this->absenceMapper->expects(self::once())
+ ->method('findByUserId')
+ ->with('user')
+ ->willReturn($absence);
+ $this->absenceMapper->expects(self::once())
+ ->method('update')
+ ->willReturnCallback(static function (Absence $absence): Absence {
+ self::assertEquals('2023-01-05', $absence->getFirstDay());
+ self::assertEquals('2023-01-10', $absence->getLastDay());
+ self::assertEquals('status', $absence->getStatus());
+ self::assertEquals('message', $absence->getMessage());
+ return $absence;
+ });
+ $this->timezoneService->expects(self::once())
+ ->method('getUserTimezone')
+ ->with('user')
+ ->willReturn('Europe/Berlin');
+ $this->eventDispatcher->expects(self::once())
+ ->method('dispatchTyped')
+ ->with(self::callback(static function (Event $event) use ($user, $tz): bool {
+ self::assertInstanceOf(OutOfOfficeChangedEvent::class, $event);
+ /** @var OutOfOfficeChangedEvent $event */
+ $data = $event->getData();
+ self::assertEquals('1', $data->getId());
+ self::assertEquals($user, $data->getUser());
+ self::assertEquals(
+ (new DateTimeImmutable('2023-01-05', $tz))->getTimeStamp(),
+ $data->getStartDate(),
+ );
+ self::assertEquals(
+ (new DateTimeImmutable('2023-01-10', $tz))->getTimeStamp() + 3600 * 23 + 59 * 60,
+ $data->getEndDate(),
+ );
+ self::assertEquals('status', $data->getShortMessage());
+ self::assertEquals('message', $data->getMessage());
+ return true;
+ }));
+ $this->timeFactory->expects(self::once())
+ ->method('getTime')
+ ->willReturn(PHP_INT_MAX);
+ $this->jobList->expects(self::never())
+ ->method('scheduleAfter');
+
+ $this->absenceService->createOrUpdateAbsence(
+ $user,
+ '2023-01-05',
+ '2023-01-10',
+ 'status',
+ 'message',
+ );
+ }
+
+ public function testCreateAbsenceSchedulesBothJobs(): void {
+ $tz = new DateTimeZone('Europe/Berlin');
+ $startDateString = '2023-01-05';
+ $startDate = new DateTimeImmutable($startDateString, $tz);
+ $endDateString = '2023-01-10';
+ $endDate = new DateTimeImmutable($endDateString, $tz);
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('user');
+
+ $this->absenceMapper->expects(self::once())
+ ->method('findByUserId')
+ ->with('user')
+ ->willThrowException(new DoesNotExistException('foo bar'));
+ $this->absenceMapper->expects(self::once())
+ ->method('insert')
+ ->willReturnCallback(function (Absence $absence): Absence {
+ $absence->setId(1);
+ return $absence;
+ });
+ $this->timezoneService->expects(self::once())
+ ->method('getUserTimezone')
+ ->with('user')
+ ->willReturn($tz->getName());
+ $this->timeFactory->expects(self::once())
+ ->method('getTime')
+ ->willReturn((new DateTimeImmutable('2023-01-01', $tz))->getTimestamp());
+ $this->jobList->expects(self::exactly(2))
+ ->method('scheduleAfter')
+ ->willReturnMap([
+ [OutOfOfficeEventDispatcherJob::class, $startDate->getTimestamp(), [
+ 'id' => '1',
+ 'event' => OutOfOfficeEventDispatcherJob::EVENT_START,
+ ]],
+ [OutOfOfficeEventDispatcherJob::class, $endDate->getTimestamp() + 3600 * 23 + 59 * 60, [
+ 'id' => '1',
+ 'event' => OutOfOfficeEventDispatcherJob::EVENT_END,
+ ]],
+ ]);
+
+ $this->absenceService->createOrUpdateAbsence(
+ $user,
+ $startDateString,
+ $endDateString,
+ '',
+ '',
+ );
+ }
+
+ public function testCreateAbsenceSchedulesOnlyEndJob(): void {
+ $tz = new DateTimeZone('Europe/Berlin');
+ $endDateString = '2023-01-10';
+ $endDate = new DateTimeImmutable($endDateString, $tz);
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('user');
+
+ $this->absenceMapper->expects(self::once())
+ ->method('findByUserId')
+ ->with('user')
+ ->willThrowException(new DoesNotExistException('foo bar'));
+ $this->absenceMapper->expects(self::once())
+ ->method('insert')
+ ->willReturnCallback(function (Absence $absence): Absence {
+ $absence->setId(1);
+ return $absence;
+ });
+ $this->timezoneService->expects(self::once())
+ ->method('getUserTimezone')
+ ->with('user')
+ ->willReturn($tz->getName());
+ $this->timeFactory->expects(self::once())
+ ->method('getTime')
+ ->willReturn((new DateTimeImmutable('2023-01-07', $tz))->getTimestamp());
+ $this->jobList->expects(self::once())
+ ->method('scheduleAfter')
+ ->with(OutOfOfficeEventDispatcherJob::class, $endDate->getTimestamp() + 3600 * 23 + 59 * 60, [
+ 'id' => '1',
+ 'event' => OutOfOfficeEventDispatcherJob::EVENT_END,
+ ]);
+
+ $this->absenceService->createOrUpdateAbsence(
+ $user,
+ '2023-01-05',
+ $endDateString,
+ '',
+ '',
+ );
+ }
+
+ public function testCreateAbsenceSchedulesNoJob(): void {
+ $tz = new DateTimeZone('Europe/Berlin');
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('user');
+
+ $this->absenceMapper->expects(self::once())
+ ->method('findByUserId')
+ ->with('user')
+ ->willThrowException(new DoesNotExistException('foo bar'));
+ $this->absenceMapper->expects(self::once())
+ ->method('insert')
+ ->willReturnCallback(function (Absence $absence): Absence {
+ $absence->setId(1);
+ return $absence;
+ });
+ $this->timezoneService->expects(self::once())
+ ->method('getUserTimezone')
+ ->with('user')
+ ->willReturn($tz->getName());
+ $this->timeFactory->expects(self::once())
+ ->method('getTime')
+ ->willReturn((new DateTimeImmutable('2023-01-12', $tz))->getTimestamp());
+ $this->jobList->expects(self::never())
+ ->method('scheduleAfter');
+
+ $this->absenceService->createOrUpdateAbsence(
+ $user,
+ '2023-01-05',
+ '2023-01-10',
+ '',
+ '',
+ );
+ }
+
+ public function testUpdateAbsenceSchedulesBothJobs(): void {
+ $tz = new DateTimeZone('Europe/Berlin');
+ $startDateString = '2023-01-05';
+ $startDate = new DateTimeImmutable($startDateString, $tz);
+ $endDateString = '2023-01-10';
+ $endDate = new DateTimeImmutable($endDateString, $tz);
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('user');
+ $absence = new Absence();
+ $absence->setId(1);
+ $absence->setFirstDay('1970-01-01');
+ $absence->setLastDay('1970-01-10');
+ $absence->setStatus('old status');
+ $absence->setMessage('old message');
+
+ $this->absenceMapper->expects(self::once())
+ ->method('findByUserId')
+ ->with('user')
+ ->willReturn($absence);
+ $this->absenceMapper->expects(self::once())
+ ->method('update')
+ ->willReturnCallback(static function (Absence $absence) use ($startDateString, $endDateString): Absence {
+ self::assertEquals($startDateString, $absence->getFirstDay());
+ self::assertEquals($endDateString, $absence->getLastDay());
+ return $absence;
+ });
+ $this->timezoneService->expects(self::once())
+ ->method('getUserTimezone')
+ ->with('user')
+ ->willReturn($tz->getName());
+ $this->timeFactory->expects(self::once())
+ ->method('getTime')
+ ->willReturn((new DateTimeImmutable('2023-01-01', $tz))->getTimestamp());
+ $this->jobList->expects(self::exactly(2))
+ ->method('scheduleAfter')
+ ->willReturnMap([
+ [OutOfOfficeEventDispatcherJob::class, $startDate->getTimestamp(), [
+ 'id' => '1',
+ 'event' => OutOfOfficeEventDispatcherJob::EVENT_START,
+ ]],
+ [OutOfOfficeEventDispatcherJob::class, $endDate->getTimestamp() + 3600 * 23 + 59 * 60, [
+ 'id' => '1',
+ 'event' => OutOfOfficeEventDispatcherJob::EVENT_END,
+ ]],
+ ]);
+
+ $this->absenceService->createOrUpdateAbsence(
+ $user,
+ $startDateString,
+ $endDateString,
+ '',
+ '',
+ );
+ }
+
+ public function testUpdateSchedulesOnlyEndJob(): void {
+ $tz = new DateTimeZone('Europe/Berlin');
+ $endDateString = '2023-01-10';
+ $endDate = new DateTimeImmutable($endDateString, $tz);
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('user');
+ $absence = new Absence();
+ $absence->setId(1);
+ $absence->setFirstDay('1970-01-01');
+ $absence->setLastDay('1970-01-10');
+ $absence->setStatus('old status');
+ $absence->setMessage('old message');
+
+ $this->absenceMapper->expects(self::once())
+ ->method('findByUserId')
+ ->with('user')
+ ->willReturn($absence);
+ $this->absenceMapper->expects(self::once())
+ ->method('update')
+ ->willReturnCallback(static function (Absence $absence) use ($endDateString): Absence {
+ self::assertEquals('2023-01-05', $absence->getFirstDay());
+ self::assertEquals($endDateString, $absence->getLastDay());
+ return $absence;
+ });
+ $this->timezoneService->expects(self::once())
+ ->method('getUserTimezone')
+ ->with('user')
+ ->willReturn($tz->getName());
+ $this->timeFactory->expects(self::once())
+ ->method('getTime')
+ ->willReturn((new DateTimeImmutable('2023-01-07', $tz))->getTimestamp());
+ $this->jobList->expects(self::once())
+ ->method('scheduleAfter')
+ ->with(OutOfOfficeEventDispatcherJob::class, $endDate->getTimestamp() + 23 * 3600 + 59 * 60, [
+ 'id' => '1',
+ 'event' => OutOfOfficeEventDispatcherJob::EVENT_END,
+ ]);
+
+ $this->absenceService->createOrUpdateAbsence(
+ $user,
+ '2023-01-05',
+ $endDateString,
+ '',
+ '',
+ );
+ }
+
+ public function testUpdateAbsenceSchedulesNoJob(): void {
+ $tz = new DateTimeZone('Europe/Berlin');
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('user');
+ $absence = new Absence();
+ $absence->setId(1);
+ $absence->setFirstDay('1970-01-01');
+ $absence->setLastDay('1970-01-10');
+ $absence->setStatus('old status');
+ $absence->setMessage('old message');
+
+ $this->absenceMapper->expects(self::once())
+ ->method('findByUserId')
+ ->with('user')
+ ->willReturn($absence);
+ $this->absenceMapper->expects(self::once())
+ ->method('update')
+ ->willReturnCallback(static function (Absence $absence): Absence {
+ self::assertEquals('2023-01-05', $absence->getFirstDay());
+ self::assertEquals('2023-01-10', $absence->getLastDay());
+ return $absence;
+ });
+ $this->timezoneService->expects(self::once())
+ ->method('getUserTimezone')
+ ->with('user')
+ ->willReturn($tz->getName());
+ $this->timeFactory->expects(self::once())
+ ->method('getTime')
+ ->willReturn((new DateTimeImmutable('2023-01-12', $tz))->getTimestamp());
+ $this->jobList->expects(self::never())
+ ->method('scheduleAfter');
+
+ $this->absenceService->createOrUpdateAbsence(
+ $user,
+ '2023-01-05',
+ '2023-01-10',
+ '',
+ '',
+ );
+ }
+}
diff --git a/apps/dav/tests/unit/Service/ExampleContactServiceTest.php b/apps/dav/tests/unit/Service/ExampleContactServiceTest.php
new file mode 100644
index 00000000000..027b66a6fb2
--- /dev/null
+++ b/apps/dav/tests/unit/Service/ExampleContactServiceTest.php
@@ -0,0 +1,194 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\Service;
+
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\Service\ExampleContactService;
+use OCP\App\IAppManager;
+use OCP\AppFramework\Services\IAppConfig;
+use OCP\Files\AppData\IAppDataFactory;
+use OCP\Files\IAppData;
+use OCP\Files\NotFoundException;
+use OCP\Files\SimpleFS\ISimpleFile;
+use OCP\Files\SimpleFS\ISimpleFolder;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Uid\Uuid;
+use Test\TestCase;
+
+class ExampleContactServiceTest extends TestCase {
+ protected ExampleContactService $service;
+ protected CardDavBackend&MockObject $cardDav;
+ protected IAppManager&MockObject $appManager;
+ protected IAppDataFactory&MockObject $appDataFactory;
+ protected LoggerInterface&MockObject $logger;
+ protected IAppConfig&MockObject $appConfig;
+ protected IAppData&MockObject $appData;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->cardDav = $this->createMock(CardDavBackend::class);
+ $this->appDataFactory = $this->createMock(IAppDataFactory::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->appConfig = $this->createMock(IAppConfig::class);
+
+ $this->appData = $this->createMock(IAppData::class);
+ $this->appDataFactory->method('get')
+ ->with('dav')
+ ->willReturn($this->appData);
+
+ $this->service = new ExampleContactService(
+ $this->appDataFactory,
+ $this->appConfig,
+ $this->logger,
+ $this->cardDav,
+ );
+ }
+
+ public function testCreateDefaultContactWithInvalidCard(): void {
+ // Invalid vCard missing required FN property
+ $vcardContent = "BEGIN:VCARD\nVERSION:3.0\nEND:VCARD";
+ $this->appConfig->method('getAppValueBool')
+ ->with('enableDefaultContact', true)
+ ->willReturn(true);
+ $folder = $this->createMock(ISimpleFolder::class);
+ $file = $this->createMock(ISimpleFile::class);
+ $file->method('getContent')->willReturn($vcardContent);
+ $folder->method('getFile')->willReturn($file);
+ $this->appData->method('getFolder')->willReturn($folder);
+
+ $this->logger->expects($this->once())
+ ->method('error')
+ ->with('Default contact is invalid', $this->anything());
+
+ $this->cardDav->expects($this->never())
+ ->method('createCard');
+
+ $this->service->createDefaultContact(123);
+ }
+
+ public function testUidAndRevAreUpdated(): void {
+ $originalUid = 'original-uid';
+ $originalRev = '20200101T000000Z';
+ $vcardContent = "BEGIN:VCARD\nVERSION:3.0\nFN:Test User\nUID:$originalUid\nREV:$originalRev\nEND:VCARD";
+
+ $this->appConfig->method('getAppValueBool')
+ ->with('enableDefaultContact', true)
+ ->willReturn(true);
+ $folder = $this->createMock(ISimpleFolder::class);
+ $file = $this->createMock(ISimpleFile::class);
+ $file->method('getContent')->willReturn($vcardContent);
+ $folder->method('getFile')->willReturn($file);
+ $this->appData->method('getFolder')->willReturn($folder);
+
+ $capturedCardData = null;
+ $this->cardDav->expects($this->once())
+ ->method('createCard')
+ ->with(
+ $this->anything(),
+ $this->anything(),
+ $this->callback(function ($cardData) use (&$capturedCardData) {
+ $capturedCardData = $cardData;
+ return true;
+ }),
+ $this->anything()
+ )->willReturn(null);
+
+ $this->service->createDefaultContact(123);
+
+ $vcard = \Sabre\VObject\Reader::read($capturedCardData);
+ $this->assertNotEquals($originalUid, $vcard->UID->getValue());
+ $this->assertTrue(Uuid::isValid($vcard->UID->getValue()));
+ $this->assertNotEquals($originalRev, $vcard->REV->getValue());
+ }
+
+ public function testDefaultContactFileDoesNotExist(): void {
+ $this->appConfig->method('getAppValueBool')
+ ->with('enableDefaultContact', true)
+ ->willReturn(true);
+ $this->appData->method('getFolder')->willThrowException(new NotFoundException());
+
+ $this->cardDav->expects($this->never())
+ ->method('createCard');
+
+ $this->service->createDefaultContact(123);
+ }
+
+ public function testUidAndRevAreAddedIfMissing(): void {
+ $vcardContent = "BEGIN:VCARD\nVERSION:3.0\nFN:Test User\nEND:VCARD";
+
+ $this->appConfig->method('getAppValueBool')
+ ->with('enableDefaultContact', true)
+ ->willReturn(true);
+ $folder = $this->createMock(ISimpleFolder::class);
+ $file = $this->createMock(ISimpleFile::class);
+ $file->method('getContent')->willReturn($vcardContent);
+ $folder->method('getFile')->willReturn($file);
+ $this->appData->method('getFolder')->willReturn($folder);
+
+ $capturedCardData = 'new-card-data';
+
+ $this->cardDav
+ ->expects($this->once())
+ ->method('createCard')
+ ->with(
+ $this->anything(),
+ $this->anything(),
+ $this->callback(function ($cardData) use (&$capturedCardData) {
+ $capturedCardData = $cardData;
+ return true;
+ }),
+ $this->anything()
+ );
+
+ $this->service->createDefaultContact(123);
+ $vcard = \Sabre\VObject\Reader::read($capturedCardData);
+
+ $this->assertNotNull($vcard->REV);
+ $this->assertNotNull($vcard->UID);
+ $this->assertTrue(Uuid::isValid($vcard->UID->getValue()));
+ }
+
+ public function testDefaultContactIsNotCreatedIfEnabled(): void {
+ $this->appConfig->method('getAppValueBool')
+ ->with('enableDefaultContact', true)
+ ->willReturn(false);
+ $this->logger->expects($this->never())
+ ->method('error');
+ $this->cardDav->expects($this->never())
+ ->method('createCard');
+
+ $this->service->createDefaultContact(123);
+ }
+
+ public static function provideDefaultContactEnableData(): array {
+ return [[true], [false]];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('provideDefaultContactEnableData')]
+ public function testIsDefaultContactEnabled(bool $enabled): void {
+ $this->appConfig->expects(self::once())
+ ->method('getAppValueBool')
+ ->with('enableDefaultContact', true)
+ ->willReturn($enabled);
+
+ $this->assertEquals($enabled, $this->service->isDefaultContactEnabled());
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('provideDefaultContactEnableData')]
+ public function testSetDefaultContactEnabled(bool $enabled): void {
+ $this->appConfig->expects(self::once())
+ ->method('setAppValueBool')
+ ->with('enableDefaultContact', $enabled);
+
+ $this->service->setDefaultContactEnabled($enabled);
+ }
+}
diff --git a/apps/dav/tests/unit/Service/ExampleEventServiceTest.php b/apps/dav/tests/unit/Service/ExampleEventServiceTest.php
new file mode 100644
index 00000000000..0f423624fb8
--- /dev/null
+++ b/apps/dav/tests/unit/Service/ExampleEventServiceTest.php
@@ -0,0 +1,196 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\Service;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\Service\ExampleEventService;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Files\IAppData;
+use OCP\Files\NotFoundException;
+use OCP\Files\SimpleFS\ISimpleFile;
+use OCP\Files\SimpleFS\ISimpleFolder;
+use OCP\IAppConfig;
+use OCP\IL10N;
+use OCP\Security\ISecureRandom;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class ExampleEventServiceTest extends TestCase {
+ private ExampleEventService $service;
+
+ private CalDavBackend&MockObject $calDavBackend;
+ private ISecureRandom&MockObject $random;
+ private ITimeFactory&MockObject $time;
+ private IAppData&MockObject $appData;
+ private IAppConfig&MockObject $appConfig;
+ private IL10N&MockObject $l10n;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->calDavBackend = $this->createMock(CalDavBackend::class);
+ $this->random = $this->createMock(ISecureRandom::class);
+ $this->time = $this->createMock(ITimeFactory::class);
+ $this->appData = $this->createMock(IAppData::class);
+ $this->appConfig = $this->createMock(IAppConfig::class);
+ $this->l10n = $this->createMock(IL10N::class);
+
+ $this->l10n->method('t')
+ ->willReturnArgument(0);
+
+ $this->service = new ExampleEventService(
+ $this->calDavBackend,
+ $this->random,
+ $this->time,
+ $this->appData,
+ $this->appConfig,
+ $this->l10n,
+ );
+ }
+
+ public static function provideCustomEventData(): array {
+ return [
+ [file_get_contents(__DIR__ . '/../test_fixtures/example-event.ics')],
+ [file_get_contents(__DIR__ . '/../test_fixtures/example-event-with-attendees.ics')],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('provideCustomEventData')]
+ public function testCreateExampleEventWithCustomEvent($customEventIcs): void {
+ $this->appConfig->expects(self::once())
+ ->method('getValueBool')
+ ->with('dav', 'create_example_event', true)
+ ->willReturn(true);
+
+ $exampleEventFolder = $this->createMock(ISimpleFolder::class);
+ $this->appData->expects(self::once())
+ ->method('getFolder')
+ ->with('example_event')
+ ->willReturn($exampleEventFolder);
+ $exampleEventFile = $this->createMock(ISimpleFile::class);
+ $exampleEventFolder->expects(self::once())
+ ->method('getFile')
+ ->with('example_event.ics')
+ ->willReturn($exampleEventFile);
+ $exampleEventFile->expects(self::once())
+ ->method('getContent')
+ ->willReturn($customEventIcs);
+
+ $this->random->expects(self::once())
+ ->method('generate')
+ ->with(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')
+ ->willReturn('RANDOM-UID');
+
+ $now = new \DateTimeImmutable('2025-01-21T00:00:00Z');
+ $this->time->expects(self::exactly(2))
+ ->method('now')
+ ->willReturn($now);
+
+ $expectedIcs = file_get_contents(__DIR__ . '/../test_fixtures/example-event-expected.ics');
+ $this->calDavBackend->expects(self::once())
+ ->method('createCalendarObject')
+ ->with(1000, 'RANDOM-UID.ics', $expectedIcs);
+
+ $this->service->createExampleEvent(1000);
+ }
+
+ public function testCreateExampleEventWithDefaultEvent(): void {
+ $this->appConfig->expects(self::once())
+ ->method('getValueBool')
+ ->with('dav', 'create_example_event', true)
+ ->willReturn(true);
+
+ $this->appData->expects(self::once())
+ ->method('getFolder')
+ ->with('example_event')
+ ->willThrowException(new NotFoundException());
+
+ $this->random->expects(self::once())
+ ->method('generate')
+ ->with(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')
+ ->willReturn('RANDOM-UID');
+
+ $now = new \DateTimeImmutable('2025-01-21T00:00:00Z');
+ $this->time->expects(self::exactly(3))
+ ->method('now')
+ ->willReturn($now);
+
+ $expectedIcs = file_get_contents(__DIR__ . '/../test_fixtures/example-event-default-expected.ics');
+ $this->calDavBackend->expects(self::once())
+ ->method('createCalendarObject')
+ ->with(1000, 'RANDOM-UID.ics', $expectedIcs);
+
+ $this->service->createExampleEvent(1000);
+ }
+
+ public function testCreateExampleWhenDisabled(): void {
+ $this->appConfig->expects(self::once())
+ ->method('getValueBool')
+ ->with('dav', 'create_example_event', true)
+ ->willReturn(false);
+
+ $this->calDavBackend->expects(self::never())
+ ->method('createCalendarObject');
+
+ $this->service->createExampleEvent(1000);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('provideCustomEventData')]
+ public function testGetExampleEventWithCustomEvent($customEventIcs): void {
+ $exampleEventFolder = $this->createMock(ISimpleFolder::class);
+ $this->appData->expects(self::once())
+ ->method('getFolder')
+ ->with('example_event')
+ ->willReturn($exampleEventFolder);
+ $exampleEventFile = $this->createMock(ISimpleFile::class);
+ $exampleEventFolder->expects(self::once())
+ ->method('getFile')
+ ->with('example_event.ics')
+ ->willReturn($exampleEventFile);
+ $exampleEventFile->expects(self::once())
+ ->method('getContent')
+ ->willReturn($customEventIcs);
+
+ $this->random->expects(self::once())
+ ->method('generate')
+ ->with(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')
+ ->willReturn('RANDOM-UID');
+
+ $now = new \DateTimeImmutable('2025-01-21T00:00:00Z');
+ $this->time->expects(self::exactly(2))
+ ->method('now')
+ ->willReturn($now);
+
+ $expectedIcs = file_get_contents(__DIR__ . '/../test_fixtures/example-event-expected.ics');
+ $actualIcs = $this->service->getExampleEvent()->getIcs();
+ $this->assertEquals($expectedIcs, $actualIcs);
+ }
+
+ public function testGetExampleEventWithDefault(): void {
+ $this->appData->expects(self::once())
+ ->method('getFolder')
+ ->with('example_event')
+ ->willThrowException(new NotFoundException());
+
+ $this->random->expects(self::once())
+ ->method('generate')
+ ->with(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')
+ ->willReturn('RANDOM-UID');
+
+ $now = new \DateTimeImmutable('2025-01-21T00:00:00Z');
+ $this->time->expects(self::exactly(3))
+ ->method('now')
+ ->willReturn($now);
+
+ $expectedIcs = file_get_contents(__DIR__ . '/../test_fixtures/example-event-default-expected.ics');
+ $actualIcs = $this->service->getExampleEvent()->getIcs();
+ $this->assertEquals($expectedIcs, $actualIcs);
+ }
+}
diff --git a/apps/dav/tests/unit/Service/UpcomingEventsServiceTest.php b/apps/dav/tests/unit/Service/UpcomingEventsServiceTest.php
new file mode 100644
index 00000000000..fdfe37d8918
--- /dev/null
+++ b/apps/dav/tests/unit/Service/UpcomingEventsServiceTest.php
@@ -0,0 +1,89 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Tests\unit\DAV\Service;
+
+use DateTimeImmutable;
+use OCA\DAV\CalDAV\UpcomingEventsService;
+use OCP\App\IAppManager;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Calendar\ICalendarQuery;
+use OCP\Calendar\IManager;
+use OCP\IURLGenerator;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+class UpcomingEventsServiceTest extends TestCase {
+
+ private IManager&MockObject $calendarManager;
+ private ITimeFactory&MockObject $timeFactory;
+ private IUserManager&MockObject $userManager;
+ private IAppManager&MockObject $appManager;
+ private IURLGenerator&MockObject $urlGenerator;
+ private UpcomingEventsService $service;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->calendarManager = $this->createMock(IManager::class);
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->appManager = $this->createMock(IAppManager::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+
+ $this->service = new UpcomingEventsService(
+ $this->calendarManager,
+ $this->timeFactory,
+ $this->userManager,
+ $this->appManager,
+ $this->urlGenerator,
+ );
+ }
+
+ public function testGetEventsByLocation(): void {
+ $now = new DateTimeImmutable('2024-07-08T18:20:20Z');
+ $this->timeFactory->method('now')
+ ->willReturn($now);
+ $query = $this->createMock(ICalendarQuery::class);
+ $this->appManager->method('isEnabledForUser')->willReturn(false);
+ $this->calendarManager->method('newQuery')
+ ->with('principals/users/user1')
+ ->willReturn($query);
+ $query->expects(self::once())
+ ->method('addSearchProperty')
+ ->with('LOCATION');
+ $query->expects(self::once())
+ ->method('setSearchPattern')
+ ->with('https://cloud.example.com/call/123');
+ $this->calendarManager->expects(self::once())
+ ->method('searchForPrincipal')
+ ->with($query)
+ ->willReturn([
+ [
+ 'uri' => 'ev1',
+ 'calendar-key' => '1',
+ 'calendar-uri' => 'personal',
+ 'objects' => [
+ 0 => [
+ 'DTSTART' => [
+ new DateTimeImmutable('now'),
+ ],
+ ],
+ ],
+ ],
+ ]);
+
+ $events = $this->service->getEvents('user1', 'https://cloud.example.com/call/123');
+
+ self::assertCount(1, $events);
+ $event1 = $events[0];
+ self::assertEquals('ev1', $event1->getUri());
+ }
+}
diff --git a/apps/dav/tests/unit/Settings/CalDAVSettingsTest.php b/apps/dav/tests/unit/Settings/CalDAVSettingsTest.php
index 4b1fad717cb..032759d64b7 100644
--- a/apps/dav/tests/unit/Settings/CalDAVSettingsTest.php
+++ b/apps/dav/tests/unit/Settings/CalDAVSettingsTest.php
@@ -1,50 +1,26 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright 2017, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @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\Tests\Unit\DAV\Settings;
+namespace OCA\DAV\Tests\unit\DAV\Settings;
use OCA\DAV\Settings\CalDAVSettings;
+use OCP\App\IAppManager;
use OCP\AppFramework\Http\TemplateResponse;
-use OCP\IConfig;
use OCP\AppFramework\Services\IInitialState;
+use OCP\IConfig;
use OCP\IURLGenerator;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class CalDAVSettingsTest extends TestCase {
-
- /** @var IConfig|MockObject */
- private $config;
-
- /** @var IInitialState|MockObject */
- private $initialState;
-
- /** @var IURLGenerator|MockObject */
- private $urlGenerator;
-
- /** @var CalDAVSettings */
+ private IConfig&MockObject $config;
+ private IInitialState&MockObject $initialState;
+ private IURLGenerator&MockObject $urlGenerator;
+ private IAppManager&MockObject $appManager;
private CalDAVSettings $settings;
protected function setUp(): void {
@@ -53,43 +29,60 @@ class CalDAVSettingsTest extends TestCase {
$this->config = $this->createMock(IConfig::class);
$this->initialState = $this->createMock(IInitialState::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
- $this->settings = new CalDAVSettings($this->config, $this->initialState, $this->urlGenerator);
+ $this->appManager = $this->createMock(IAppManager::class);
+ $this->settings = new CalDAVSettings($this->config, $this->initialState, $this->urlGenerator, $this->appManager);
}
- public function testGetForm() {
+ public function testGetForm(): void {
$this->config->method('getAppValue')
- ->withConsecutive(
- ['dav', 'sendInvitations', 'yes'],
- ['dav', 'generateBirthdayCalendar', 'yes'],
- ['dav', 'sendEventReminders', 'yes'],
- ['dav', 'sendEventRemindersToSharedGroupMembers', 'yes'],
- ['dav', 'sendEventRemindersPush', 'no'],
- )
- ->will($this->onConsecutiveCalls('yes', 'no', 'yes', 'yes', 'yes'));
+ ->willReturnMap([
+ ['dav', 'sendInvitations', 'yes', 'yes'],
+ ['dav', 'generateBirthdayCalendar', 'yes', 'no'],
+ ['dav', 'sendEventReminders', 'yes', 'yes'],
+ ['dav', 'sendEventRemindersToSharedUsers', 'yes', 'yes'],
+ ['dav', 'sendEventRemindersPush', 'yes', 'yes'],
+ ]);
$this->urlGenerator
->expects($this->once())
->method('linkToDocs')
->with('user-sync-calendars')
->willReturn('Some docs URL');
+
+ $calls = [
+ ['userSyncCalendarsDocUrl', 'Some docs URL'],
+ ['sendInvitations', true],
+ ['generateBirthdayCalendar', false],
+ ['sendEventReminders', true],
+ ['sendEventRemindersToSharedUsers', true],
+ ['sendEventRemindersPush', true],
+ ];
$this->initialState->method('provideInitialState')
- ->withConsecutive(
- ['userSyncCalendarsDocUrl', 'Some docs URL'],
- ['sendInvitations', true],
- ['generateBirthdayCalendar', false],
- ['sendEventReminders', true],
- ['sendEventRemindersToSharedGroupMembers', true],
- ['sendEventRemindersPush', true],
- );
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
$result = $this->settings->getForm();
$this->assertInstanceOf(TemplateResponse::class, $result);
}
- public function testGetSection() {
+ public function testGetSection(): void {
+ $this->appManager->expects(self::once())
+ ->method('isBackendRequired')
+ ->with(IAppManager::BACKEND_CALDAV)
+ ->willReturn(true);
$this->assertEquals('groupware', $this->settings->getSection());
}
- public function testGetPriority() {
+ public function testGetSectionWithoutCaldavBackend(): void {
+ $this->appManager->expects(self::once())
+ ->method('isBackendRequired')
+ ->with(IAppManager::BACKEND_CALDAV)
+ ->willReturn(false);
+ $this->assertEquals(null, $this->settings->getSection());
+ }
+
+ public function testGetPriority(): void {
$this->assertEquals(10, $this->settings->getPriority());
}
}
diff --git a/apps/dav/tests/unit/SystemTag/SystemTagMappingNodeTest.php b/apps/dav/tests/unit/SystemTag/SystemTagMappingNodeTest.php
index e39cb0a04d3..39342811377 100644
--- a/apps/dav/tests/unit/SystemTag/SystemTagMappingNodeTest.php
+++ b/apps/dav/tests/unit/SystemTag/SystemTagMappingNodeTest.php
@@ -1,81 +1,52 @@
<?php
+
+declare(strict_types=1);
/**
- * @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\Tests\unit\SystemTag;
use OC\SystemTag\SystemTag;
+use OCA\DAV\SystemTag\SystemTagMappingNode;
use OCP\IUser;
use OCP\SystemTag\ISystemTag;
use OCP\SystemTag\ISystemTagManager;
use OCP\SystemTag\ISystemTagObjectMapper;
use OCP\SystemTag\TagNotFoundException;
+use PHPUnit\Framework\MockObject\MockObject;
class SystemTagMappingNodeTest extends \Test\TestCase {
-
- /**
- * @var \OCP\SystemTag\ISystemTagManager
- */
- private $tagManager;
-
- /**
- * @var \OCP\SystemTag\ISystemTagObjectMapper
- */
- private $tagMapper;
-
- /**
- * @var \OCP\IUser
- */
- private $user;
+ private ISystemTagManager&MockObject $tagManager;
+ private ISystemTagObjectMapper&MockObject $tagMapper;
+ private IUser&MockObject $user;
protected function setUp(): void {
parent::setUp();
- $this->tagManager = $this->getMockBuilder(ISystemTagManager::class)
- ->getMock();
- $this->tagMapper = $this->getMockBuilder(ISystemTagObjectMapper::class)
- ->getMock();
- $this->user = $this->getMockBuilder(IUser::class)
- ->getMock();
+ $this->tagManager = $this->createMock(ISystemTagManager::class);
+ $this->tagMapper = $this->createMock(ISystemTagObjectMapper::class);
+ $this->user = $this->createMock(IUser::class);
}
- public function getMappingNode($tag = null) {
+ public function getMappingNode($tag = null, array $writableNodeIds = []) {
if ($tag === null) {
- $tag = new SystemTag(1, 'Test', true, true);
+ $tag = new SystemTag('1', 'Test', true, true);
}
- return new \OCA\DAV\SystemTag\SystemTagMappingNode(
+ return new SystemTagMappingNode(
$tag,
- 123,
+ '123',
'files',
$this->user,
$this->tagManager,
- $this->tagMapper
+ $this->tagMapper,
+ fn ($id): bool => in_array($id, $writableNodeIds),
);
}
- public function testGetters() {
- $tag = new SystemTag(1, 'Test', true, false);
+ public function testGetters(): void {
+ $tag = new SystemTag('1', 'Test', true, false);
$node = $this->getMappingNode($tag);
$this->assertEquals('1', $node->getName());
$this->assertEquals($tag, $node->getSystemTag());
@@ -83,8 +54,8 @@ class SystemTagMappingNodeTest extends \Test\TestCase {
$this->assertEquals('files', $node->getObjectType());
}
- public function testDeleteTag() {
- $node = $this->getMappingNode();
+ public function testDeleteTag(): void {
+ $node = $this->getMappingNode(null, [123]);
$this->tagManager->expects($this->once())
->method('canUserSeeTag')
->with($node->getSystemTag())
@@ -102,25 +73,42 @@ class SystemTagMappingNodeTest extends \Test\TestCase {
$node->delete();
}
- public function tagNodeDeleteProviderPermissionException() {
+ public function testDeleteTagForbidden(): void {
+ $node = $this->getMappingNode();
+ $this->tagManager->expects($this->once())
+ ->method('canUserSeeTag')
+ ->with($node->getSystemTag())
+ ->willReturn(true);
+ $this->tagManager->expects($this->once())
+ ->method('canUserAssignTag')
+ ->with($node->getSystemTag())
+ ->willReturn(true);
+ $this->tagManager->expects($this->never())
+ ->method('deleteTags');
+ $this->tagMapper->expects($this->never())
+ ->method('unassignTags');
+
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
+ $node->delete();
+ }
+
+ public static function tagNodeDeleteProviderPermissionException(): array {
return [
[
// cannot unassign invisible tag
- new SystemTag(1, 'Original', false, true),
+ new SystemTag('1', 'Original', false, true),
'Sabre\DAV\Exception\NotFound',
],
[
// cannot unassign non-assignable tag
- new SystemTag(1, 'Original', true, false),
+ new SystemTag('1', 'Original', true, false),
'Sabre\DAV\Exception\Forbidden',
],
];
}
- /**
- * @dataProvider tagNodeDeleteProviderPermissionException
- */
- public function testDeleteTagExpectedException(ISystemTag $tag, $expectedException) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('tagNodeDeleteProviderPermissionException')]
+ public function testDeleteTagExpectedException(ISystemTag $tag, $expectedException): void {
$this->tagManager->expects($this->any())
->method('canUserSeeTag')
->with($tag)
@@ -144,13 +132,13 @@ class SystemTagMappingNodeTest extends \Test\TestCase {
$this->assertInstanceOf($expectedException, $thrown);
}
-
- public function testDeleteTagNotFound() {
+
+ public function testDeleteTagNotFound(): void {
$this->expectException(\Sabre\DAV\Exception\NotFound::class);
// assuming the tag existed at the time the node was created,
// but got deleted concurrently in the database
- $tag = new SystemTag(1, 'Test', true, true);
+ $tag = new SystemTag('1', 'Test', true, true);
$this->tagManager->expects($this->once())
->method('canUserSeeTag')
->with($tag)
@@ -162,8 +150,8 @@ class SystemTagMappingNodeTest extends \Test\TestCase {
$this->tagMapper->expects($this->once())
->method('unassignTags')
->with(123, 'files', 1)
- ->will($this->throwException(new TagNotFoundException()));
+ ->willThrowException(new TagNotFoundException());
- $this->getMappingNode($tag)->delete();
+ $this->getMappingNode($tag, [123])->delete();
}
}
diff --git a/apps/dav/tests/unit/SystemTag/SystemTagNodeTest.php b/apps/dav/tests/unit/SystemTag/SystemTagNodeTest.php
index 409132919a5..594b5e15db6 100644
--- a/apps/dav/tests/unit/SystemTag/SystemTagNodeTest.php
+++ b/apps/dav/tests/unit/SystemTag/SystemTagNodeTest.php
@@ -1,79 +1,56 @@
<?php
+
+declare(strict_types=1);
/**
- * @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\Tests\unit\SystemTag;
use OC\SystemTag\SystemTag;
+use OCA\DAV\SystemTag\SystemTagNode;
use OCP\IUser;
use OCP\SystemTag\ISystemTag;
use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\ISystemTagObjectMapper;
use OCP\SystemTag\TagAlreadyExistsException;
use OCP\SystemTag\TagNotFoundException;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\Exception\Forbidden;
class SystemTagNodeTest extends \Test\TestCase {
-
- /**
- * @var \OCP\SystemTag\ISystemTagManager|\PHPUnit\Framework\MockObject\MockObject
- */
- private $tagManager;
-
- /**
- * @var \OCP\IUser
- */
- private $user;
+ private ISystemTagManager&MockObject $tagManager;
+ private ISystemTagObjectMapper&MockObject $tagMapper;
+ private IUser&MockObject $user;
protected function setUp(): void {
parent::setUp();
- $this->tagManager = $this->getMockBuilder(ISystemTagManager::class)
- ->getMock();
- $this->user = $this->getMockBuilder(IUser::class)
- ->getMock();
+ $this->tagManager = $this->createMock(ISystemTagManager::class);
+ $this->tagMapper = $this->createMock(ISystemTagObjectMapper::class);
+ $this->user = $this->createMock(IUser::class);
}
protected function getTagNode($isAdmin = true, $tag = null) {
if ($tag === null) {
- $tag = new SystemTag(1, 'Test', true, true);
+ $tag = new SystemTag('1', 'Test', true, true);
}
- return new \OCA\DAV\SystemTag\SystemTagNode(
+ return new SystemTagNode(
$tag,
$this->user,
$isAdmin,
- $this->tagManager
+ $this->tagManager,
+ $this->tagMapper,
);
}
- public function adminFlagProvider() {
+ public static function adminFlagProvider(): array {
return [[true], [false]];
}
- /**
- * @dataProvider adminFlagProvider
- */
- public function testGetters($isAdmin) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('adminFlagProvider')]
+ public function testGetters(bool $isAdmin): void {
$tag = new SystemTag('1', 'Test', true, true);
$node = $this->getTagNode($isAdmin, $tag);
$this->assertEquals('1', $node->getName());
@@ -81,39 +58,37 @@ class SystemTagNodeTest extends \Test\TestCase {
}
- public function testSetName() {
+ public function testSetName(): void {
$this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class);
$this->getTagNode()->setName('2');
}
- public function tagNodeProvider() {
+ public static function tagNodeProvider(): array {
return [
// admin
[
true,
- new SystemTag(1, 'Original', true, true),
- ['Renamed', true, true]
+ new SystemTag('1', 'Original', true, true),
+ ['Renamed', true, true, null]
],
[
true,
- new SystemTag(1, 'Original', true, true),
- ['Original', false, false]
+ new SystemTag('1', 'Original', true, true),
+ ['Original', false, false, null]
],
// non-admin
[
// renaming allowed
false,
- new SystemTag(1, 'Original', true, true),
- ['Rename', true, true]
+ new SystemTag('1', 'Original', true, true),
+ ['Rename', true, true, '0082c9']
],
];
}
- /**
- * @dataProvider tagNodeProvider
- */
- public function testUpdateTag($isAdmin, ISystemTag $originalTag, $changedArgs) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('tagNodeProvider')]
+ public function testUpdateTag(bool $isAdmin, ISystemTag $originalTag, array $changedArgs): void {
$this->tagManager->expects($this->once())
->method('canUserSeeTag')
->with($originalTag)
@@ -124,56 +99,54 @@ class SystemTagNodeTest extends \Test\TestCase {
->willReturn($originalTag->isUserAssignable() || $isAdmin);
$this->tagManager->expects($this->once())
->method('updateTag')
- ->with(1, $changedArgs[0], $changedArgs[1], $changedArgs[2]);
+ ->with(1, $changedArgs[0], $changedArgs[1], $changedArgs[2], $changedArgs[3]);
$this->getTagNode($isAdmin, $originalTag)
- ->update($changedArgs[0], $changedArgs[1], $changedArgs[2]);
+ ->update($changedArgs[0], $changedArgs[1], $changedArgs[2], $changedArgs[3]);
}
- public function tagNodeProviderPermissionException() {
+ public static function tagNodeProviderPermissionException(): array {
return [
[
// changing permissions not allowed
- new SystemTag(1, 'Original', true, true),
- ['Original', false, true],
+ new SystemTag('1', 'Original', true, true),
+ ['Original', false, true, ''],
'Sabre\DAV\Exception\Forbidden',
],
[
// changing permissions not allowed
- new SystemTag(1, 'Original', true, true),
- ['Original', true, false],
+ new SystemTag('1', 'Original', true, true),
+ ['Original', true, false, ''],
'Sabre\DAV\Exception\Forbidden',
],
[
// changing permissions not allowed
- new SystemTag(1, 'Original', true, true),
- ['Original', false, false],
+ new SystemTag('1', 'Original', true, true),
+ ['Original', false, false, ''],
'Sabre\DAV\Exception\Forbidden',
],
[
// changing non-assignable not allowed
- new SystemTag(1, 'Original', true, false),
- ['Rename', true, false],
+ new SystemTag('1', 'Original', true, false),
+ ['Rename', true, false, ''],
'Sabre\DAV\Exception\Forbidden',
],
[
// changing non-assignable not allowed
- new SystemTag(1, 'Original', true, false),
- ['Original', true, true],
+ new SystemTag('1', 'Original', true, false),
+ ['Original', true, true, ''],
'Sabre\DAV\Exception\Forbidden',
],
[
// invisible tag does not exist
- new SystemTag(1, 'Original', false, false),
- ['Rename', false, false],
+ new SystemTag('1', 'Original', false, false),
+ ['Rename', false, false, ''],
'Sabre\DAV\Exception\NotFound',
],
];
}
- /**
- * @dataProvider tagNodeProviderPermissionException
- */
- public function testUpdateTagPermissionException(ISystemTag $originalTag, $changedArgs, $expectedException = null) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('tagNodeProviderPermissionException')]
+ public function testUpdateTagPermissionException(ISystemTag $originalTag, array $changedArgs, string $expectedException): void {
$this->tagManager->expects($this->any())
->method('canUserSeeTag')
->with($originalTag)
@@ -189,7 +162,7 @@ class SystemTagNodeTest extends \Test\TestCase {
try {
$this->getTagNode(false, $originalTag)
- ->update($changedArgs[0], $changedArgs[1], $changedArgs[2]);
+ ->update($changedArgs[0], $changedArgs[1], $changedArgs[2], $changedArgs[3]);
} catch (\Exception $e) {
$thrown = $e;
}
@@ -198,10 +171,10 @@ class SystemTagNodeTest extends \Test\TestCase {
}
- public function testUpdateTagAlreadyExists() {
+ public function testUpdateTagAlreadyExists(): void {
$this->expectException(\Sabre\DAV\Exception\Conflict::class);
- $tag = new SystemTag(1, 'tag1', true, true);
+ $tag = new SystemTag('1', 'tag1', true, true);
$this->tagManager->expects($this->any())
->method('canUserSeeTag')
->with($tag)
@@ -213,15 +186,15 @@ class SystemTagNodeTest extends \Test\TestCase {
$this->tagManager->expects($this->once())
->method('updateTag')
->with(1, 'Renamed', true, true)
- ->will($this->throwException(new TagAlreadyExistsException()));
- $this->getTagNode(false, $tag)->update('Renamed', true, true);
+ ->willThrowException(new TagAlreadyExistsException());
+ $this->getTagNode(false, $tag)->update('Renamed', true, true, null);
}
- public function testUpdateTagNotFound() {
+ public function testUpdateTagNotFound(): void {
$this->expectException(\Sabre\DAV\Exception\NotFound::class);
- $tag = new SystemTag(1, 'tag1', true, true);
+ $tag = new SystemTag('1', 'tag1', true, true);
$this->tagManager->expects($this->any())
->method('canUserSeeTag')
->with($tag)
@@ -233,15 +206,13 @@ class SystemTagNodeTest extends \Test\TestCase {
$this->tagManager->expects($this->once())
->method('updateTag')
->with(1, 'Renamed', true, true)
- ->will($this->throwException(new TagNotFoundException()));
- $this->getTagNode(false, $tag)->update('Renamed', true, true);
+ ->willThrowException(new TagNotFoundException());
+ $this->getTagNode(false, $tag)->update('Renamed', true, true, null);
}
- /**
- * @dataProvider adminFlagProvider
- */
- public function testDeleteTag($isAdmin) {
- $tag = new SystemTag(1, 'tag1', true, true);
+ #[\PHPUnit\Framework\Attributes\DataProvider('adminFlagProvider')]
+ public function testDeleteTag(bool $isAdmin): void {
+ $tag = new SystemTag('1', 'tag1', true, true);
$this->tagManager->expects($isAdmin ? $this->once() : $this->never())
->method('canUserSeeTag')
->with($tag)
@@ -255,25 +226,23 @@ class SystemTagNodeTest extends \Test\TestCase {
$this->getTagNode($isAdmin, $tag)->delete();
}
- public function tagNodeDeleteProviderPermissionException() {
+ public static function tagNodeDeleteProviderPermissionException(): array {
return [
[
// cannot delete invisible tag
- new SystemTag(1, 'Original', false, true),
+ new SystemTag('1', 'Original', false, true),
'Sabre\DAV\Exception\Forbidden',
],
[
// cannot delete non-assignable tag
- new SystemTag(1, 'Original', true, false),
+ new SystemTag('1', 'Original', true, false),
'Sabre\DAV\Exception\Forbidden',
],
];
}
- /**
- * @dataProvider tagNodeDeleteProviderPermissionException
- */
- public function testDeleteTagPermissionException(ISystemTag $tag, $expectedException) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('tagNodeDeleteProviderPermissionException')]
+ public function testDeleteTagPermissionException(ISystemTag $tag, string $expectedException): void {
$this->tagManager->expects($this->any())
->method('canUserSeeTag')
->with($tag)
@@ -286,10 +255,10 @@ class SystemTagNodeTest extends \Test\TestCase {
}
- public function testDeleteTagNotFound() {
+ public function testDeleteTagNotFound(): void {
$this->expectException(\Sabre\DAV\Exception\NotFound::class);
- $tag = new SystemTag(1, 'tag1', true, true);
+ $tag = new SystemTag('1', 'tag1', true, true);
$this->tagManager->expects($this->any())
->method('canUserSeeTag')
->with($tag)
@@ -297,7 +266,7 @@ class SystemTagNodeTest extends \Test\TestCase {
$this->tagManager->expects($this->once())
->method('deleteTags')
->with('1')
- ->will($this->throwException(new TagNotFoundException()));
+ ->willThrowException(new TagNotFoundException());
$this->getTagNode(true, $tag)->delete();
}
}
diff --git a/apps/dav/tests/unit/SystemTag/SystemTagPluginTest.php b/apps/dav/tests/unit/SystemTag/SystemTagPluginTest.php
index aa7026d3854..e0c4685c1fb 100644
--- a/apps/dav/tests/unit/SystemTag/SystemTagPluginTest.php
+++ b/apps/dav/tests/unit/SystemTag/SystemTagPluginTest.php
@@ -1,105 +1,59 @@
<?php
+
+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 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\Tests\unit\SystemTag;
use OC\SystemTag\SystemTag;
use OCA\DAV\SystemTag\SystemTagNode;
+use OCA\DAV\SystemTag\SystemTagPlugin;
use OCA\DAV\SystemTag\SystemTagsByIdCollection;
use OCA\DAV\SystemTag\SystemTagsObjectMappingCollection;
+use OCP\Files\IRootFolder;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserSession;
use OCP\SystemTag\ISystemTag;
use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\ISystemTagObjectMapper;
use OCP\SystemTag\TagAlreadyExistsException;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\Tree;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class SystemTagPluginTest extends \Test\TestCase {
- public const ID_PROPERTYNAME = \OCA\DAV\SystemTag\SystemTagPlugin::ID_PROPERTYNAME;
- public const DISPLAYNAME_PROPERTYNAME = \OCA\DAV\SystemTag\SystemTagPlugin::DISPLAYNAME_PROPERTYNAME;
- public const USERVISIBLE_PROPERTYNAME = \OCA\DAV\SystemTag\SystemTagPlugin::USERVISIBLE_PROPERTYNAME;
- public const USERASSIGNABLE_PROPERTYNAME = \OCA\DAV\SystemTag\SystemTagPlugin::USERASSIGNABLE_PROPERTYNAME;
- public const CANASSIGN_PROPERTYNAME = \OCA\DAV\SystemTag\SystemTagPlugin::CANASSIGN_PROPERTYNAME;
- public const GROUPS_PROPERTYNAME = \OCA\DAV\SystemTag\SystemTagPlugin::GROUPS_PROPERTYNAME;
-
- /**
- * @var \Sabre\DAV\Server
- */
- private $server;
-
- /**
- * @var \Sabre\DAV\Tree
- */
- private $tree;
-
- /**
- * @var \OCP\SystemTag\ISystemTagManager
- */
- private $tagManager;
-
- /**
- * @var IGroupManager
- */
- private $groupManager;
-
- /**
- * @var IUserSession
- */
- private $userSession;
-
- /**
- * @var IUser
- */
- private $user;
-
- /**
- * @var \OCA\DAV\SystemTag\SystemTagPlugin
- */
- private $plugin;
+ public const ID_PROPERTYNAME = SystemTagPlugin::ID_PROPERTYNAME;
+ public const DISPLAYNAME_PROPERTYNAME = SystemTagPlugin::DISPLAYNAME_PROPERTYNAME;
+ public const USERVISIBLE_PROPERTYNAME = SystemTagPlugin::USERVISIBLE_PROPERTYNAME;
+ public const USERASSIGNABLE_PROPERTYNAME = SystemTagPlugin::USERASSIGNABLE_PROPERTYNAME;
+ public const CANASSIGN_PROPERTYNAME = SystemTagPlugin::CANASSIGN_PROPERTYNAME;
+ public const GROUPS_PROPERTYNAME = SystemTagPlugin::GROUPS_PROPERTYNAME;
+
+ private \Sabre\DAV\Server $server;
+ private \Sabre\DAV\Tree&MockObject $tree;
+ private ISystemTagManager&MockObject $tagManager;
+ private IGroupManager&MockObject $groupManager;
+ private IUserSession&MockObject $userSession;
+ private IRootFolder&MockObject $rootFolder;
+ private IUser&MockObject $user;
+ private ISystemTagObjectMapper&MockObject $tagMapper;
+ private SystemTagPlugin $plugin;
protected function setUp(): void {
parent::setUp();
- $this->tree = $this->getMockBuilder(Tree::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $this->tree = $this->createMock(Tree::class);
$this->server = new \Sabre\DAV\Server($this->tree);
- $this->tagManager = $this->getMockBuilder(ISystemTagManager::class)
- ->getMock();
- $this->groupManager = $this->getMockBuilder(IGroupManager::class)
- ->getMock();
- $this->user = $this->getMockBuilder(IUser::class)
- ->getMock();
- $this->userSession = $this->getMockBuilder(IUserSession::class)
- ->getMock();
+ $this->tagManager = $this->createMock(ISystemTagManager::class);
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->user = $this->createMock(IUser::class);
+ $this->userSession = $this->createMock(IUserSession::class);
$this->userSession
->expects($this->any())
->method('getUser')
@@ -109,18 +63,23 @@ class SystemTagPluginTest extends \Test\TestCase {
->method('isLoggedIn')
->willReturn(true);
- $this->plugin = new \OCA\DAV\SystemTag\SystemTagPlugin(
+ $this->tagMapper = $this->createMock(ISystemTagObjectMapper::class);
+ $this->rootFolder = $this->createMock(IRootFolder::class);
+
+ $this->plugin = new SystemTagPlugin(
$this->tagManager,
$this->groupManager,
- $this->userSession
+ $this->userSession,
+ $this->rootFolder,
+ $this->tagMapper
);
$this->plugin->initialize($this->server);
}
- public function getPropertiesDataProvider() {
+ public static function getPropertiesDataProvider(): array {
return [
[
- new SystemTag(1, 'Test', true, true),
+ new SystemTag('1', 'Test', true, true),
[],
[
self::ID_PROPERTYNAME,
@@ -138,7 +97,7 @@ class SystemTagPluginTest extends \Test\TestCase {
]
],
[
- new SystemTag(1, 'Test', true, false),
+ new SystemTag('1', 'Test', true, false),
[],
[
self::ID_PROPERTYNAME,
@@ -156,7 +115,7 @@ class SystemTagPluginTest extends \Test\TestCase {
]
],
[
- new SystemTag(1, 'Test', true, false),
+ new SystemTag('1', 'Test', true, false),
['group1', 'group2'],
[
self::ID_PROPERTYNAME,
@@ -168,7 +127,7 @@ class SystemTagPluginTest extends \Test\TestCase {
]
],
[
- new SystemTag(1, 'Test', true, true),
+ new SystemTag('1', 'Test', true, true),
['group1', 'group2'],
[
self::ID_PROPERTYNAME,
@@ -183,10 +142,8 @@ class SystemTagPluginTest extends \Test\TestCase {
];
}
- /**
- * @dataProvider getPropertiesDataProvider
- */
- public function testGetProperties(ISystemTag $systemTag, $groups, $requestedProperties, $expectedProperties) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('getPropertiesDataProvider')]
+ public function testGetProperties(ISystemTag $systemTag, array $groups, array $requestedProperties, array $expectedProperties): void {
$this->user->expects($this->any())
->method('getUID')
->willReturn('admin');
@@ -233,11 +190,11 @@ class SystemTagPluginTest extends \Test\TestCase {
$this->assertEquals($expectedProperties, $result[200]);
}
-
- public function testGetPropertiesForbidden() {
+
+ public function testGetPropertiesForbidden(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
- $systemTag = new SystemTag(1, 'Test', true, false);
+ $systemTag = new SystemTag('1', 'Test', true, false);
$requestedProperties = [
self::ID_PROPERTYNAME,
self::GROUPS_PROPERTYNAME,
@@ -275,8 +232,8 @@ class SystemTagPluginTest extends \Test\TestCase {
);
}
- public function testUpdatePropertiesAdmin() {
- $systemTag = new SystemTag(1, 'Test', true, false);
+ public function testUpdatePropertiesAdmin(): void {
+ $systemTag = new SystemTag('1', 'Test', true, false);
$this->user->expects($this->any())
->method('getUID')
->willReturn('admin');
@@ -330,11 +287,11 @@ class SystemTagPluginTest extends \Test\TestCase {
$this->assertEquals(200, $result[self::USERVISIBLE_PROPERTYNAME]);
}
-
- public function testUpdatePropertiesForbidden() {
+
+ public function testUpdatePropertiesForbidden(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
- $systemTag = new SystemTag(1, 'Test', true, false);
+ $systemTag = new SystemTag('1', 'Test', true, false);
$this->user->expects($this->any())
->method('getUID')
->willReturn('admin');
@@ -375,17 +332,15 @@ class SystemTagPluginTest extends \Test\TestCase {
$propPatch->commit();
}
- public function createTagInsufficientPermissionsProvider() {
+ public static function createTagInsufficientPermissionsProvider(): array {
return [
[true, false, ''],
[false, true, ''],
[true, true, 'group1|group2'],
];
}
- /**
- * @dataProvider createTagInsufficientPermissionsProvider
- */
- public function testCreateNotAssignableTagAsRegularUser($userVisible, $userAssignable, $groups) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('createTagInsufficientPermissionsProvider')]
+ public function testCreateNotAssignableTagAsRegularUser(bool $userVisible, bool $userAssignable, string $groups): void {
$this->expectException(\Sabre\DAV\Exception\BadRequest::class);
$this->expectExceptionMessage('Not sufficient permissions');
@@ -408,9 +363,7 @@ class SystemTagPluginTest extends \Test\TestCase {
}
$requestData = json_encode($requestData);
- $node = $this->getMockBuilder(SystemTagsByIdCollection::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $node = $this->createMock(SystemTagsByIdCollection::class);
$this->tagManager->expects($this->never())
->method('createTag');
$this->tagManager->expects($this->never())
@@ -421,12 +374,8 @@ class SystemTagPluginTest extends \Test\TestCase {
->with('/systemtags')
->willReturn($node);
- $request = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
$request->expects($this->once())
->method('getPath')
@@ -444,8 +393,8 @@ class SystemTagPluginTest extends \Test\TestCase {
$this->plugin->httpPost($request, $response);
}
- public function testCreateTagInByIdCollectionAsRegularUser() {
- $systemTag = new SystemTag(1, 'Test', true, false);
+ public function testCreateTagInByIdCollectionAsRegularUser(): void {
+ $systemTag = new SystemTag('1', 'Test', true, false);
$requestData = json_encode([
'name' => 'Test',
@@ -453,9 +402,7 @@ class SystemTagPluginTest extends \Test\TestCase {
'userAssignable' => true,
]);
- $node = $this->getMockBuilder(SystemTagsByIdCollection::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $node = $this->createMock(SystemTagsByIdCollection::class);
$this->tagManager->expects($this->once())
->method('createTag')
->with('Test', true, true)
@@ -466,12 +413,8 @@ class SystemTagPluginTest extends \Test\TestCase {
->with('/systemtags')
->willReturn($node);
- $request = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
$request->expects($this->once())
->method('getPath')
@@ -497,7 +440,7 @@ class SystemTagPluginTest extends \Test\TestCase {
$this->plugin->httpPost($request, $response);
}
- public function createTagProvider() {
+ public static function createTagProvider(): array {
return [
[true, false, ''],
[false, false, ''],
@@ -505,10 +448,8 @@ class SystemTagPluginTest extends \Test\TestCase {
];
}
- /**
- * @dataProvider createTagProvider
- */
- public function testCreateTagInByIdCollection($userVisible, $userAssignable, $groups) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('createTagProvider')]
+ public function testCreateTagInByIdCollection(bool $userVisible, bool $userAssignable, string $groups): void {
$this->user->expects($this->once())
->method('getUID')
->willReturn('admin');
@@ -518,7 +459,7 @@ class SystemTagPluginTest extends \Test\TestCase {
->with('admin')
->willReturn(true);
- $systemTag = new SystemTag(1, 'Test', true, false);
+ $systemTag = new SystemTag('1', 'Test', true, false);
$requestData = [
'name' => 'Test',
@@ -530,14 +471,12 @@ class SystemTagPluginTest extends \Test\TestCase {
}
$requestData = json_encode($requestData);
- $node = $this->getMockBuilder(SystemTagsByIdCollection::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $node = $this->createMock(SystemTagsByIdCollection::class);
$this->tagManager->expects($this->once())
->method('createTag')
->with('Test', $userVisible, $userAssignable)
->willReturn($systemTag);
-
+
if (!empty($groups)) {
$this->tagManager->expects($this->once())
->method('setTagGroups')
@@ -553,12 +492,8 @@ class SystemTagPluginTest extends \Test\TestCase {
->with('/systemtags')
->willReturn($node);
- $request = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
$request->expects($this->once())
->method('getPath')
@@ -584,14 +519,14 @@ class SystemTagPluginTest extends \Test\TestCase {
$this->plugin->httpPost($request, $response);
}
- public function nodeClassProvider() {
+ public static function nodeClassProvider(): array {
return [
['\OCA\DAV\SystemTag\SystemTagsByIdCollection'],
['\OCA\DAV\SystemTag\SystemTagsObjectMappingCollection'],
];
}
- public function testCreateTagInMappingCollection() {
+ public function testCreateTagInMappingCollection(): void {
$this->user->expects($this->once())
->method('getUID')
->willReturn('admin');
@@ -601,7 +536,7 @@ class SystemTagPluginTest extends \Test\TestCase {
->with('admin')
->willReturn(true);
- $systemTag = new SystemTag(1, 'Test', true, false);
+ $systemTag = new SystemTag('1', 'Test', true, false);
$requestData = json_encode([
'name' => 'Test',
@@ -609,9 +544,7 @@ class SystemTagPluginTest extends \Test\TestCase {
'userAssignable' => false,
]);
- $node = $this->getMockBuilder(SystemTagsObjectMappingCollection::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $node = $this->createMock(SystemTagsObjectMappingCollection::class);
$this->tagManager->expects($this->once())
->method('createTag')
@@ -627,12 +560,8 @@ class SystemTagPluginTest extends \Test\TestCase {
->method('createFile')
->with(1);
- $request = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
$request->expects($this->once())
->method('getPath')
@@ -658,17 +587,15 @@ class SystemTagPluginTest extends \Test\TestCase {
$this->plugin->httpPost($request, $response);
}
-
- public function testCreateTagToUnknownNode() {
+
+ public function testCreateTagToUnknownNode(): void {
$this->expectException(\Sabre\DAV\Exception\NotFound::class);
- $node = $this->getMockBuilder(SystemTagsObjectMappingCollection::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $node = $this->createMock(SystemTagsObjectMappingCollection::class);
$this->tree->expects($this->any())
->method('getNodeForPath')
- ->will($this->throwException(new \Sabre\DAV\Exception\NotFound()));
+ ->willThrowException(new \Sabre\DAV\Exception\NotFound());
$this->tagManager->expects($this->never())
->method('createTag');
@@ -676,12 +603,8 @@ class SystemTagPluginTest extends \Test\TestCase {
$node->expects($this->never())
->method('createFile');
- $request = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
$request->expects($this->once())
->method('getPath')
@@ -690,10 +613,8 @@ class SystemTagPluginTest extends \Test\TestCase {
$this->plugin->httpPost($request, $response);
}
- /**
- * @dataProvider nodeClassProvider
- */
- public function testCreateTagConflict($nodeClass) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('nodeClassProvider')]
+ public function testCreateTagConflict(string $nodeClass): void {
$this->expectException(\Sabre\DAV\Exception\Conflict::class);
$this->user->expects($this->once())
@@ -711,25 +632,19 @@ class SystemTagPluginTest extends \Test\TestCase {
'userAssignable' => false,
]);
- $node = $this->getMockBuilder($nodeClass)
- ->disableOriginalConstructor()
- ->getMock();
+ $node = $this->createMock($nodeClass);
$this->tagManager->expects($this->once())
->method('createTag')
->with('Test', true, false)
- ->will($this->throwException(new TagAlreadyExistsException('Tag already exists')));
+ ->willThrowException(new TagAlreadyExistsException('Tag already exists'));
$this->tree->expects($this->any())
->method('getNodeForPath')
->with('/systemtags')
->willReturn($node);
- $request = $this->getMockBuilder(RequestInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- $response = $this->getMockBuilder(ResponseInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $request = $this->createMock(RequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
$request->expects($this->once())
->method('getPath')
diff --git a/apps/dav/tests/unit/SystemTag/SystemTagsByIdCollectionTest.php b/apps/dav/tests/unit/SystemTag/SystemTagsByIdCollectionTest.php
index 72da59dfcce..8f7848452fe 100644
--- a/apps/dav/tests/unit/SystemTag/SystemTagsByIdCollectionTest.php
+++ b/apps/dav/tests/unit/SystemTag/SystemTagsByIdCollectionTest.php
@@ -1,100 +1,82 @@
<?php
+
+declare(strict_types=1);
/**
- * @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\Tests\unit\SystemTag;
use OC\SystemTag\SystemTag;
+use OCA\DAV\SystemTag\SystemTagsByIdCollection;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserSession;
use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\ISystemTagObjectMapper;
use OCP\SystemTag\TagNotFoundException;
+use PHPUnit\Framework\MockObject\MockObject;
class SystemTagsByIdCollectionTest extends \Test\TestCase {
-
- /**
- * @var \OCP\SystemTag\ISystemTagManager
- */
- private $tagManager;
-
- /**
- * @var \OCP\IUser
- */
- private $user;
+ private ISystemTagManager&MockObject $tagManager;
+ private IUser&MockObject $user;
protected function setUp(): void {
parent::setUp();
- $this->tagManager = $this->getMockBuilder(ISystemTagManager::class)
- ->getMock();
+ $this->tagManager = $this->createMock(ISystemTagManager::class);
}
- public function getNode($isAdmin = true) {
- $this->user = $this->getMockBuilder(IUser::class)
- ->getMock();
+ public function getNode(bool $isAdmin = true) {
+ $this->user = $this->createMock(IUser::class);
$this->user->expects($this->any())
->method('getUID')
->willReturn('testuser');
- $userSession = $this->getMockBuilder(IUserSession::class)
- ->getMock();
+
+ /** @var IUserSession&MockObject */
+ $userSession = $this->createMock(IUserSession::class);
$userSession->expects($this->any())
->method('getUser')
->willReturn($this->user);
- $groupManager = $this->getMockBuilder(IGroupManager::class)
- ->getMock();
+
+ /** @var IGroupManager&MockObject */
+ $groupManager = $this->createMock(IGroupManager::class);
$groupManager->expects($this->any())
->method('isAdmin')
->with('testuser')
->willReturn($isAdmin);
- return new \OCA\DAV\SystemTag\SystemTagsByIdCollection(
+
+ /** @var ISystemTagObjectMapper&MockObject */
+ $tagMapper = $this->createMock(ISystemTagObjectMapper::class);
+ return new SystemTagsByIdCollection(
$this->tagManager,
$userSession,
- $groupManager
+ $groupManager,
+ $tagMapper,
);
}
- public function adminFlagProvider() {
+ public static function adminFlagProvider(): array {
return [[true], [false]];
}
-
- public function testForbiddenCreateFile() {
+
+ public function testForbiddenCreateFile(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$this->getNode()->createFile('555');
}
-
- public function testForbiddenCreateDirectory() {
+
+ public function testForbiddenCreateDirectory(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$this->getNode()->createDirectory('789');
}
- public function testGetChild() {
- $tag = new SystemTag(123, 'Test', true, false);
+ public function testGetChild(): void {
+ $tag = new SystemTag('123', 'Test', true, false);
$this->tagManager->expects($this->once())
->method('canUserSeeTag')
->with($tag)
@@ -112,35 +94,35 @@ class SystemTagsByIdCollectionTest extends \Test\TestCase {
$this->assertEquals($tag, $childNode->getSystemTag());
}
-
- public function testGetChildInvalidName() {
+
+ public function testGetChildInvalidName(): void {
$this->expectException(\Sabre\DAV\Exception\BadRequest::class);
$this->tagManager->expects($this->once())
->method('getTagsByIds')
->with(['invalid'])
- ->will($this->throwException(new \InvalidArgumentException()));
+ ->willThrowException(new \InvalidArgumentException());
$this->getNode()->getChild('invalid');
}
-
- public function testGetChildNotFound() {
+
+ public function testGetChildNotFound(): void {
$this->expectException(\Sabre\DAV\Exception\NotFound::class);
$this->tagManager->expects($this->once())
->method('getTagsByIds')
->with(['444'])
- ->will($this->throwException(new TagNotFoundException()));
+ ->willThrowException(new TagNotFoundException());
$this->getNode()->getChild('444');
}
-
- public function testGetChildUserNotVisible() {
+
+ public function testGetChildUserNotVisible(): void {
$this->expectException(\Sabre\DAV\Exception\NotFound::class);
- $tag = new SystemTag(123, 'Test', false, false);
+ $tag = new SystemTag('123', 'Test', false, false);
$this->tagManager->expects($this->once())
->method('getTagsByIds')
@@ -150,9 +132,9 @@ class SystemTagsByIdCollectionTest extends \Test\TestCase {
$this->getNode(false)->getChild('123');
}
- public function testGetChildrenAdmin() {
- $tag1 = new SystemTag(123, 'One', true, false);
- $tag2 = new SystemTag(456, 'Two', true, true);
+ public function testGetChildrenAdmin(): void {
+ $tag1 = new SystemTag('123', 'One', true, false);
+ $tag2 = new SystemTag('456', 'Two', true, true);
$this->tagManager->expects($this->once())
->method('getAllTags')
@@ -169,9 +151,9 @@ class SystemTagsByIdCollectionTest extends \Test\TestCase {
$this->assertEquals($tag2, $children[1]->getSystemTag());
}
- public function testGetChildrenNonAdmin() {
- $tag1 = new SystemTag(123, 'One', true, false);
- $tag2 = new SystemTag(456, 'Two', true, true);
+ public function testGetChildrenNonAdmin(): void {
+ $tag1 = new SystemTag('123', 'One', true, false);
+ $tag2 = new SystemTag('456', 'Two', true, true);
$this->tagManager->expects($this->once())
->method('getAllTags')
@@ -188,7 +170,7 @@ class SystemTagsByIdCollectionTest extends \Test\TestCase {
$this->assertEquals($tag2, $children[1]->getSystemTag());
}
- public function testGetChildrenEmpty() {
+ public function testGetChildrenEmpty(): void {
$this->tagManager->expects($this->once())
->method('getAllTags')
->with(null)
@@ -196,18 +178,16 @@ class SystemTagsByIdCollectionTest extends \Test\TestCase {
$this->assertCount(0, $this->getNode()->getChildren());
}
- public function childExistsProvider() {
+ public static function childExistsProvider(): array {
return [
[true, true],
[false, false],
];
}
- /**
- * @dataProvider childExistsProvider
- */
- public function testChildExists($userVisible, $expectedResult) {
- $tag = new SystemTag(123, 'One', $userVisible, false);
+ #[\PHPUnit\Framework\Attributes\DataProvider('childExistsProvider')]
+ public function testChildExists(bool $userVisible, bool $expectedResult): void {
+ $tag = new SystemTag('123', 'One', $userVisible, false);
$this->tagManager->expects($this->once())
->method('canUserSeeTag')
->with($tag)
@@ -221,23 +201,23 @@ class SystemTagsByIdCollectionTest extends \Test\TestCase {
$this->assertEquals($expectedResult, $this->getNode()->childExists('123'));
}
- public function testChildExistsNotFound() {
+ public function testChildExistsNotFound(): void {
$this->tagManager->expects($this->once())
->method('getTagsByIds')
->with(['123'])
- ->will($this->throwException(new TagNotFoundException()));
+ ->willThrowException(new TagNotFoundException());
$this->assertFalse($this->getNode()->childExists('123'));
}
-
- public function testChildExistsBadRequest() {
+
+ public function testChildExistsBadRequest(): void {
$this->expectException(\Sabre\DAV\Exception\BadRequest::class);
$this->tagManager->expects($this->once())
->method('getTagsByIds')
->with(['invalid'])
- ->will($this->throwException(new \InvalidArgumentException()));
+ ->willThrowException(new \InvalidArgumentException());
$this->getNode()->childExists('invalid');
}
diff --git a/apps/dav/tests/unit/SystemTag/SystemTagsObjectMappingCollectionTest.php b/apps/dav/tests/unit/SystemTag/SystemTagsObjectMappingCollectionTest.php
index bb71de8ea8e..5aea1242e2a 100644
--- a/apps/dav/tests/unit/SystemTag/SystemTagsObjectMappingCollectionTest.php
+++ b/apps/dav/tests/unit/SystemTag/SystemTagsObjectMappingCollectionTest.php
@@ -1,76 +1,46 @@
<?php
+
+declare(strict_types=1);
/**
- * @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\Tests\unit\SystemTag;
use OC\SystemTag\SystemTag;
+use OCA\DAV\SystemTag\SystemTagsObjectMappingCollection;
use OCP\IUser;
use OCP\SystemTag\ISystemTagManager;
use OCP\SystemTag\ISystemTagObjectMapper;
use OCP\SystemTag\TagNotFoundException;
+use PHPUnit\Framework\MockObject\MockObject;
class SystemTagsObjectMappingCollectionTest extends \Test\TestCase {
-
- /**
- * @var \OCP\SystemTag\ISystemTagManager
- */
- private $tagManager;
-
- /**
- * @var \OCP\SystemTag\ISystemTagObjectMapper
- */
- private $tagMapper;
-
- /**
- * @var \OCP\IUser
- */
- private $user;
+ private ISystemTagManager&MockObject $tagManager;
+ private ISystemTagObjectMapper&MockObject $tagMapper;
+ private IUser&MockObject $user;
protected function setUp(): void {
parent::setUp();
- $this->tagManager = $this->getMockBuilder(ISystemTagManager::class)
- ->getMock();
- $this->tagMapper = $this->getMockBuilder(ISystemTagObjectMapper::class)
- ->getMock();
-
- $this->user = $this->getMockBuilder(IUser::class)
- ->getMock();
+ $this->tagManager = $this->createMock(ISystemTagManager::class);
+ $this->tagMapper = $this->createMock(ISystemTagObjectMapper::class);
+ $this->user = $this->createMock(IUser::class);
}
- public function getNode() {
- return new \OCA\DAV\SystemTag\SystemTagsObjectMappingCollection(
- 111,
+ public function getNode(array $writableNodeIds = []): SystemTagsObjectMappingCollection {
+ return new SystemTagsObjectMappingCollection(
+ '111',
'files',
$this->user,
$this->tagManager,
- $this->tagMapper
+ $this->tagMapper,
+ fn ($id): bool => in_array($id, $writableNodeIds),
);
}
- public function testAssignTag() {
+ public function testAssignTag(): void {
$tag = new SystemTag('1', 'Test', true, true);
$this->tagManager->expects($this->once())
->method('canUserSeeTag')
@@ -89,10 +59,32 @@ class SystemTagsObjectMappingCollectionTest extends \Test\TestCase {
->method('assignTags')
->with(111, 'files', '555');
+ $this->getNode([111])->createFile('555');
+ }
+
+ public function testAssignTagForbidden(): void {
+ $tag = new SystemTag('1', 'Test', true, true);
+ $this->tagManager->expects($this->once())
+ ->method('canUserSeeTag')
+ ->with($tag)
+ ->willReturn(true);
+ $this->tagManager->expects($this->once())
+ ->method('canUserAssignTag')
+ ->with($tag)
+ ->willReturn(true);
+
+ $this->tagManager->expects($this->once())
+ ->method('getTagsByIds')
+ ->with(['555'])
+ ->willReturn([$tag]);
+ $this->tagMapper->expects($this->never())
+ ->method('assignTags');
+
+ $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$this->getNode()->createFile('555');
}
- public function permissionsProvider() {
+ public static function permissionsProvider(): array {
return [
// invisible, tag does not exist for user
[false, true, '\Sabre\DAV\Exception\PreconditionFailed'],
@@ -101,10 +93,8 @@ class SystemTagsObjectMappingCollectionTest extends \Test\TestCase {
];
}
- /**
- * @dataProvider permissionsProvider
- */
- public function testAssignTagNoPermission($userVisible, $userAssignable, $expectedException) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('permissionsProvider')]
+ public function testAssignTagNoPermission(bool $userVisible, bool $userAssignable, string $expectedException): void {
$tag = new SystemTag('1', 'Test', $userVisible, $userAssignable);
$this->tagManager->expects($this->once())
->method('canUserSeeTag')
@@ -132,27 +122,27 @@ class SystemTagsObjectMappingCollectionTest extends \Test\TestCase {
$this->assertInstanceOf($expectedException, $thrown);
}
-
- public function testAssignTagNotFound() {
+
+ public function testAssignTagNotFound(): void {
$this->expectException(\Sabre\DAV\Exception\PreconditionFailed::class);
$this->tagManager->expects($this->once())
->method('getTagsByIds')
->with(['555'])
- ->will($this->throwException(new TagNotFoundException()));
+ ->willThrowException(new TagNotFoundException());
$this->getNode()->createFile('555');
}
-
- public function testForbiddenCreateDirectory() {
+
+ public function testForbiddenCreateDirectory(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$this->getNode()->createDirectory('789');
}
- public function testGetChild() {
- $tag = new SystemTag(555, 'TheTag', true, false);
+ public function testGetChild(): void {
+ $tag = new SystemTag('555', 'TheTag', true, false);
$this->tagManager->expects($this->once())
->method('canUserSeeTag')
->with($tag)
@@ -174,11 +164,11 @@ class SystemTagsObjectMappingCollectionTest extends \Test\TestCase {
$this->assertEquals('555', $childNode->getName());
}
-
- public function testGetChildNonVisible() {
+
+ public function testGetChildNonVisible(): void {
$this->expectException(\Sabre\DAV\Exception\NotFound::class);
- $tag = new SystemTag(555, 'TheTag', false, false);
+ $tag = new SystemTag('555', 'TheTag', false, false);
$this->tagManager->expects($this->once())
->method('canUserSeeTag')
->with($tag)
@@ -197,8 +187,8 @@ class SystemTagsObjectMappingCollectionTest extends \Test\TestCase {
$this->getNode()->getChild('555');
}
-
- public function testGetChildRelationNotFound() {
+
+ public function testGetChildRelationNotFound(): void {
$this->expectException(\Sabre\DAV\Exception\NotFound::class);
$this->tagMapper->expects($this->once())
@@ -209,34 +199,34 @@ class SystemTagsObjectMappingCollectionTest extends \Test\TestCase {
$this->getNode()->getChild('777');
}
-
- public function testGetChildInvalidId() {
+
+ public function testGetChildInvalidId(): void {
$this->expectException(\Sabre\DAV\Exception\BadRequest::class);
$this->tagMapper->expects($this->once())
->method('haveTag')
->with([111], 'files', 'badid')
- ->will($this->throwException(new \InvalidArgumentException()));
+ ->willThrowException(new \InvalidArgumentException());
$this->getNode()->getChild('badid');
}
-
- public function testGetChildTagDoesNotExist() {
+
+ public function testGetChildTagDoesNotExist(): void {
$this->expectException(\Sabre\DAV\Exception\NotFound::class);
$this->tagMapper->expects($this->once())
->method('haveTag')
->with([111], 'files', '777')
- ->will($this->throwException(new TagNotFoundException()));
+ ->willThrowException(new TagNotFoundException());
$this->getNode()->getChild('777');
}
- public function testGetChildren() {
- $tag1 = new SystemTag(555, 'TagOne', true, false);
- $tag2 = new SystemTag(556, 'TagTwo', true, true);
- $tag3 = new SystemTag(557, 'InvisibleTag', false, true);
+ public function testGetChildren(): void {
+ $tag1 = new SystemTag('555', 'TagOne', true, false);
+ $tag2 = new SystemTag('556', 'TagTwo', true, true);
+ $tag3 = new SystemTag('557', 'InvisibleTag', false, true);
$this->tagMapper->expects($this->once())
->method('getTagIdsForObjects')
@@ -270,8 +260,8 @@ class SystemTagsObjectMappingCollectionTest extends \Test\TestCase {
$this->assertEquals($tag2, $children[1]->getSystemTag());
}
- public function testChildExistsWithVisibleTag() {
- $tag = new SystemTag(555, 'TagOne', true, false);
+ public function testChildExistsWithVisibleTag(): void {
+ $tag = new SystemTag('555', 'TagOne', true, false);
$this->tagMapper->expects($this->once())
->method('haveTag')
@@ -291,8 +281,8 @@ class SystemTagsObjectMappingCollectionTest extends \Test\TestCase {
$this->assertTrue($this->getNode()->childExists('555'));
}
- public function testChildExistsWithInvisibleTag() {
- $tag = new SystemTag(555, 'TagOne', false, false);
+ public function testChildExistsWithInvisibleTag(): void {
+ $tag = new SystemTag('555', 'TagOne', false, false);
$this->tagMapper->expects($this->once())
->method('haveTag')
@@ -307,7 +297,7 @@ class SystemTagsObjectMappingCollectionTest extends \Test\TestCase {
$this->assertFalse($this->getNode()->childExists('555'));
}
- public function testChildExistsNotFound() {
+ public function testChildExistsNotFound(): void {
$this->tagMapper->expects($this->once())
->method('haveTag')
->with([111], 'files', '555')
@@ -316,42 +306,42 @@ class SystemTagsObjectMappingCollectionTest extends \Test\TestCase {
$this->assertFalse($this->getNode()->childExists('555'));
}
- public function testChildExistsTagNotFound() {
+ public function testChildExistsTagNotFound(): void {
$this->tagMapper->expects($this->once())
->method('haveTag')
->with([111], 'files', '555')
- ->will($this->throwException(new TagNotFoundException()));
+ ->willThrowException(new TagNotFoundException());
$this->assertFalse($this->getNode()->childExists('555'));
}
-
- public function testChildExistsInvalidId() {
+
+ public function testChildExistsInvalidId(): void {
$this->expectException(\Sabre\DAV\Exception\BadRequest::class);
$this->tagMapper->expects($this->once())
->method('haveTag')
->with([111], 'files', '555')
- ->will($this->throwException(new \InvalidArgumentException()));
+ ->willThrowException(new \InvalidArgumentException());
$this->getNode()->childExists('555');
}
-
- public function testDelete() {
+
+ public function testDelete(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$this->getNode()->delete();
}
-
- public function testSetName() {
+
+ public function testSetName(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$this->getNode()->setName('somethingelse');
}
- public function testGetName() {
+ public function testGetName(): void {
$this->assertEquals('111', $this->getNode()->getName());
}
}
diff --git a/apps/dav/tests/unit/SystemTag/SystemTagsObjectTypeCollectionTest.php b/apps/dav/tests/unit/SystemTag/SystemTagsObjectTypeCollectionTest.php
index 11c9fc5977c..301eb528436 100644
--- a/apps/dav/tests/unit/SystemTag/SystemTagsObjectTypeCollectionTest.php
+++ b/apps/dav/tests/unit/SystemTag/SystemTagsObjectTypeCollectionTest.php
@@ -1,177 +1,151 @@
<?php
+
+declare(strict_types=1);
/**
- * @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\Tests\unit\SystemTag;
+use OCA\DAV\SystemTag\SystemTagsObjectTypeCollection;
use OCP\Files\Folder;
+use OCP\Files\Node;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserSession;
use OCP\SystemTag\ISystemTagManager;
use OCP\SystemTag\ISystemTagObjectMapper;
+use PHPUnit\Framework\MockObject\MockObject;
class SystemTagsObjectTypeCollectionTest extends \Test\TestCase {
-
- /**
- * @var \OCA\DAV\SystemTag\SystemTagsObjectTypeCollection
- */
- private $node;
-
- /**
- * @var \OCP\SystemTag\ISystemTagManager
- */
- private $tagManager;
-
- /**
- * @var \OCP\SystemTag\ISystemTagObjectMapper
- */
- private $tagMapper;
-
- /**
- * @var \OCP\Files\Folder
- */
- private $userFolder;
+ private ISystemTagManager&MockObject $tagManager;
+ private ISystemTagObjectMapper&MockObject $tagMapper;
+ private Folder&MockObject $userFolder;
+ private SystemTagsObjectTypeCollection $node;
protected function setUp(): void {
parent::setUp();
- $this->tagManager = $this->getMockBuilder(ISystemTagManager::class)
- ->getMock();
- $this->tagMapper = $this->getMockBuilder(ISystemTagObjectMapper::class)
- ->getMock();
+ $this->tagManager = $this->createMock(ISystemTagManager::class);
+ $this->tagMapper = $this->createMock(ISystemTagObjectMapper::class);
- $user = $this->getMockBuilder(IUser::class)
- ->getMock();
+ $user = $this->createMock(IUser::class);
$user->expects($this->any())
->method('getUID')
->willReturn('testuser');
- $userSession = $this->getMockBuilder(IUserSession::class)
- ->getMock();
+ $userSession = $this->createMock(IUserSession::class);
$userSession->expects($this->any())
->method('getUser')
->willReturn($user);
- $groupManager = $this->getMockBuilder(IGroupManager::class)
- ->getMock();
+ $groupManager = $this->createMock(IGroupManager::class);
$groupManager->expects($this->any())
->method('isAdmin')
->with('testuser')
->willReturn(true);
- $this->userFolder = $this->getMockBuilder(Folder::class)
- ->getMock();
+ $this->userFolder = $this->createMock(Folder::class);
$userFolder = $this->userFolder;
$closure = function ($name) use ($userFolder) {
- $nodes = $userFolder->getById(intval($name));
- return !empty($nodes);
+ $node = $userFolder->getFirstNodeById((int)$name);
+ return $node !== null;
+ };
+ $writeAccessClosure = function ($name) use ($userFolder) {
+ $nodes = $userFolder->getById((int)$name);
+ foreach ($nodes as $node) {
+ if (($node->getPermissions() & Constants::PERMISSION_UPDATE) === Constants::PERMISSION_UPDATE) {
+ return true;
+ }
+ }
+ return false;
};
- $this->node = new \OCA\DAV\SystemTag\SystemTagsObjectTypeCollection(
+ $this->node = new SystemTagsObjectTypeCollection(
'files',
$this->tagManager,
$this->tagMapper,
$userSession,
$groupManager,
- $closure
+ $closure,
+ $writeAccessClosure,
);
}
-
- public function testForbiddenCreateFile() {
+
+ public function testForbiddenCreateFile(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$this->node->createFile('555');
}
-
- public function testForbiddenCreateDirectory() {
+
+ public function testForbiddenCreateDirectory(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$this->node->createDirectory('789');
}
- public function testGetChild() {
+ public function testGetChild(): void {
$this->userFolder->expects($this->once())
- ->method('getById')
+ ->method('getFirstNodeById')
->with('555')
- ->willReturn([true]);
+ ->willReturn($this->createMock(Node::class));
$childNode = $this->node->getChild('555');
$this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagsObjectMappingCollection', $childNode);
$this->assertEquals('555', $childNode->getName());
}
-
- public function testGetChildWithoutAccess() {
+
+ public function testGetChildWithoutAccess(): void {
$this->expectException(\Sabre\DAV\Exception\NotFound::class);
$this->userFolder->expects($this->once())
- ->method('getById')
+ ->method('getFirstNodeById')
->with('555')
- ->willReturn([]);
+ ->willReturn(null);
$this->node->getChild('555');
}
-
- public function testGetChildren() {
+
+ public function testGetChildren(): void {
$this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class);
$this->node->getChildren();
}
- public function testChildExists() {
+ public function testChildExists(): void {
$this->userFolder->expects($this->once())
- ->method('getById')
+ ->method('getFirstNodeById')
->with('123')
- ->willReturn([true]);
+ ->willReturn($this->createMock(Node::class));
$this->assertTrue($this->node->childExists('123'));
}
- public function testChildExistsWithoutAccess() {
+ public function testChildExistsWithoutAccess(): void {
$this->userFolder->expects($this->once())
- ->method('getById')
+ ->method('getFirstNodeById')
->with('555')
- ->willReturn([]);
+ ->willReturn(null);
$this->assertFalse($this->node->childExists('555'));
}
-
- public function testDelete() {
+
+ public function testDelete(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$this->node->delete();
}
-
- public function testSetName() {
+
+ public function testSetName(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$this->node->setName('somethingelse');
}
- public function testGetName() {
+ public function testGetName(): void {
$this->assertEquals('files', $this->node->getName());
}
}
diff --git a/apps/dav/tests/unit/Upload/AssemblyStreamTest.php b/apps/dav/tests/unit/Upload/AssemblyStreamTest.php
index c7d2fea2871..ec5d0a9ab5b 100644
--- a/apps/dav/tests/unit/Upload/AssemblyStreamTest.php
+++ b/apps/dav/tests/unit/Upload/AssemblyStreamTest.php
@@ -1,66 +1,57 @@
<?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 Markus Goetz <markus@woboq.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\Tests\unit\Upload;
+use OCA\DAV\Upload\AssemblyStream;
use Sabre\DAV\File;
class AssemblyStreamTest extends \Test\TestCase {
- /**
- * @dataProvider providesNodes()
- */
- public function testGetContents($expected, $nodes) {
- $stream = \OCA\DAV\Upload\AssemblyStream::wrap($nodes);
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesNodes')]
+ public function testGetContents(string $expected, array $nodeData): void {
+ $nodes = [];
+ foreach ($nodeData as $data) {
+ $nodes[] = $this->buildNode(...$data);
+ }
+ $stream = AssemblyStream::wrap($nodes);
$content = stream_get_contents($stream);
$this->assertEquals($expected, $content);
}
- /**
- * @dataProvider providesNodes()
- */
- public function testGetContentsFread($expected, $nodes) {
- $stream = \OCA\DAV\Upload\AssemblyStream::wrap($nodes);
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesNodes')]
+ public function testGetContentsFread(string $expected, array $nodeData, int $chunkLength = 3): void {
+ $nodes = [];
+ foreach ($nodeData as $data) {
+ $nodes[] = $this->buildNode(...$data);
+ }
+ $stream = AssemblyStream::wrap($nodes);
$content = '';
while (!feof($stream)) {
- $content .= fread($stream, 3);
+ $chunk = fread($stream, $chunkLength);
+ $content .= $chunk;
+ if ($chunkLength !== 3) {
+ $this->assertEquals($chunkLength, strlen($chunk));
+ }
}
$this->assertEquals($expected, $content);
}
- /**
- * @dataProvider providesNodes()
- */
- public function testSeek($expected, $nodes) {
- $stream = \OCA\DAV\Upload\AssemblyStream::wrap($nodes);
+ #[\PHPUnit\Framework\Attributes\DataProvider('providesNodes')]
+ public function testSeek(string $expected, array $nodeData): void {
+ $nodes = [];
+ foreach ($nodeData as $data) {
+ $nodes[] = $this->buildNode(...$data);
+ }
+
+ $stream = AssemblyStream::wrap($nodes);
$offset = floor(strlen($expected) * 0.6);
if (fseek($stream, $offset) === -1) {
@@ -71,63 +62,75 @@ class AssemblyStreamTest extends \Test\TestCase {
$this->assertEquals(substr($expected, $offset), $content);
}
- public function providesNodes() {
- $data8k = $this->makeData(8192);
- $dataLess8k = $this->makeData(8191);
+ public static function providesNodes(): array {
+ $data8k = self::makeData(8192);
+ $dataLess8k = self::makeData(8191);
$tonofnodes = [];
- $tonofdata = "";
+ $tonofdata = '';
for ($i = 0; $i < 101; $i++) {
- $thisdata = rand(0,100); // variable length and content
+ $thisdata = random_int(0, 100); // variable length and content
$tonofdata .= $thisdata;
- array_push($tonofnodes, $this->buildNode($i,$thisdata));
+ $tonofnodes[] = [(string)$i, (string)$thisdata];
}
return[
'one node zero bytes' => [
'', [
- $this->buildNode('0', '')
+ ['0', ''],
]],
'one node only' => [
'1234567890', [
- $this->buildNode('0', '1234567890')
+ ['0', '1234567890'],
]],
'one node buffer boundary' => [
$data8k, [
- $this->buildNode('0', $data8k)
+ ['0', $data8k],
]],
'two nodes' => [
'1234567890', [
- $this->buildNode('1', '67890'),
- $this->buildNode('0', '12345')
+ ['1', '67890'],
+ ['0', '12345'],
]],
'two nodes end on buffer boundary' => [
$data8k . $data8k, [
- $this->buildNode('1', $data8k),
- $this->buildNode('0', $data8k)
+ ['1', $data8k],
+ ['0', $data8k],
]],
'two nodes with one on buffer boundary' => [
$data8k . $dataLess8k, [
- $this->buildNode('1', $dataLess8k),
- $this->buildNode('0', $data8k)
+ ['1', $dataLess8k],
+ ['0', $data8k],
]],
'two nodes on buffer boundary plus one byte' => [
$data8k . 'X' . $data8k, [
- $this->buildNode('1', $data8k),
- $this->buildNode('0', $data8k . 'X')
+ ['1', $data8k],
+ ['0', $data8k . 'X'],
]],
'two nodes on buffer boundary plus one byte at the end' => [
$data8k . $data8k . 'X', [
- $this->buildNode('1', $data8k . 'X'),
- $this->buildNode('0', $data8k)
+ ['1', $data8k . 'X'],
+ ['0', $data8k],
]],
'a ton of nodes' => [
$tonofdata, $tonofnodes
- ]
+ ],
+ 'one read over multiple nodes' => [
+ '1234567890', [
+ ['0', '1234'],
+ ['1', '5678'],
+ ['2', '90'],
+ ], 10],
+ 'two reads over multiple nodes' => [
+ '1234567890', [
+ ['0', '1234'],
+ ['1', '5678'],
+ ['2', '90'],
+ ], 5],
];
}
- private function makeData($count) {
+ private static function makeData(int $count): string {
$data = '';
$base = '1234567890';
$j = 0;
@@ -141,10 +144,10 @@ class AssemblyStreamTest extends \Test\TestCase {
return $data;
}
- private function buildNode($name, $data) {
+ private function buildNode(string $name, string $data) {
$node = $this->getMockBuilder(File::class)
- ->setMethods(['getName', 'get', 'getSize'])
- ->getMockForAbstractClass();
+ ->onlyMethods(['getName', 'get', 'getSize'])
+ ->getMock();
$node->expects($this->any())
->method('getName')
diff --git a/apps/dav/tests/unit/Upload/ChunkingPluginTest.php b/apps/dav/tests/unit/Upload/ChunkingPluginTest.php
index 766b3e1f457..00ed7657dd3 100644
--- a/apps/dav/tests/unit/Upload/ChunkingPluginTest.php
+++ b/apps/dav/tests/unit/Upload/ChunkingPluginTest.php
@@ -1,70 +1,34 @@
<?php
+
+declare(strict_types=1);
/**
- * @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 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: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2017 ownCloud GmbH
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Tests\unit\Upload;
use OCA\DAV\Connector\Sabre\Directory;
use OCA\DAV\Upload\ChunkingPlugin;
use OCA\DAV\Upload\FutureFile;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\Exception\NotFound;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
use Test\TestCase;
class ChunkingPluginTest extends TestCase {
-
-
- /**
- * @var \Sabre\DAV\Server | \PHPUnit\Framework\MockObject\MockObject
- */
- private $server;
-
- /**
- * @var \Sabre\DAV\Tree | \PHPUnit\Framework\MockObject\MockObject
- */
- private $tree;
-
- /**
- * @var ChunkingPlugin
- */
- private $plugin;
- /** @var RequestInterface | \PHPUnit\Framework\MockObject\MockObject */
- private $request;
- /** @var ResponseInterface | \PHPUnit\Framework\MockObject\MockObject */
- private $response;
+ private \Sabre\DAV\Server&MockObject $server;
+ private \Sabre\DAV\Tree&MockObject $tree;
+ private ChunkingPlugin $plugin;
+ private RequestInterface&MockObject $request;
+ private ResponseInterface&MockObject $response;
protected function setUp(): void {
parent::setUp();
- $this->server = $this->getMockBuilder('\Sabre\DAV\Server')
- ->disableOriginalConstructor()
- ->getMock();
- $this->tree = $this->getMockBuilder('\Sabre\DAV\Tree')
- ->disableOriginalConstructor()
- ->getMock();
+ $this->server = $this->createMock('\Sabre\DAV\Server');
+ $this->tree = $this->createMock('\Sabre\DAV\Tree');
$this->server->tree = $this->tree;
$this->plugin = new ChunkingPlugin();
@@ -77,7 +41,7 @@ class ChunkingPluginTest extends TestCase {
$this->plugin->initialize($this->server);
}
- public function testBeforeMoveFutureFileSkip() {
+ public function testBeforeMoveFutureFileSkip(): void {
$node = $this->createMock(Directory::class);
$this->tree->expects($this->any())
@@ -90,41 +54,45 @@ class ChunkingPluginTest extends TestCase {
$this->assertNull($this->plugin->beforeMove('source', 'target'));
}
- public function testBeforeMoveDestinationIsDirectory() {
+ public function testBeforeMoveDestinationIsDirectory(): void {
$this->expectException(\Sabre\DAV\Exception\BadRequest::class);
$this->expectExceptionMessage('The given destination target is a directory.');
$sourceNode = $this->createMock(FutureFile::class);
$targetNode = $this->createMock(Directory::class);
- $this->tree->expects($this->at(0))
+ $this->tree->expects($this->exactly(2))
->method('getNodeForPath')
- ->with('source')
- ->willReturn($sourceNode);
- $this->tree->expects($this->at(1))
- ->method('getNodeForPath')
- ->with('target')
- ->willReturn($targetNode);
+ ->willReturnMap([
+ ['source', $sourceNode],
+ ['target', $targetNode],
+ ]);
$this->response->expects($this->never())
->method('setStatus');
$this->assertNull($this->plugin->beforeMove('source', 'target'));
}
- public function testBeforeMoveFutureFileSkipNonExisting() {
+ public function testBeforeMoveFutureFileSkipNonExisting(): void {
$sourceNode = $this->createMock(FutureFile::class);
$sourceNode->expects($this->once())
->method('getSize')
->willReturn(4);
- $this->tree->expects($this->at(0))
- ->method('getNodeForPath')
- ->with('source')
- ->willReturn($sourceNode);
- $this->tree->expects($this->at(1))
+ $calls = [
+ ['source', $sourceNode],
+ ['target', new NotFound()],
+ ];
+ $this->tree->expects($this->exactly(2))
->method('getNodeForPath')
- ->with('target')
- ->willThrowException(new NotFound());
+ ->willReturnCallback(function (string $path) use (&$calls) {
+ $expected = array_shift($calls);
+ $this->assertSame($expected[0], $path);
+ if ($expected[1] instanceof \Throwable) {
+ throw $expected[1];
+ }
+ return $expected[1];
+ });
$this->tree->expects($this->any())
->method('nodeExists')
->with('target')
@@ -143,20 +111,27 @@ class ChunkingPluginTest extends TestCase {
$this->assertFalse($this->plugin->beforeMove('source', 'target'));
}
- public function testBeforeMoveFutureFileMoveIt() {
+ public function testBeforeMoveFutureFileMoveIt(): void {
$sourceNode = $this->createMock(FutureFile::class);
$sourceNode->expects($this->once())
->method('getSize')
->willReturn(4);
- $this->tree->expects($this->at(0))
- ->method('getNodeForPath')
- ->with('source')
- ->willReturn($sourceNode);
- $this->tree->expects($this->at(1))
+ $calls = [
+ ['source', $sourceNode],
+ ['target', new NotFound()],
+ ];
+ $this->tree->expects($this->exactly(2))
->method('getNodeForPath')
- ->with('target')
- ->willThrowException(new NotFound());
+ ->willReturnCallback(function (string $path) use (&$calls) {
+ $expected = array_shift($calls);
+ $this->assertSame($expected[0], $path);
+ if ($expected[1] instanceof \Throwable) {
+ throw $expected[1];
+ }
+ return $expected[1];
+ });
+
$this->tree->expects($this->any())
->method('nodeExists')
->with('target')
@@ -180,7 +155,7 @@ class ChunkingPluginTest extends TestCase {
}
- public function testBeforeMoveSizeIsWrong() {
+ public function testBeforeMoveSizeIsWrong(): void {
$this->expectException(\Sabre\DAV\Exception\BadRequest::class);
$this->expectExceptionMessage('Chunks on server do not sum up to 4 but to 3 bytes');
@@ -189,14 +164,21 @@ class ChunkingPluginTest extends TestCase {
->method('getSize')
->willReturn(3);
- $this->tree->expects($this->at(0))
+ $calls = [
+ ['source', $sourceNode],
+ ['target', new NotFound()],
+ ];
+ $this->tree->expects($this->exactly(2))
->method('getNodeForPath')
- ->with('source')
- ->willReturn($sourceNode);
- $this->tree->expects($this->at(1))
- ->method('getNodeForPath')
- ->with('target')
- ->willThrowException(new NotFound());
+ ->willReturnCallback(function (string $path) use (&$calls) {
+ $expected = array_shift($calls);
+ $this->assertSame($expected[0], $path);
+ if ($expected[1] instanceof \Throwable) {
+ throw $expected[1];
+ }
+ return $expected[1];
+ });
+
$this->request->expects($this->once())
->method('getHeader')
->with('OC-Total-Length')
diff --git a/apps/dav/tests/unit/Upload/FutureFileTest.php b/apps/dav/tests/unit/Upload/FutureFileTest.php
index 0c276167e1c..1409df937c0 100644
--- a/apps/dav/tests/unit/Upload/FutureFileTest.php
+++ b/apps/dav/tests/unit/Upload/FutureFileTest.php
@@ -1,100 +1,81 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @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 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\Tests\unit\Upload;
use OCA\DAV\Connector\Sabre\Directory;
+use OCA\DAV\Upload\FutureFile;
class FutureFileTest extends \Test\TestCase {
- public function testGetContentType() {
+ public function testGetContentType(): void {
$f = $this->mockFutureFile();
$this->assertEquals('application/octet-stream', $f->getContentType());
}
- public function testGetETag() {
+ public function testGetETag(): void {
$f = $this->mockFutureFile();
$this->assertEquals('1234567890', $f->getETag());
}
- public function testGetName() {
+ public function testGetName(): void {
$f = $this->mockFutureFile();
$this->assertEquals('foo.txt', $f->getName());
}
- public function testGetLastModified() {
+ public function testGetLastModified(): void {
$f = $this->mockFutureFile();
$this->assertEquals(12121212, $f->getLastModified());
}
- public function testGetSize() {
+ public function testGetSize(): void {
$f = $this->mockFutureFile();
$this->assertEquals(0, $f->getSize());
}
- public function testGet() {
+ public function testGet(): void {
$f = $this->mockFutureFile();
$stream = $f->get();
$this->assertTrue(is_resource($stream));
}
- public function testDelete() {
+ public function testDelete(): void {
$d = $this->getMockBuilder(Directory::class)
->disableOriginalConstructor()
- ->setMethods(['delete'])
+ ->onlyMethods(['delete'])
->getMock();
$d->expects($this->once())
->method('delete');
- $f = new \OCA\DAV\Upload\FutureFile($d, 'foo.txt');
+ $f = new FutureFile($d, 'foo.txt');
$f->delete();
}
-
- public function testPut() {
+
+ public function testPut(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$f = $this->mockFutureFile();
$f->put('');
}
-
- public function testSetName() {
+
+ public function testSetName(): void {
$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
$f = $this->mockFutureFile();
$f->setName('');
}
- /**
- * @return \OCA\DAV\Upload\FutureFile
- */
- private function mockFutureFile() {
+ private function mockFutureFile(): FutureFile {
$d = $this->getMockBuilder(Directory::class)
->disableOriginalConstructor()
- ->setMethods(['getETag', 'getLastModified', 'getChildren'])
+ ->onlyMethods(['getETag', 'getLastModified', 'getChildren'])
->getMock();
$d->expects($this->any())
@@ -109,6 +90,6 @@ class FutureFileTest extends \Test\TestCase {
->method('getChildren')
->willReturn([]);
- return new \OCA\DAV\Upload\FutureFile($d, 'foo.txt');
+ return new FutureFile($d, 'foo.txt');
}
}
diff --git a/apps/dav/tests/unit/Upload/UploadAutoMkcolPluginTest.php b/apps/dav/tests/unit/Upload/UploadAutoMkcolPluginTest.php
new file mode 100644
index 00000000000..baae839c8da
--- /dev/null
+++ b/apps/dav/tests/unit/Upload/UploadAutoMkcolPluginTest.php
@@ -0,0 +1,133 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\Upload;
+
+use Generator;
+use OCA\DAV\Upload\UploadAutoMkcolPlugin;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\DAV\ICollection;
+use Sabre\DAV\INode;
+use Sabre\DAV\Server;
+use Sabre\DAV\Tree;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Test\TestCase;
+
+class UploadAutoMkcolPluginTest extends TestCase {
+
+ private Tree&MockObject $tree;
+ private RequestInterface&MockObject $request;
+ private ResponseInterface&MockObject $response;
+
+ public static function dataMissingHeaderShouldReturnTrue(): Generator {
+ yield 'missing X-NC-WebDAV-Auto-Mkcol header' => [null];
+ yield 'empty X-NC-WebDAV-Auto-Mkcol header' => [''];
+ yield 'invalid X-NC-WebDAV-Auto-Mkcol header' => ['enable'];
+ }
+
+ public function testBeforeMethodWithRootNodeNotAnICollectionShouldReturnTrue(): void {
+ $this->request->method('getHeader')->willReturn('1');
+ $this->request->expects(self::once())
+ ->method('getPath')
+ ->willReturn('/non-relevant/path.txt');
+ $this->tree->expects(self::once())
+ ->method('nodeExists')
+ ->with('/non-relevant')
+ ->willReturn(false);
+
+ $mockNode = $this->getMockBuilder(INode::class);
+ $this->tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->willReturn($mockNode);
+
+ $return = $this->plugin->beforeMethod($this->request, $this->response);
+ $this->assertTrue($return);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataMissingHeaderShouldReturnTrue')]
+ public function testBeforeMethodWithMissingHeaderShouldReturnTrue(?string $header): void {
+ $this->request->expects(self::once())
+ ->method('getHeader')
+ ->with('X-NC-WebDAV-Auto-Mkcol')
+ ->willReturn($header);
+
+ $this->request->expects(self::never())
+ ->method('getPath');
+
+ $return = $this->plugin->beforeMethod($this->request, $this->response);
+ self::assertTrue($return);
+ }
+
+ public function testBeforeMethodWithExistingPathShouldReturnTrue(): void {
+ $this->request->method('getHeader')->willReturn('1');
+ $this->request->expects(self::once())
+ ->method('getPath')
+ ->willReturn('/files/user/deep/image.jpg');
+ $this->tree->expects(self::once())
+ ->method('nodeExists')
+ ->with('/files/user/deep')
+ ->willReturn(true);
+
+ $this->tree->expects(self::never())
+ ->method('getNodeForPath');
+
+ $return = $this->plugin->beforeMethod($this->request, $this->response);
+ self::assertTrue($return);
+ }
+
+ public function testBeforeMethodShouldSucceed(): void {
+ $this->request->method('getHeader')->willReturn('1');
+ $this->request->expects(self::once())
+ ->method('getPath')
+ ->willReturn('/files/user/my/deep/path/image.jpg');
+ $this->tree->expects(self::once())
+ ->method('nodeExists')
+ ->with('/files/user/my/deep/path')
+ ->willReturn(false);
+
+ $mockNode = $this->createMock(ICollection::class);
+ $this->tree->expects(self::once())
+ ->method('getNodeForPath')
+ ->with('/files')
+ ->willReturn($mockNode);
+ $mockNode->expects(self::exactly(4))
+ ->method('childExists')
+ ->willReturnMap([
+ ['user', true],
+ ['my', true],
+ ['deep', false],
+ ['path', false],
+ ]);
+ $mockNode->expects(self::exactly(2))
+ ->method('createDirectory');
+ $mockNode->expects(self::exactly(4))
+ ->method('getChild')
+ ->willReturn($mockNode);
+
+ $return = $this->plugin->beforeMethod($this->request, $this->response);
+ self::assertTrue($return);
+ }
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $server = $this->createMock(Server::class);
+ $this->tree = $this->createMock(Tree::class);
+
+ $server->tree = $this->tree;
+ $this->plugin = new UploadAutoMkcolPlugin();
+
+ $this->request = $this->createMock(RequestInterface::class);
+ $this->response = $this->createMock(ResponseInterface::class);
+ $server->httpRequest = $this->request;
+ $server->httpResponse = $this->response;
+
+ $this->plugin->initialize($server);
+ }
+}
diff --git a/apps/dav/tests/unit/bootstrap.php b/apps/dav/tests/unit/bootstrap.php
index cf7ea1cc842..ee76bb6677b 100644
--- a/apps/dav/tests/unit/bootstrap.php
+++ b/apps/dav/tests/unit/bootstrap.php
@@ -1,33 +1,21 @@
<?php
+
+declare(strict_types=1);
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
+
+use OCP\App\IAppManager;
+use OCP\Server;
+
if (!defined('PHPUNIT_RUN')) {
define('PHPUNIT_RUN', 1);
}
-require_once __DIR__.'/../../../../lib/base.php';
-
-\OC::$composerAutoloader->addPsr4('Test\\', OC::$SERVERROOT . '/tests/lib/', true);
-
-\OC_App::loadApp('dav');
+require_once __DIR__ . '/../../../../lib/base.php';
+require_once __DIR__ . '/../../../../tests/autoload.php';
-OC_Hook::clear();
+Server::get(IAppManager::class)->loadApp('dav');
diff --git a/apps/dav/tests/unit/phpunit.xml b/apps/dav/tests/unit/phpunit.xml
index 3f0a9107aaa..c85d07c6fcb 100644
--- a/apps/dav/tests/unit/phpunit.xml
+++ b/apps/dav/tests/unit/phpunit.xml
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ - SPDX-FileCopyrightText: 2015-2017 ownCloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+ -->
<phpunit bootstrap="bootstrap.php"
verbose="true"
timeoutForSmallTests="900"
@@ -22,4 +26,3 @@
<log type="coverage-clover" target="./clover.xml"/>
</logging>
</phpunit>
-
diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-1.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-1.ics
new file mode 100644
index 00000000000..e76ac3c9b2f
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-1.ics
@@ -0,0 +1,17 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VEVENT
+CREATED:20240507T105946Z
+LAST-MODIFIED:20240507T121113Z
+DTSTAMP:20240507T121113Z
+UID:07514c7b-1014-425c-b1b8-2c35ab0eea1d
+SUMMARY:Event A
+RRULE:FREQ=YEARLY
+DTSTART;TZID=Europe/Berlin:20240101T101500
+DTEND;TZID=Europe/Berlin:20240101T111500
+TRANSP:OPAQUE
+X-MOZ-GENERATION:4
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-2.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-2.ics
new file mode 100644
index 00000000000..fe948321d51
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-2.ics
@@ -0,0 +1,17 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VEVENT
+CREATED:20240507T110122Z
+LAST-MODIFIED:20240507T121120Z
+DTSTAMP:20240507T121120Z
+UID:67cf8134-ff10-49a7-913d-acfeda463db6
+SUMMARY:Event B
+RRULE:FREQ=YEARLY
+DTSTART;TZID=Europe/Berlin:20240101T123000
+DTEND;TZID=Europe/Berlin:20240101T133000
+TRANSP:OPAQUE
+X-MOZ-GENERATION:4
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-3.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-3.ics
new file mode 100644
index 00000000000..de7765b28d2
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-3.ics
@@ -0,0 +1,17 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VEVENT
+CREATED:20240507T120352Z
+LAST-MODIFIED:20240507T121128Z
+DTSTAMP:20240507T121128Z
+UID:59090ca1-e52b-447f-8e08-491d1da729fa
+SUMMARY:Event C
+RRULE:FREQ=YEARLY
+DTSTART;TZID=Europe/Berlin:20240101T151000
+DTEND;TZID=Europe/Berlin:20240101T161000
+TRANSP:OPAQUE
+X-MOZ-GENERATION:2
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-4.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-4.ics
new file mode 100644
index 00000000000..b4d2f752c0a
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-4.ics
@@ -0,0 +1,17 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VEVENT
+CREATED:20240507T120414Z
+LAST-MODIFIED:20240507T121134Z
+DTSTAMP:20240507T121134Z
+UID:b1814d32-9adf-4518-8535-37f2c037f423
+SUMMARY:Event D
+RRULE:FREQ=YEARLY
+DTSTART;TZID=Europe/Berlin:20240101T164500
+DTEND;TZID=Europe/Berlin:20240101T171500
+TRANSP:OPAQUE
+SEQUENCE:2
+X-MOZ-GENERATION:3
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-5.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-5.ics
new file mode 100644
index 00000000000..1cd8b7ebf13
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-5.ics
@@ -0,0 +1,14 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VEVENT
+CREATED:20240507T122221Z
+LAST-MODIFIED:20240507T122237Z
+DTSTAMP:20240507T122237Z
+UID:19c4e049-0b09-4101-a2ad-061a837e6a5e
+SUMMARY:Cake Tasting
+DTSTART;TZID=Europe/Berlin:20240509T151500
+DTEND;TZID=Europe/Berlin:20240509T171500
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-6.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-6.ics
new file mode 100644
index 00000000000..6c24d534281
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-6.ics
@@ -0,0 +1,15 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VEVENT
+CREATED:20240507T122246Z
+LAST-MODIFIED:20240507T175258Z
+DTSTAMP:20240507T175258Z
+UID:60a7d310-aa7b-4974-8a8a-ff9339367e1d
+SUMMARY:Pasta Day
+DTSTART;TZID=Europe/Berlin:20240514T123000
+DTEND;TZID=Europe/Berlin:20240514T133000
+TRANSP:OPAQUE
+X-MOZ-GENERATION:2
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-missing-start-1.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-missing-start-1.ics
new file mode 100644
index 00000000000..a7865eaf5ef
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/caldav-search-missing-start-1.ics
@@ -0,0 +1,14 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VEVENT
+CREATED:20240507T122246Z
+LAST-MODIFIED:20240507T175258Z
+DTSTAMP:20240507T175258Z
+UID:39e1b04f-d1cc-4622-bf97-11c38e070f43
+SUMMARY:Missing DTSTART 1
+DTEND;TZID=Europe/Berlin:20240514T133000
+TRANSP:OPAQUE
+X-MOZ-GENERATION:2
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-missing-start-2.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-missing-start-2.ics
new file mode 100644
index 00000000000..4a33f2b1c8a
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/caldav-search-missing-start-2.ics
@@ -0,0 +1,14 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VEVENT
+CREATED:20240507T122246Z
+LAST-MODIFIED:20240507T175258Z
+DTSTAMP:20240507T175258Z
+UID:12413feb-4b8c-4e95-ae7f-9ec4f42f3348
+SUMMARY:Missing DTSTART 2
+DTEND;TZID=Europe/Berlin:20240514T133000
+TRANSP:OPAQUE
+X-MOZ-GENERATION:2
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/example-event-default-expected.ics b/apps/dav/tests/unit/test_fixtures/example-event-default-expected.ics
new file mode 100644
index 00000000000..09606ca5ee4
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/example-event-default-expected.ics
@@ -0,0 +1,20 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Sabre//Sabre VObject 4.5.6//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:RANDOM-UID
+DTSTAMP:20250121T000000Z
+SUMMARY:Example event - open me!
+DTSTART:20250128T100000Z
+DTEND:20250128T110000Z
+DESCRIPTION:Welcome to Nextcloud Calendar!\n\nThis is a sample event - expl
+ ore the flexibility of planning with Nextcloud Calendar by making any edit
+ s you want!\n\nWith Nextcloud Calendar\, you can:\n- Create\, edit\, and m
+ anage events effortlessly.\n- Create multiple calendars and share them wit
+ h teammates\, friends\, or family.\n- Check availability and display your
+ busy times to others.\n- Seamlessly integrate with apps and devices via Ca
+ lDAV.\n- Customize your experience: schedule recurring events\, adjust not
+ ifications and other settings.
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/example-event-default-expected.ics.license b/apps/dav/tests/unit/test_fixtures/example-event-default-expected.ics.license
new file mode 100644
index 00000000000..23e2d6b1908
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/example-event-default-expected.ics.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/apps/dav/tests/unit/test_fixtures/example-event-expected.ics b/apps/dav/tests/unit/test_fixtures/example-event-expected.ics
new file mode 100644
index 00000000000..f9dfc37718e
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/example-event-expected.ics
@@ -0,0 +1,18 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//IDN nextcloud.com//Calendar app 5.2.0-dev.1//EN
+BEGIN:VEVENT
+CREATED:20250128T091147Z
+DTSTAMP:20250128T091507Z
+LAST-MODIFIED:20250128T091507Z
+SEQUENCE:2
+STATUS:CONFIRMED
+SUMMARY:Welcome!
+DESCRIPTION:Welcome!!!
+LOCATION:Test
+UID:RANDOM-UID
+DTSTART:20250128T100000Z
+DTEND:20250128T110000Z
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/example-event-expected.ics.license b/apps/dav/tests/unit/test_fixtures/example-event-expected.ics.license
new file mode 100644
index 00000000000..23e2d6b1908
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/example-event-expected.ics.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/apps/dav/tests/unit/test_fixtures/example-event-with-attendees.ics b/apps/dav/tests/unit/test_fixtures/example-event-with-attendees.ics
new file mode 100644
index 00000000000..8018552f2a5
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/example-event-with-attendees.ics
@@ -0,0 +1,21 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//IDN nextcloud.com//Calendar app 5.2.0-dev.1//EN
+BEGIN:VEVENT
+CREATED:20250128T091147Z
+DTSTAMP:20250128T091507Z
+LAST-MODIFIED:20250128T091507Z
+SEQUENCE:2
+UID:3b4df6a8-84df-43d5-baf9-377b43390b70
+DTSTART;VALUE=DATE:20250130
+DTEND;VALUE=DATE:20250131
+STATUS:CONFIRMED
+SUMMARY:Welcome!
+DESCRIPTION:Welcome!!!
+LOCATION:Test
+ATTENDEE;CN=user a;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICI
+ PANT;RSVP=TRUE;LANGUAGE=en;SCHEDULE-STATUS=1.1:mailto:usera@imap.localhost
+ORGANIZER;CN=Admin Account:mailto:admin@imap.localhost
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/example-event-with-attendees.ics.license b/apps/dav/tests/unit/test_fixtures/example-event-with-attendees.ics.license
new file mode 100644
index 00000000000..23e2d6b1908
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/example-event-with-attendees.ics.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/apps/dav/tests/unit/test_fixtures/example-event.ics b/apps/dav/tests/unit/test_fixtures/example-event.ics
new file mode 100644
index 00000000000..6fc1848ea52
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/example-event.ics
@@ -0,0 +1,18 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//IDN nextcloud.com//Calendar app 5.2.0-dev.1//EN
+BEGIN:VEVENT
+CREATED:20250128T091147Z
+DTSTAMP:20250128T091507Z
+LAST-MODIFIED:20250128T091507Z
+SEQUENCE:2
+UID:3b4df6a8-84df-43d5-baf9-377b43390b70
+STATUS:CONFIRMED
+SUMMARY:Welcome!
+DESCRIPTION:Welcome!!!
+LOCATION:Test
+DTSTART:20250204T100000Z
+DTEND:20250204T110000Z
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/example-event.ics.license b/apps/dav/tests/unit/test_fixtures/example-event.ics.license
new file mode 100644
index 00000000000..23e2d6b1908
--- /dev/null
+++ b/apps/dav/tests/unit/test_fixtures/example-event.ics.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+SPDX-License-Identifier: AGPL-3.0-or-later